diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 3aa5c35..00783b3 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -31,11 +31,3 @@ body: description: We warmly welcome any recommendations on potential fixes, insights, or considerations that contributors should keep in mind when working to resolve this issue. validations: required: false - - - type: checkboxes - id: agreement - attributes: - label: 'Please confirm the following:' - options: - - label: 'I have searched the existing issues and confirm this is a new bug report.' - required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index ec38843..b2b5c36 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -23,12 +23,3 @@ body: description: "Do you have an idea for how this could be implemented? Please include those details here." validations: required: false - - - type: checkboxes - id: agreement - attributes: - label: 'Please confirm the following:' - description: 'Understanding that feature requests are evaluated for their applicability and benefit to the community, and not all requests may be implemented.' - options: - - label: 'I have searched the existing issues and confirm this is a new feature request.' - required: true diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 4a170c7..0fee582 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -23,4 +23,4 @@ jobs: run: | black --check qbraid_qir tests tools examples isort --check-only qbraid_qir tests tools - python tools/verify_headers.py qbraid_qir tests \ No newline at end of file + qbraid admin headers qbraid_qir tests \ No newline at end of file diff --git a/CITATION.cff b/CITATION.cff index adeb8b9..423a678 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -6,6 +6,9 @@ message: >- metadata from this file. type: software authors: + - given-names: Harshit + family-names: Gupta + affiliation: qBraid Co. - given-names: Samuel family-names: Kushnir affiliation: University of Maryland @@ -15,15 +18,12 @@ authors: - given-names: Priyansh family-names: Parakh affiliation: University of Toronto - - given-names: Harshit - family-names: Gupta - affiliation: VISA Inc. - given-names: Ryan James family-names: Hill affiliation: qBraid Co. repository-code: 'https://github.com/qBraid/qbraid-qir' -url: 'https://docs.qbraid.com/projects/qir/en/v0.1.1/' -repository-artifact: 'https://github.com/qBraid/qbraid-qir/releases/tag/v0.1.1' +url: 'https://docs.qbraid.com/projects/qir/en/v0.2.0/' +repository-artifact: 'https://github.com/qBraid/qbraid-qir/releases/tag/v0.2.0' keywords: - python - quantum-computing @@ -32,5 +32,5 @@ keywords: - cirq - openasm license: GPL-3.0 -version: 0.1.1 -date-released: '2024-03-04' \ No newline at end of file +version: 0.2.0 +date-released: '2024-05-09' \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in index 2b5e09b..f76bd64 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,4 @@ include *.txt *.md recursive-include docs *.rst *.py *.png *.ico *.txt recursive-include qbraid_qir *.py -recursive-include tools *.py prune docs/build/ \ No newline at end of file diff --git a/README.md b/README.md index 2c5e40b..ab081ed 100644 --- a/README.md +++ b/README.md @@ -181,10 +181,10 @@ The BibTeX entry below is aligned with the latest stable release. For the most u citation details, please refer to [CITATION.cff](CITATION.cff). ```tex -@software{Kushnir_qBraid-QIR_Python_package_2024, -author = {Kushnir, Samuel and Gupta, Harshit and Jain, Rohan and Parakh, Priyansh and Hill, Ryan James}, +@software{Gupta_qBraid-QIR_Python_package_2024, +author = {Gupta, Harshit and Kushnir, Samuel and Jain, Rohan and Parakh, Priyansh and Hill, Ryan James}, license = {GPL-3.0}, -month = mar, +month = may, title = {{qBraid-QIR: Python package for QIR conversions, integrations, and utilities.}}, url = {https://github.com/qBraid/qbraid-qir}, version = {0.2.0}, diff --git a/docs/api/qbraid_qir.runner.rst b/docs/api/qbraid_qir.runner.rst deleted file mode 100644 index f420048..0000000 --- a/docs/api/qbraid_qir.runner.rst +++ /dev/null @@ -1,7 +0,0 @@ -qbraid_qir.runner -================== - -.. automodule:: qbraid_qir.runner - :members: - :undoc-members: - :show-inheritance: \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index 1127381..f09d7b8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,19 +9,13 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -# -import os -import sys import qbraid_qir -sys.path.insert(0, os.path.abspath("..")) -sys.path.insert(0, os.path.abspath("../../qbraid_qir")) - # -- Project information ----------------------------------------------------- project = "qbraid-qir" -copyright = "2023, qBraid Development Team" +copyright = "2024, qBraid Development Team" author = "qBraid Development Team" # The full version, including alpha/beta/rc tags diff --git a/docs/index.rst b/docs/index.rst index dc87c4d..6dffcf8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -176,7 +176,6 @@ This project was conceived in cooperation with the Quantum Open Source Foundatio api/qbraid_qir api/qbraid_qir.cirq api/qbraid_qir.qasm3 - api/qbraid_qir.runner Indices and Tables diff --git a/pyproject.toml b/pyproject.toml index 62f644e..7f2e89d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,8 +7,8 @@ name = "qbraid-qir" dynamic = ["version"] description = "qBraid-SDK extension providing support for QIR conversions." readme = "README.md" -authors = [{ name = "qBraid Development Team", email = "contact@qbraid.com" }] -license = { file = "LICENSE" } +authors = [{name = "qBraid Development Team"}, {email = "contact@qbraid.com"}] +license = {text = "GNU General Public License v3.0"} keywords = ["qbraid", "quantum", "qir", "llvm", "cirq", "openqasm"] classifiers = [ "Intended Audience :: Developers", @@ -25,7 +25,7 @@ classifiers = [ "Topic :: Scientific/Engineering :: Physics" ] dependencies = ["pyqir~=0.10.0", "numpy"] -requires-python = ">= 3.9" +requires-python = ">=3.9" [project.urls] Homepage = "https://github.com/qBraid/qbraid-qir" @@ -38,7 +38,7 @@ Discord = "https://discord.gg/TPBU2sa8Et" cirq = ["cirq-core>=1.3.0,<1.4.0"] qasm3 = ["openqasm3[parser]>=0.4.0,<0.6.0"] test = ["qbraid>=0.5.3,<0.7.0", "pytest", "pytest-cov"] -lint = ["black[jupyter]", "isort", "pylint"] +lint = ["black[jupyter]", "isort", "pylint", "qbraid-cli"] docs = ["sphinx~=7.3.7", "sphinx-autodoc-typehints>=1.24,<2.2", "sphinx-rtd-theme~=2.0.0", "docutils<0.22"] [tool.setuptools.dynamic] @@ -74,6 +74,5 @@ exclude_lines = [ parallel = true source = ["qbraid_qir"] omit = [ - "**/qbraid_qir/runner/simulator.py", "**/qbraid_qir/__init__.py" ] diff --git a/qbraid_qir/__init__.py b/qbraid_qir/__init__.py index 59d998c..20bb103 100644 --- a/qbraid_qir/__init__.py +++ b/qbraid_qir/__init__.py @@ -43,7 +43,7 @@ "dumps", ] -_lazy_mods = ["cirq", "qasm3", "runner"] +_lazy_mods = ["cirq", "qasm3"] def __getattr__(name): diff --git a/qbraid_qir/_version.py b/qbraid_qir/_version.py index 3daad7a..2aa26df 100644 --- a/qbraid_qir/_version.py +++ b/qbraid_qir/_version.py @@ -14,4 +14,4 @@ Version number (major.minor.patch[-label]) """ -__version__ = "0.2.0.dev" +__version__ = "0.2.0" diff --git a/qbraid_qir/qasm3/elements.py b/qbraid_qir/qasm3/elements.py index 43e2abf..120271b 100644 --- a/qbraid_qir/qasm3/elements.py +++ b/qbraid_qir/qasm3/elements.py @@ -64,6 +64,7 @@ class Variable: """ + # pylint: disable-next=too-many-arguments def __init__(self, name, base_type, base_size, dims, value, is_constant=False): self.name = name self.base_type = base_type diff --git a/qbraid_qir/qasm3/visitor.py b/qbraid_qir/qasm3/visitor.py index 20e2ab3..c358f16 100644 --- a/qbraid_qir/qasm3/visitor.py +++ b/qbraid_qir/qasm3/visitor.py @@ -17,7 +17,7 @@ import sys from abc import ABCMeta, abstractmethod -# pylint: disable=too-many-instance-attributes +# pylint: disable=too-many-instance-attributes,too-many-lines from collections import deque from typing import Any, List, Optional, Tuple, Union @@ -706,12 +706,15 @@ def _validate_variable_assignment_value(self, variable: Variable, value: Any) -> qasm_type = base_type.__class__ try: type_to_match = VARIABLE_TYPE_MAP[qasm_type] - except KeyError: - raise Qasm3ConversionError(f"Invalid type {qasm_type} for variable {variable.name}") + except KeyError as err: + raise Qasm3ConversionError( + f"Invalid type {qasm_type} for variable {variable.name}" + ) from err if not isinstance(value, type_to_match): raise Qasm3ConversionError( - f"Invalid assignment of type {type(value)} to variable {variable.name} of type {qasm_type}" + f"Invalid assignment of type {type(value)} to variable {variable.name} " + f"of type {qasm_type}" ) # check 2 - range match , if bits mentioned in base size @@ -725,13 +728,15 @@ def _validate_variable_assignment_value(self, variable: Variable, value: Any) -> left, right = 0, 2**base_size - 1 if value < left or value > right: raise Qasm3ConversionError( - f"Value {value} out of limits for variable {variable.name} with base size {base_size}" + f"Value {value} out of limits for variable {variable.name} " + f"with base size {base_size}" ) elif type_to_match == float: base_size = variable.base_size left, right = 0, 0 - # Reference : https://openqasm.com/language/types.html#floating-point-numbers and IEEE 754 standard + # IEEE 754 Standard for floats + # https://openqasm.com/language/types.html#floating-point-numbers if base_size == 32: left, right = -(1.70141183 * (10**38)), (1.70141183 * (10**38)) else: @@ -739,7 +744,8 @@ def _validate_variable_assignment_value(self, variable: Variable, value: Any) -> if value < left or value > right: raise Qasm3ConversionError( - f"Value {value} out of limits for variable {variable.name} with base size {base_size}" + f"Value {value} out of limits for variable {variable.name} " + f"with base size {base_size}" ) elif type_to_match == bool: pass @@ -768,8 +774,8 @@ def _visit_constant_declaration(self, statement: ConstantDeclaration) -> None: self._print_err_location(statement.span) raise Qasm3ConversionError(f"Re-declaration of variable {var_name}") - # TODO: extend to checking that only CONST vars are allowed when instantiating - # a constant variable + # TODO: extend to checking that only CONST vars are allowed + # when instantiating a constant variable init_value = self._evaluate_expression(statement.init_expression) base_type = statement.type @@ -880,8 +886,8 @@ def _analyse_classical_indices(self, indices: List[List[Any]], var_name: str) -> if len(indices) != len(var_dimensions): self._print_err_location(indices[0][0].span) raise Qasm3ConversionError( - f"Invalid number of indices for variable {var_name}. Expected {len(var_dimensions)} " - f"but got {len(indices)}" + f"Invalid number of indices for variable {var_name}. " + f"Expected {len(var_dimensions)} but got {len(indices)}" ) for i, index in enumerate(indices): @@ -904,7 +910,7 @@ def _analyse_classical_indices(self, indices: List[List[Any]], var_name: str) -> raise Qasm3ConversionError( f"Index {index_value} out of bounds for dimension {i+1} of variable {var_name}" ) - # Column major representation : https://en.wikipedia.org/wiki/Row-_and_column-major_order + # Column major representation: https://en.wikipedia.org/wiki/Row-_and_column-major_order flat_index = flat_index + multiplier * index_value multiplier = multiplier * curr_dimension @@ -1149,6 +1155,7 @@ def visit_contextual_statement(self, statement: Statement) -> None: def visit_scoped_statement(self, statement: Statement) -> None: pass + # pylint: disable-next=too-many-branches def visit_statement(self, statement: Statement) -> None: """Visit a statement element. diff --git a/qbraid_qir/runner/__init__.py b/qbraid_qir/runner/__init__.py deleted file mode 100644 index f131f9e..0000000 --- a/qbraid_qir/runner/__init__.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright (C) 2024 qBraid -# -# This file is part of the qBraid-SDK -# -# The qBraid-SDK is free software released under the GNU General Public License v3 -# or later. You can redistribute and/or modify it under the terms of the GPL v3. -# See the LICENSE file in the project root or . -# -# THERE IS NO WARRANTY for the qBraid-SDK, as per Section 15 of the GPL v3. - -""" -Module containing tools for executing QIR programs - -.. currentmodule:: qbraid_qir.runner - -Classes ---------- - -.. autosummary:: - :toctree: ../stubs/ - - Simulator - Result - - -Exceptions ------------ - -.. autosummary:: - :toctree: ../stubs/ - - QirRunnerError - -""" -from .exceptions import QirRunnerError -from .result import Result -from .simulator import Simulator diff --git a/qbraid_qir/runner/exceptions.py b/qbraid_qir/runner/exceptions.py deleted file mode 100644 index 66f6206..0000000 --- a/qbraid_qir/runner/exceptions.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (C) 2024 qBraid -# -# This file is part of the qBraid-SDK -# -# The qBraid-SDK is free software released under the GNU General Public License v3 -# or later. You can redistribute and/or modify it under the terms of the GPL v3. -# See the LICENSE file in the project root or . -# -# THERE IS NO WARRANTY for the qBraid-SDK, as per Section 15 of the GPL v3. - -""" -Module defining exceptions for errors raised during QIR program execution. - -""" -from qbraid_qir.exceptions import QbraidQirError - - -class QirRunnerError(QbraidQirError): - """Class for errors raised when executing QIR programs.""" diff --git a/qbraid_qir/runner/result.py b/qbraid_qir/runner/result.py deleted file mode 100644 index 6f368b5..0000000 --- a/qbraid_qir/runner/result.py +++ /dev/null @@ -1,117 +0,0 @@ -# Copyright (C) 2024 qBraid -# -# This file is part of the qBraid-SDK -# -# The qBraid-SDK is free software released under the GNU General Public License v3 -# or later. You can redistribute and/or modify it under the terms of the GPL v3. -# See the LICENSE file in the project root or . -# -# THERE IS NO WARRANTY for the qBraid-SDK, as per Section 15 of the GPL v3. - -""" -Module defining qir-runner sparse quantum state simulator result class. - -""" -from collections import defaultdict -from typing import Dict, List, Optional - -import numpy as np - - -class Result: - def __init__(self, stdout: str, execution_duration: Optional[float] = None): - self._raw_output = stdout - self._parsed_data = self._parse_results(stdout) - self._measurements = self._data_to_measurements() - self._execution_duration = execution_duration - self._cached_histogram = None - self._cached_metadata = None - - @property - def measurements(self) -> np.ndarray: - """Return the measurement results as a 2D numpy array.""" - return self._measurements - - def measurement_counts(self, decimal: bool = False) -> Dict[str, int]: - """Dynamically calculates and returns the histogram of measurement results.""" - counts = self._array_to_histogram(self.measurements) - - if decimal is True: - counts = {int(key, 2): value for key, value in counts.items()} - - return counts - - def measurement_probabilities(self, **kwargs) -> Dict[str, float]: - """Calculate and return the probabilities of each measurement result.""" - counts = self.measurement_counts(**kwargs) - probabilities = self.counts_to_probabilities(counts) - return probabilities - - def _parse_results(self, output: str) -> Dict[str, List[int]]: - """Parse the raw output from the execution to extract measurement results.""" - results = defaultdict(list) - current_shot_results = [] - - for line in output.splitlines(): - elements = line.split() - if len(elements) == 3 and elements[:2] == ["OUTPUT", "RESULT"]: - _, _, bit = elements - current_shot_results.append(int(bit)) - elif line.startswith("END"): - for idx, result in enumerate(current_shot_results): - results[f"q{idx}"].append(result) - current_shot_results = [] - - return dict(results) - - def _data_to_measurements(self) -> np.ndarray: - """Convert parsed data to a 2D array of measurement results.""" - if not self._parsed_data: - return np.array([], dtype=np.int8) - return np.array( - [self._parsed_data[key] for key in sorted(self._parsed_data.keys())], dtype=np.int8 - ).T - - def _array_to_histogram(self, arr: np.ndarray) -> Dict[str, int]: - """Implement caching mechanism here.""" - if self._cached_histogram is None: - row_strings = ["".join(map(str, row)) for row in arr] - self._cached_histogram = {row: row_strings.count(row) for row in set(row_strings)} - return self._cached_histogram - - @staticmethod - def counts_to_probabilities(counts: Dict[str, int]) -> Dict[str, float]: - """ - Convert histogram counts to probabilities. - - Args: - counts (Dict[str, int]): A dictionary with measurement outcomes as keys and - their counts as values. - - Returns: - Dict[str, float]: A dictionary with measurement outcomes as keys and their - probabilities as values. - """ - total_counts = sum(counts.values()) - measurement_probabilities = { - outcome: count / total_counts for outcome, count in counts.items() - } - return measurement_probabilities - - def metadata(self) -> Dict[str, int]: - """Return metadata about the measurement results.""" - if self._cached_metadata is None: - num_shots, num_qubits = self.measurements.shape - self._cached_metadata = { - "num_shots": num_shots, - "num_qubits": num_qubits, - "execution_duration": self._execution_duration, - "measurements": self.measurements, - "measurement_counts": self.measurement_counts(), - "measurement_probabilities": self.measurement_probabilities(), - } - - return self._cached_metadata - - def __repr__(self) -> str: - return f"Result({self.metadata()})" diff --git a/qbraid_qir/runner/simulator.py b/qbraid_qir/runner/simulator.py deleted file mode 100644 index d1e5578..0000000 --- a/qbraid_qir/runner/simulator.py +++ /dev/null @@ -1,167 +0,0 @@ -# Copyright (C) 2024 qBraid -# -# This file is part of the qBraid-SDK -# -# The qBraid-SDK is free software released under the GNU General Public License v3 -# or later. You can redistribute and/or modify it under the terms of the GPL v3. -# See the LICENSE file in the project root or . -# -# THERE IS NO WARRANTY for the qBraid-SDK, as per Section 15 of the GPL v3. - -""" -Module containing Python wrapper for the qir-runner sparse quantum state simulator. - -""" -import re -import shutil -import subprocess -import time -import warnings -from typing import Dict, List, Optional - -from .exceptions import QirRunnerError -from .result import Result - - -def _is_valid_semantic_version(v: str) -> bool: - """ - Returns True if given string represents a valid - semantic version, False otherwise. - - """ - try: - # pylint: disable-next=import-outside-toplevel - from packaging import version - - version.Version(v) - return True - except ImportError: - # Fallback to regex matching if packaging is not installed - semantic_version_pattern = re.compile( - r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)" - r"(-([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?" - r"(\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?$" - ) - return bool(semantic_version_pattern.match(v)) - except version.InvalidVersion: - return False - - -class Simulator: - """A sparse simulator that extends the functionality of the qir-runner. - - This simulator is a Python wrapper for the qir-runner, a command-line tool - for executing compiled QIR files. It uses sparse matrices to represent quantum - states and can be used to simulate quantum circuits that have been compiled to QIR. - The simulator allows for setting a seed for random number generation and specifying - an entry point for the execution. - - The qir-runner can be found at: https://github.com/qir-alliance/qir-runner - - Attributes: - seed (optional, int): The value to use when seeding the random number generator used - for quantum simulation. - qir_runner (str): Path to the qir-runner executable. - version (str): The version of the qir-runner executable. - """ - - def __init__(self, seed: Optional[int] = None, qir_runner_path: Optional[str] = None): - self.seed = seed - self._version = None - self._qir_runner = None - self.qir_runner = qir_runner_path - if not _is_valid_semantic_version(self.version): - warnings.warn( - f"Invalid qir-runner version '{self.version}' detected. Executable may be corrupt." - ) - - @property - def qir_runner(self) -> str: - """Path to the qir-runner executable.""" - return self._qir_runner - - @qir_runner.setter - def qir_runner(self, value: Optional[str]) -> None: - """Set the qir-runner path with additional validation.""" - resolved_path = shutil.which(value or "qir-runner") - if resolved_path is None: - if value is None: - error_message = "No value was provided for the qir_runner_path, \ - and the qir-runner executable was not found in the system PATH." - else: - error_message = f"The provided qir-runner executable path '{value}' does not exist." - raise FileNotFoundError(error_message) - - self._qir_runner = resolved_path - self._version = None # Reset version cache since qir_runner changed - - @property - def version(self) -> str: - """Get the version of the qir-runner executable, caching the result.""" - if self._version is None: - if self._qir_runner is None: - raise ValueError("qir-runner path is not set.") - output = self._execute_subprocess( - [self.qir_runner, "--version"], stderr=subprocess.STDOUT - ) - self._version = output.strip().split()[-1] - return self._version - - @staticmethod - def _execute_subprocess(command: List[str], text: bool = True, **kwargs) -> str: - """Execute a subprocess command and return its output. - - Args: - command (list): The command to execute as a list of arguments. - - Returns: - str: The output from the command execution. - - Raises: - QirRunnerError: If there's an error executing the command. - """ - try: - return subprocess.check_output(command, text=text, **kwargs) - except (subprocess.CalledProcessError, OSError) as err: - raise QirRunnerError(f"Error executing qir-runner command: {command}") from err - - def run( - self, file_name: str, entrypoint: Optional[str] = None, shots: Optional[int] = None - ) -> Dict[str, List[int]]: - """Runs the qir-runner executable with the given QIR file and shots. - - Args: - file_name (str): Path to the QIR file to run ('.ll' or '.bc' file extension) - entrypoint (optional, str): Name of the entrypoint function to execute in the QIR file. - shots (optional, int): The number of times to repeat the execution of the chosen entry - point in the program. Defaults to 1. - - Returns: - A dictionary mapping 'qubit_i' to a list of measurement results. - """ - # Build the command with required and optional arguments - command = [self.qir_runner, "--shots", str(shots), "-f", file_name] - if entrypoint: - command.extend(["-e", entrypoint]) - if self.seed is not None: - command.extend(["-r", str(self.seed)]) - - # Execute the qir-runner with the built command - start = time.time() - raw_out = self._execute_subprocess(command) - stop = time.time() - miliseconds = int((stop - start) * 1000) - return Result(raw_out, execution_duration=miliseconds) - - def __eq__(self, other): - """Check if two Simulator instances are equal based on their attributes.""" - if not isinstance(other, Simulator): - return NotImplemented - return ( - (self.seed == other.seed) - and (self.qir_runner == other.qir_runner) - and (self.version == other.version) - ) - - def __repr__(self): - return f"Simulator(seed={self.seed}, qir_runner={self.qir_runner}, version={self.version})" diff --git a/tests/qasm3_qir/fixtures/variables.py b/tests/qasm3_qir/fixtures/variables.py index 003e5c5..8d23f87 100644 --- a/tests/qasm3_qir/fixtures/variables.py +++ b/tests/qasm3_qir/fixtures/variables.py @@ -125,7 +125,10 @@ int x = 3; x = 4.3232; """, - "Invalid assignment of type to variable x of type ", + ( + "Invalid assignment of type to variable x " + "of type " + ), ), "int_out_of_range": ( """ @@ -152,7 +155,7 @@ float[32] x = 123456789123456789123456789123456789123456789.1; """, - rf"Value .* out of limits for variable x with base size 32", + "Value .* out of limits for variable x with base size 32", ), "indexing_non_array": ( """ diff --git a/tests/runner/__init__.py b/tests/runner/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/runner/test_result.py b/tests/runner/test_result.py deleted file mode 100644 index e8d80cb..0000000 --- a/tests/runner/test_result.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright (C) 2024 qBraid -# -# This file is part of the qBraid-SDK -# -# The qBraid-SDK is free software released under the GNU General Public License v3 -# or later. You can redistribute and/or modify it under the terms of the GPL v3. -# See the LICENSE file in the project root or . -# -# THERE IS NO WARRANTY for the qBraid-SDK, as per Section 15 of the GPL v3. - -""" -Unit tests for qir-runner Python result wrapper. - -""" -import numpy as np - -from qbraid_qir.runner.result import Result - -stdout = """ -START -METADATA entry_point -METADATA output_labeling_schema -METADATA qir_profiles custom -METADATA required_num_qubits 2 -METADATA required_num_results 2 -OUTPUT RESULT 1 -OUTPUT RESULT 1 -END 0 -START -METADATA entry_point -METADATA output_labeling_schema -METADATA qir_profiles custom -METADATA required_num_qubits 2 -METADATA required_num_results 2 -OUTPUT RESULT 1 -OUTPUT RESULT 1 -END 0 -START -METADATA entry_point -METADATA output_labeling_schema -METADATA qir_profiles custom -METADATA required_num_qubits 2 -METADATA required_num_results 2 -OUTPUT RESULT 1 -OUTPUT RESULT 1 -END 0 -START -METADATA entry_point -METADATA output_labeling_schema -METADATA qir_profiles custom -METADATA required_num_qubits 2 -METADATA required_num_results 2 -OUTPUT RESULT 0 -OUTPUT RESULT 0 -END 0 -START -METADATA entry_point -METADATA output_labeling_schema -METADATA qir_profiles custom -METADATA required_num_qubits 2 -METADATA required_num_results 2 -OUTPUT RESULT 1 -OUTPUT RESULT 1 -END 0 -""" - - -def test_result(): - """Test the Result class.""" - result = Result(stdout, execution_duration=100) - parsed_expected = {"q0": [1, 1, 1, 0, 1], "q1": [1, 1, 1, 0, 1]} - measurements_expected = np.array([[1, 1], [1, 1], [1, 1], [0, 0], [1, 1]]) - counts_expected = {"00": 1, "11": 4} - counts_decimal_expected = {0: 1, 3: 4} - probabilities_expected = {"00": 0.2, "11": 0.8} - metadata_expected = { - "num_shots": 5, - "num_qubits": 2, - "execution_duration": 100, - "measurements": measurements_expected, - "measurement_counts": counts_expected, - "measurement_probabilities": probabilities_expected, - } - assert result._parsed_data == parsed_expected - assert np.array_equal(result.measurements, measurements_expected) - assert result.measurement_counts() == counts_expected - assert result.measurement_counts(decimal=True) == counts_decimal_expected - assert result.measurement_probabilities() == probabilities_expected - - metadata_out = result.metadata() - assert metadata_out["num_shots"] == metadata_expected["num_shots"] - assert metadata_out["num_qubits"] == metadata_expected["num_qubits"] - assert metadata_out["execution_duration"] == metadata_expected["execution_duration"] - assert repr(result).startswith("Result") diff --git a/tests/runner/test_simulator.py b/tests/runner/test_simulator.py deleted file mode 100644 index 00f5af0..0000000 --- a/tests/runner/test_simulator.py +++ /dev/null @@ -1,156 +0,0 @@ -# Copyright (C) 2024 qBraid -# -# This file is part of the qBraid-SDK -# -# The qBraid-SDK is free software released under the GNU General Public License v3 -# or later. You can redistribute and/or modify it under the terms of the GPL v3. -# See the LICENSE file in the project root or . -# -# THERE IS NO WARRANTY for the qBraid-SDK, as per Section 15 of the GPL v3. - -""" -Unit tests for qir-runner Python simulator wrapper. - -""" -import random -import shutil -from typing import Optional - -import cirq -import numpy as np -import pytest - -from qbraid_qir import dumps -from qbraid_qir.cirq import cirq_to_qir -from qbraid_qir.runner.result import Result -from qbraid_qir.runner.simulator import Simulator, _is_valid_semantic_version - -skip_runner_tests = shutil.which("qir-runner") is None -REASON = "qir-runner executable not available" - -# pylint: disable=redefined-outer-name - - -def _is_uniform_comput_basis(array: np.ndarray) -> bool: - """ - Check if each measurement (row) in the array represents a uniform computational basis - state, i.e., for each shot, that qubit measurements are either all |0⟩s or all |1⟩s. - - Args: - array (np.ndarray): A 2D numpy array where each row represents a measurement shot, - and each column represents a qubit's state in that shot. - - Returns: - bool: True if every measurement is in a uniform computational basis state - (all |0⟩s or all |1⟩s). False otherwise. - - Raises: - ValueError: If the given array is not 2D. - """ - if array.ndim != 2: - raise ValueError("The input array must be 2D.") - - for shot in array: - # Check if all qubits in the shot are measured as |0⟩ or all as |1⟩ - if not (np.all(shot == 0) or np.all(shot == 1)): - return False - return True - - -def _sparse_circuit(num_qubits: Optional[int] = None) -> cirq.Circuit: - """ - Generates a quantum circuit designed to benchmark the performance of a sparse simulator. - - This circuit is structured to maintain a level of sparsity in the system's state vector, making - it a good candidate for testing sparse quantum simulators. Sparse simulators excel in - simulating circuits where the state vector remains sparse, i.e., most of its elements are zero - or can be efficiently represented. - - Args: - num_qubits (optional, int): The number of qubits to use in the circuit. If not provided, - a random number of qubits between 10 and 20 will be used. - - Returns: - cirq.Circuit: The constructed circuit for benchmarking - """ - num_qubits = num_qubits or random.randint(10, 20) - # Create a circuit - circuit = cirq.Circuit() - - # Create qubits - qubits = cirq.LineQubit.range(num_qubits) - - # Apply Hadamard gates to the first half of the qubits - for qubit in qubits[: num_qubits // 2]: - circuit.append(cirq.H(qubit)) - - # Apply a CNOT ladder - for i in range(num_qubits - 1): - circuit.append(cirq.CNOT(qubits[i], qubits[i + 1])) - - # Apply Z gates to randomly selected qubits - for qubit in random.sample(qubits, k=num_qubits // 2): - circuit.append(cirq.Z(qubit)) - - # Measurement (optional) - circuit.append(cirq.measure(*qubits, key="result")) - - return circuit - - -@pytest.fixture -def cirq_sparse(): - """Cirq circuit used for testing.""" - yield _sparse_circuit - - -@pytest.mark.skipif(skip_runner_tests, reason=REASON) -def test_sparse_simulator(cirq_sparse): - """Test qir-runner sparse simulator python wrapper(s).""" - circuit = cirq_sparse() - num_qubits = len(circuit.all_qubits()) - - file_prefix = "sparse_simulator_test" - module = cirq_to_qir(circuit, name=file_prefix) - dumps(module) - simulator = Simulator() - - shots = random.randint(500, 1000) - result = simulator.run(f"{file_prefix}.bc", shots=shots) - assert isinstance(result, Result) - - counts = result.measurement_counts() - probabilities = result.measurement_probabilities() - assert len(counts) == len(probabilities) == 2 - assert sum(probabilities.values()) == 1.0 - - metadata = result.metadata() - assert metadata["num_shots"] == shots - assert metadata["num_qubits"] == num_qubits - assert isinstance(metadata["execution_duration"], float) - - measurements = result.measurements - assert _is_uniform_comput_basis(measurements) - - -@pytest.mark.parametrize( - "version_str, expected", - [ - ("1.0.0", True), - ("0.1.2", True), - ("2.0.0-rc.1", True), - ("1.0.0-alpha+001", True), - ("1.2.3+meta-valid", True), - ("+invalid", False), # no major, minor or patch version - ("-invalid", False), # no major, minor or patch version - ("1.0.0-", False), # pre-release info cannot be empty if hyphen is present - ("1.0.0+", False), # build metadata cannot be empty if plus is present - ("1.0.0+meta/valid", False), # build metadata contains invalid characters - ("1.0.0-alpha", True), - ("1.1.2+meta-123", True), - ("1.1.2+meta.123", True), - ], -) -def test_is_valid_semantic_version(version_str, expected): - """Test the _is_valid_semantic_version function used to verify qir-runner setup.""" - assert _is_valid_semantic_version(version_str) == expected diff --git a/tools/verify_headers.py b/tools/verify_headers.py deleted file mode 100755 index 0397439..0000000 --- a/tools/verify_headers.py +++ /dev/null @@ -1,173 +0,0 @@ -# Copyright (C) 2023 qBraid -# -# This file is part of the qBraid-SDK -# -# The qBraid-SDK is free software released under the GNU General Public License v3 -# or later. You can redistribute and/or modify it under the terms of the GPL v3. -# See the LICENSE file in the project root or . -# -# THERE IS NO WARRANTY for the qBraid-SDK, as per Section 15 of the GPL v3. - -# pylint: skip-file - -""" -Script to verify qBraid copyright file headers - -""" -import os -import sys - -# ANSI escape codes -BLUE = "\033[94m" -BOLD = "\033[1m" -RESET = "\033[0m" - -header = """# Copyright (C) 2024 qBraid -# -# This file is part of the qBraid-SDK -# -# The qBraid-SDK is free software released under the GNU General Public License v3 -# or later. You can redistribute and/or modify it under the terms of the GPL v3. -# See the LICENSE file in the project root or . -# -# THERE IS NO WARRANTY for the qBraid-SDK, as per Section 15 of the GPL v3. -""" - -header_2023 = header.replace("2024", "2023") - -skip_files = [] - -failed_headers = [] -fixed_headers = [] - - -def should_skip(file_path: str, content: str) -> bool: - if file_path in skip_files: - return True - - if os.path.basename(file_path) == "__init__.py": - return not content.strip() - - skip_header_tag = "# qbraid: skip-header" - line_number = 0 - - for line in content.splitlines(): - line_number += 1 - if 5 <= line_number <= 30 and skip_header_tag in line: - return True - if line_number > 30: - break - - return False - - -def replace_or_add_header(file_path: str, fix: bool = False) -> None: - with open(file_path, "r", encoding="ISO-8859-1") as f: - content = f.read() - - if ( - content.startswith(header) - or content.startswith(header_2023) - or should_skip(file_path, content) - ): - return - - if not fix: - failed_headers.append(file_path) - return - - lines = content.splitlines() - - comment_lines = [] - first_skipped_line = None - for index, line in enumerate(lines): - if line.startswith("#"): - comment_lines.append(line) - else: - first_skipped_line = index - break - - new_content_lines = [header.rstrip("\r\n")] + lines[first_skipped_line:] - new_content = "\n".join(new_content_lines) + "\n" - - with open(file_path, "w", encoding="ISO-8859-1") as f: - f.write(new_content) - - fixed_headers.append(file_path) - - -def process_files_in_directory(directory: str, fix: bool = False) -> int: - count = 0 - for root, _, files in os.walk(directory): - for file in files: - if file.endswith(".py"): - file_path = os.path.join(root, file) - replace_or_add_header(file_path, fix) - count += 1 - return count - - -def display_help() -> None: - help_message = """ - Usage: python verify_headers.py SRC [OPTIONS] ... - - This script checks for copyright headers at the specified path. - If no flags are passed, it will indicate which files would be - modified without actually making any changes. - - Options: - --help Display this help message and exit. - --fix Adds/modifies file headers as necessary. - """ - print(help_message) - sys.exit(0) - - -if __name__ == "__main__": - if "--help" in sys.argv: - display_help() - - # Check if at least two arguments are provided and the first argument is not a flag - if len(sys.argv) < 2 or sys.argv[1].startswith("--"): - print("Usage: python verify_headers.py SRC [OPTIONS] ...") - sys.exit(1) - - script_directory = os.path.dirname(os.path.abspath(__file__)) - project_directory = os.path.abspath(os.path.join(script_directory, "..")) - skip_files = [os.path.join(project_directory, file_path) for file_path in skip_files] - - fix = "--fix" in sys.argv - files_and_dirs = [arg for arg in sys.argv[1:] if arg != "--fix"] - - checked = 0 - for item in files_and_dirs: - full_path = os.path.join(project_directory, item) - if os.path.isdir(full_path): - checked += process_files_in_directory(full_path, fix) - elif os.path.isfile(full_path) and full_path.endswith(".py"): - replace_or_add_header(full_path, fix) - checked += 1 - else: - failed_headers.append(full_path) - print(f"Directory not found: {full_path}") - - if not fix: - if failed_headers: - for file in failed_headers: - print(f"failed {os.path.relpath(file)}") - num_failed = len(failed_headers) - s1, s2 = ("", "s") if num_failed == 1 else ("s", "") - sys.stderr.write(f"\n{num_failed} file header{s1} need{s2} updating.\n") - sys.exit(1) - else: - print("All file header checks passed!") - - else: - for file in fixed_headers: - print(f"fixed {os.path.relpath(file)}") - num_fixed = len(fixed_headers) - num_ok = checked - num_fixed - s_fixed = "" if num_fixed == 1 else "s" - s_ok = "" if num_ok == 1 else "s" - print("\nAll done! ✨ 🚀 ✨") - print(f"{num_fixed} header{s_fixed} fixed, {num_ok} header{s_ok} left unchanged.")