diff --git a/.github/workflows/test-publish.yml b/.github/workflows/test-publish.yml deleted file mode 100644 index b6607d2..0000000 --- a/.github/workflows/test-publish.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Publish to TestPyPI - -on: - workflow_dispatch: - -jobs: - pypi-publish: - name: Build dist & upload to TestPyPI - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 1 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.10' - - - name: Build binary wheel + source tarball - run: | - python3 -m pip install --upgrade pip build - python3 -m build - - - name: Publish package to TestPyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - user: __token__ - password: ${{ secrets.TEST_PYPI_API_TOKEN }} - repository-url: https://test.pypi.org/legacy/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2adbb18..48dd425 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Docs docs/stubs +.vscode/ # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/README.md b/README.md index 5f30d0c..d3dcbf1 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,62 @@ -# qbraid-qir +qbraid-qir-header -

- - CI +

+ + CI - - License + + Documentation Status - - Discord + + PyPI version + + + PyPI version + + + License + + + Discord

*Work in progress* -qBraid-SDK extension providing support for QIR conversions +qBraid-SDK extension providing support for QIR conversions. + +This project aims to make [QIR](https://www.qir-alliance.org/) representations accessible via the qBraid-SDK [transpiler](#architecture-diagram), and by doing so, open the door to language-specific conversions from any and all high-level quantum languages [supported](https://docs.qbraid.com/en/latest/sdk/overview.html#supported-frontends) by `qbraid`. See QIR Alliance: [why do we need it?](https://www.qir-alliance.org/qir-book/concepts/why-do-we-need.html). -## Planned features +## Getting started -This project aims to make [QIR](https://www.qir-alliance.org/) representations accessible via the qBraid-SDK hub and spokes [model](#architecture-diagram), and by doing so, open the door to language-specific conversions from any and all high-level quantum languages [supported](https://docs.qbraid.com/en/latest/sdk/overview.html#supported-frontends) by `qbraid`. +### Installation + +```bash +pip install qbraid-qir +``` -- [ ] Cirq $\rightarrow$ QIR - - [ ] Quantum operations - - [ ] Classical operations -- [ ] OpenQASM 3 $\rightarrow$ QIR +### Example + +```python +import cirq +from qbraid_qir import cirq_to_qir + +q0, q1 = cirq.LineQubit.range(2) + +circuit = cirq.Circuit( + cirq.H(q0), + cirq.CNOT(q0, q1), + cirq.measure(q0, q1) +) + +module = cirq_to_qir(circuit, name="my-circuit") + +ir = str(module) +``` -See: https://www.qir-alliance.org/qir-book/concepts/why-do-we-need.html +## Development -## Local install +### Install from source ```bash git clone https://github.com/qBraid/qbraid-qir.git @@ -35,7 +64,7 @@ cd qbraid-qir pip install -e . ``` -## Run tests +### Run tests ```bash pip install -r requirements-dev.txt @@ -48,7 +77,7 @@ with coverage report pytest --cov=qbraid_qir --cov-report=term tests/ ``` -## Build docs +### Build docs ```bash cd docs diff --git a/docs/api/qbraid_qir.cirq.rst b/docs/api/qbraid_qir.cirq.rst index 9e16800..ce984f1 100644 --- a/docs/api/qbraid_qir.cirq.rst +++ b/docs/api/qbraid_qir.cirq.rst @@ -1,7 +1,7 @@ :orphan: qbraid_qir.cirq -================= +================ .. automodule:: qbraid_qir.cirq :members: diff --git a/docs/index.rst b/docs/index.rst index ce1a2f1..a0335c7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -84,5 +84,4 @@ Indices and Tables ------------------ * :ref:`genindex` -* :ref:`modindex` -* :ref:`search` +* :ref:`modindex` \ No newline at end of file diff --git a/docs/userguide/cirq_qir.rst b/docs/userguide/cirq_qir.rst index 98e7c0b..53511d5 100644 --- a/docs/userguide/cirq_qir.rst +++ b/docs/userguide/cirq_qir.rst @@ -20,11 +20,10 @@ Convert a ``Cirq`` circuit to ``QIR`` code: circuit = cirq.Circuit( cirq.H(q0), cirq.CNOT(q0, q1), - cirq.measure(q0, key='result0'), - cirq.measure(q1, key='result1') + cirq.measure(q0, q1) ) - qir_code = cirq_to_qir(circuit) + qir_code = cirq_to_qir(circuit, name="Bell") print(qir_code) diff --git a/qbraid_qir/__init__.py b/qbraid_qir/__init__.py index ce26d60..abcd10a 100644 --- a/qbraid_qir/__init__.py +++ b/qbraid_qir/__init__.py @@ -13,14 +13,6 @@ .. currentmodule:: qbraid_qir -Functions ------------ - -.. autosummary:: - :toctree: ../stubs/ - - cirq_to_qir - Exceptions ----------- diff --git a/qbraid_qir/cirq/__init__.py b/qbraid_qir/cirq/__init__.py index ab209cc..0d53582 100644 --- a/qbraid_qir/cirq/__init__.py +++ b/qbraid_qir/cirq/__init__.py @@ -21,5 +21,17 @@ cirq_to_qir + +Classes +--------- + +.. autosummary:: + :toctree: ../stubs/ + + CirqModule + BasicQisVisitor + """ from .convert import cirq_to_qir +from .elements import CirqModule +from .visitor import BasicQisVisitor diff --git a/qbraid_qir/cirq/convert.py b/qbraid_qir/cirq/convert.py index 952084c..a0782cb 100644 --- a/qbraid_qir/cirq/convert.py +++ b/qbraid_qir/cirq/convert.py @@ -15,32 +15,14 @@ from typing import Optional import cirq -import qbraid.programs.cirq from pyqir import Context, Module, qir_module from qbraid_qir.cirq.elements import CirqModule, generate_module_id +from qbraid_qir.cirq.passes import preprocess_circuit from qbraid_qir.cirq.visitor import BasicQisVisitor from qbraid_qir.exceptions import QirConversionError -def _preprocess_circuit(circuit: cirq.Circuit) -> cirq.Circuit: - """ - Preprocesses a Cirq circuit to ensure that it is compatible with the QIR conversion. - - Args: - circuit (cirq.Circuit): The Cirq circuit to preprocess. - - Returns: - cirq.Circuit: The preprocessed Cirq circuit. - - """ - # circuit = cirq.contrib.qasm_import.circuit_from_qasm(circuit.to_qasm()) # decompose? - qprogram = qbraid.programs.cirq.CirqCircuit(circuit) - qprogram._convert_to_line_qubits() - cirq_circuit = qprogram.program - return cirq_circuit - - def cirq_to_qir(circuit: cirq.Circuit, name: Optional[str] = None, **kwargs) -> Module: """ Converts a Cirq circuit to a PyQIR module. @@ -49,21 +31,32 @@ def cirq_to_qir(circuit: cirq.Circuit, name: Optional[str] = None, **kwargs) -> circuit (cirq.Circuit): The Cirq circuit to convert. name (str, optional): Identifier for created QIR module. Auto-generated if not provided. + Keyword Args: + initialize_runtime (bool): Whether to perform quantum runtime environment initialization, + default `True`. + record_output (bool): Whether to record output calls for registers, default `True` + Returns: The QIR ``pyqir.Module`` representation of the input Cirq circuit. Raises: TypeError: If the input is not a valid Cirq circuit. + ValueError: If the input circuit is empty. QirConversionError: If the conversion fails. """ if not isinstance(circuit, cirq.Circuit): raise TypeError("Input quantum program must be of type cirq.Circuit.") + if len(circuit) == 0: + raise ValueError( + "Input quantum circuit must consist of at least one operation." + ) + if name is None: name = generate_module_id(circuit) try: - circuit = _preprocess_circuit(circuit) + circuit = preprocess_circuit(circuit) except Exception as e: # pylint: disable=broad-exception-caught raise QirConversionError("Failed to preprocess circuit.") from e diff --git a/qbraid_qir/cirq/elements.py b/qbraid_qir/cirq/elements.py index 4f18926..4a30b4e 100644 --- a/qbraid_qir/cirq/elements.py +++ b/qbraid_qir/cirq/elements.py @@ -71,6 +71,25 @@ def accept(self, visitor): class CirqModule: + """ + A module representing a quantum circuit in Cirq using QIR. + + This class encapsulates a quantum circuit from Cirq and translates it into QIR format, + maintaining information about quantum operations, qubits, and classical bits. It provides + methods to interact with the underlying QIR module and circuit elements. + + Args: + name (str): Name of the module. + module (Module): QIR Module instance. + num_qubits (int): Number of qubits in the circuit. + elements (List[_CircuitElement]): List of circuit elements. + + Example: + >>> circuit = cirq.Circuit() + >>> cirq_module = CirqModule.from_circuit(circuit) + >>> print(cirq_module.num_qubits) + """ + def __init__( self, name: str, @@ -86,25 +105,30 @@ def __init__( @property def name(self) -> str: + """Returns the name of the module.""" return self._name @property def module(self) -> Module: + """Returns the QIR Module instance.""" return self._module @property def num_qubits(self) -> int: + """Returns the number of qubits in the circuit.""" return self._num_qubits @property def num_clbits(self) -> int: + """Returns the number of classical bits in the circuit.""" return self._num_clbits @classmethod def from_circuit( cls, circuit: cirq.Circuit, module: Optional[Module] = None ) -> "CirqModule": - """Create a new CirqModule from a cirq.Circuit object.""" + """Class method. Constructs a CirqModule from a given cirq.Circuit object + and an optional QIR Module.""" elements: List[_CircuitElement] = [] # Register(s). Tentatively using cirq.Qid as input. Better approaches might exist tbd. @@ -117,7 +141,7 @@ def from_circuit( if module is None: module = Module(Context(), generate_module_id(circuit)) return cls( - name=module.source_filename, + name="main", module=module, num_qubits=len(circuit.all_qubits()), elements=elements, diff --git a/qbraid_qir/cirq/opsets.py b/qbraid_qir/cirq/opsets.py index 2b2b345..f1b4282 100644 --- a/qbraid_qir/cirq/opsets.py +++ b/qbraid_qir/cirq/opsets.py @@ -41,6 +41,7 @@ # Two-Qubit Gates "SWAP": pyqir._native.swap, "CNOT": pyqir._native.cx, + "CZ": pyqir._native.cz, # Three-Qubit Gates "TOFFOLI": pyqir._native.ccx, # Classical Gates/Operations @@ -69,7 +70,7 @@ def map_cirq_op_to_pyqir_callable(op: cirq.Operation) -> Tuple[Callable, str]: if isinstance(gate, cirq.ops.MeasurementGate): op_name = "MEASURE" elif isinstance(gate, (cirq.ops.Rx, cirq.ops.Ry, cirq.ops.Rz)): - op_name = re.search(r"([A-Za-z]+)\(", str(gate)).group(1) + op_name = re.search(r"([Rx-z]+)\(", str(gate)).group(1) else: op_name = str(gate) else: diff --git a/qbraid_qir/cirq/passes.py b/qbraid_qir/cirq/passes.py new file mode 100644 index 0000000..52e3512 --- /dev/null +++ b/qbraid_qir/cirq/passes.py @@ -0,0 +1,75 @@ +# 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. + +""" +Module for processing Cirq circuits before conversion to QIR. + +""" + +from typing import List + +import cirq +import qbraid.programs.cirq + +from qbraid_qir.cirq.opsets import map_cirq_op_to_pyqir_callable +from qbraid_qir.exceptions import QirConversionError + + +def _decompose_gate_op(op: cirq.GateOperation) -> List[cirq.OP_TREE]: + try: + # Try converting to PyQIR. If successful, keep the operation. + _ = map_cirq_op_to_pyqir_callable(op) + return [op] + except QirConversionError: + pass + + return cirq.decompose_once(op, flatten=True, default=[op]) + + +def _decompose_unsupported_gates(circuit: cirq.Circuit) -> cirq.Circuit: + """ + Decompose gates in a circuit that are not in the supported set. + + Args: + circuit (cirq.Circuit): The quantum circuit to process. + + Returns: + cirq.Circuit: A new circuit with unsupported gates decomposed. + """ + new_circuit = cirq.Circuit() + for moment in circuit: + new_ops = [] + for op in moment: + if isinstance(op, cirq.GateOperation): + decomposed_ops = _decompose_gate_op(op) + new_ops.extend(decomposed_ops) + else: + new_ops.append(op) + + new_circuit.append(new_ops) + return new_circuit + + +def preprocess_circuit(circuit: cirq.Circuit) -> cirq.Circuit: + """ + Preprocesses a Cirq circuit to ensure that it is compatible with the QIR conversion. + + Args: + circuit (cirq.Circuit): The Cirq circuit to preprocess. + + Returns: + cirq.Circuit: The preprocessed Cirq circuit. + + """ + circuit = _decompose_unsupported_gates(circuit) + qprogram = qbraid.programs.cirq.CirqCircuit(circuit) + qprogram._convert_to_line_qubits() + cirq_circuit = qprogram.program + return cirq_circuit diff --git a/qbraid_qir/cirq/visitor.py b/qbraid_qir/cirq/visitor.py index 1a7a88e..20c859e 100644 --- a/qbraid_qir/cirq/visitor.py +++ b/qbraid_qir/cirq/visitor.py @@ -40,16 +40,25 @@ def visit_operation(self, operation): class BasicQisVisitor(CircuitElementVisitor): - def __init__(self, profile: str = "AdaptiveExecution", **kwargs): + """A visitor for QIS (Quantum Instruction Set) basic elements. + + This class is designed to traverse and interact with elements in a quantum circuit. + + Args: + initialize_runtime (bool): If True, quantum runtime will be initialized. Defaults to True. + record_output (bool): If True, output of the circuit will be recorded. Defaults to True. + """ + + def __init__(self, initialize_runtime: bool = True, record_output: bool = True): self._module = None self._builder = None self._entry_point = None self._qubit_labels = {} - self._profile = profile self._measured_qubits = {} - self._record_output = kwargs.get("record_output", True) + self._initialize_runtime = initialize_runtime + self._record_output = record_output - def visit_cirq_module(self, module: CirqModule): + def visit_cirq_module(self, module: CirqModule) -> None: _log.debug("Visiting Cirq module '%s' (%d)", module.name, module.num_qubits) self._module = module.module context = self._module.context @@ -61,9 +70,10 @@ def visit_cirq_module(self, module: CirqModule): self._builder = Builder(context) self._builder.insert_at_end(BasicBlock(context, "entry", entry)) - i8p = PointerType(IntType(context, 8)) - nullptr = Constant.null(i8p) - pyqir.rt.initialize(self._builder, nullptr) + if self._initialize_runtime is True: + i8p = PointerType(IntType(context, 8)) + nullptr = Constant.null(i8p) + pyqir.rt.initialize(self._builder, nullptr) @property def entry_point(self) -> str: @@ -96,7 +106,7 @@ def visit_register(self, qids: List[cirq.Qid]) -> None: ) _log.debug("Added labels for qubits %s", str(qids)) - def visit_operation(self, operation: cirq.Operation): + def visit_operation(self, operation: cirq.Operation) -> None: qlabels = [self._qubit_labels.get(bit) for bit in operation.qubits] qubits = [pyqir.qubit(self._module.context, n) for n in qlabels] results = [pyqir.result(self._module.context, n) for n in qlabels] @@ -104,9 +114,10 @@ def visit_operation(self, operation: cirq.Operation): pyqir_func, op_str = map_cirq_op_to_pyqir_callable(operation) if op_str == "MEASURE": - # TODO: naive implementation, revisit and test _log.debug("Visiting measurement operation '%s'", str(operation)) - pyqir_func(self._builder, *qubits, *results) + for qubit, result in zip(qubits, results): + self._measured_qubits[pyqir.qubit_id(qubit)] = True + pyqir_func(self._builder, qubit, result) elif op_str in ["Rx", "Ry", "Rz"]: angle = operation.gate._rads * np.pi pyqir_func(self._builder, angle, *qubits) diff --git a/requirements.txt b/requirements.txt index 26d0f2b..0953bf6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ pyqir~=0.10.0 -qbraid==0.5.0.dev20231213012035 -cirq-core \ No newline at end of file +qbraid~=0.5.0.dev20240101201141 +cirq-core~=1.3.0 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index facff9f..d43ae30 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,10 +6,10 @@ author_email = contact@qbraid.com description = qBraid-SDK extension providing support for QIR conversions long_description = file: README.md long_description_content_type = text/markdown -keywords = qbraid, quantum +keywords = qbraid, quantum, qir url = https://www.qbraid.com/ project_urls = - Documentation = https://docs.qbraid.com/en/latest/ + Documentation = https://docs.qbraid.com/projects/qir/en/latest/ Bug Tracker = https://github.com/qBraid/qbraid-qir/issues Source Code = https://github.com/qBraid/qbraid-qir Discord = https://discord.gg/TPBU2sa8Et @@ -19,6 +19,7 @@ classifiers = Intended Audience :: Science/Research Natural Language :: English Programming Language :: Python + Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Topic :: Scientific/Engineering @@ -26,4 +27,4 @@ classifiers = [options] packages = find: -python_requires = >=3.10 +python_requires = >=3.9 diff --git a/test-containers/README.md b/test-containers/README.md new file mode 100644 index 0000000..c60b57b --- /dev/null +++ b/test-containers/README.md @@ -0,0 +1,76 @@ +# Test containers + +Docker containers used for testing. + +## QIR Runner + +[Docker image](./qir_runner/Dockerfile) providing an environment for testing and executing QIR programs +with the [qir-runner](https://github.com/qir-alliance/qir-runner/tree/main) package. + +### Build & run image + +Build the QIR runner image: + +```bash +docker build -t qbraid-test/qir-runner:latest qir_runner +``` + +Start the container running a Jupyter Server with the JupyterLab frontend and expose the container's internal port `8888` to port `8888` of the host machine: + +```bash +docker run -p 8888:8888 qbraid-test/qir-runner:latest +``` + +Visiting `http://:8888/?token=` in a browser will launch JupyterLab, where: + +- The hostname is the name of the computer running Docker (e.g. `localhost`) +- The token is the secret token printed in the console. + +Alternatively, you can open a shell inside the running container directly: + +```bash +docker exec -it /bin/bash +``` + +### Testing + +Once inside the container, the `qir-runner` executable is accessible via command-line: + +```bash +Usage: qir-runner [OPTIONS] --file + +Options: + -f, --file         (Required) Path to the QIR file to run + -e, --entrypoint   Name of the entry point function to execute + -s, --shots         The number of times to repeat the execution of the chosen entry point in the program [default: 1] + -r, --rngseed       The value to use when seeding the random number generator used for quantum simulation + -h, --help               Print help +``` + +Convert a cirq program and save the output to a file + +```python +import cirq +from qbraid_qir import cirq_to_qir + +# create a test circuit +q0, q1 = cirq.LineQubit.range(2) +circuit = cirq.Circuit(cirq.H(q0), cirq.CNOT(q0, q1), cirq.measure(q0, q1)) + +# convert to QIR +module = cirq_to_qir(circuit) + +# save to file +file_path = os.path.join(os.path.dirname(__file__), "bell.ll") + +with open(file_path, "w") as file: + file.write(str(module)) + +print("Saved to", file_path) +``` + +And then execute the QIR program: + +```bash +qir-runner -f bell.ll +``` diff --git a/test-containers/qir_runner/Dockerfile b/test-containers/qir_runner/Dockerfile new file mode 100644 index 0000000..8272f1b --- /dev/null +++ b/test-containers/qir_runner/Dockerfile @@ -0,0 +1,82 @@ +# Copyright (C) 2024 qBraid Development Team. +# Distributed under terms of the GNU General Public License v3. +FROM jupyter/minimal-notebook:latest + +USER root + +RUN apt-get update --yes && apt-get install --yes --no-install-recommends \ + # Basic utilities + vim \ + git \ + curl \ + pkg-config \ + lsb-release \ + wget \ + software-properties-common \ + gnupg \ + # SSL related dependencies + openssl \ + libssl-dev \ + # Build dependencies + g++ \ + ninja-build \ + cmake \ + gfortran \ + build-essential \ + # Mathematical libraries + libblas-dev \ + libopenblas-dev \ + liblapack-dev \ + # Compression library + libz-dev \ + # LLVM dependencies + clang-format \ + clang-tidy \ + clang-tools \ + clang \ + clangd \ + libc++-dev \ + libc++1 \ + libc++abi-dev \ + libc++abi1 \ + libclang-dev \ + libclang1 \ + liblldb-dev \ + libllvm-ocaml-dev \ + libomp-dev \ + libomp5 \ + lld \ + lldb \ + llvm-dev \ + llvm-runtime \ + llvm \ + python3-clang + +USER $NB_UID + +# Install Rustup: https://www.rust-lang.org/tools/install +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + +# Set PATH to include Cargo's bin directory +ENV PATH="$HOME/.cargo/bin:${PATH}" + +USER root + +# Install LLVM +RUN bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)" + +# Clone and build qir-runner +RUN git clone https://github.com/qir-alliance/qir-runner.git /opt/qir-runner && \ + chown -R $NB_UID:$NB_GID /opt/qir-runner + +USER $NB_UID + +WORKDIR /opt/qir-runner + +# Install llvmenv and build qir-runner +RUN cargo install llvmenv && \ + cargo build --release + +ENV PATH="/opt/qir-runner/target/release:${PATH}" + +WORKDIR $HOME \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index f75e669..8dd504c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,6 +15,6 @@ """ # pylint: disable=unused-import -from .fixtures.basic_gates import single_op_tests -from .fixtures.cirq_circuits import cirq_bell -from .fixtures.pyqir_circuits import pyqir_bell +from .fixtures.basic_gates import * +from .fixtures.cirq_circuits import * +from .fixtures.pyqir_circuits import * diff --git a/tests/fixtures/basic_gates.py b/tests/fixtures/basic_gates.py index c5817b9..c7db4ed 100644 --- a/tests/fixtures/basic_gates.py +++ b/tests/fixtures/basic_gates.py @@ -63,7 +63,7 @@ def _generate_one_qubit_fixture(gate_name: str): @pytest.fixture() def test_fixture(): circuit = cirq.Circuit() - q = cirq.NamedQubit("q") + q = cirq.NamedQubit("q0") circuit.append(getattr(cirq, gate_name)(q)) return _map_gate_name(gate_name), circuit @@ -80,7 +80,7 @@ def _generate_rotation_fixture(gate_name: str): @pytest.fixture() def test_fixture(): circuit = cirq.Circuit() - q = cirq.NamedQubit("q") + q = cirq.NamedQubit("q0") circuit.append(getattr(cirq, gate_name)(rads=0.5)(q)) return _map_gate_name(gate_name), circuit @@ -157,7 +157,7 @@ def test_qft(): @pytest.mark.parametrize("angle", np.linspace(0, 2 * np.pi, 5)) def test_rx_gate(angle): - qubit = cirq.NamedQubit("q") + qubit = cirq.NamedQubit("q0") circuit = cirq.Circuit(cirq.rx(angle)(qubit)) # Add assertions or checks for the rotation diff --git a/tests/test_cirq_decompose.py b/tests/test_cirq_decompose.py new file mode 100644 index 0000000..f103c57 --- /dev/null +++ b/tests/test_cirq_decompose.py @@ -0,0 +1,69 @@ +# 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. + +""" +Test functions that decompose unsupported Cirq gates before conversion to QIR. + +""" + +import cirq +import numpy as np +from qbraid.programs.testing import circuits_allclose + +from qbraid_qir.cirq.passes import _decompose_unsupported_gates + + +def test_only_supported_gates(): + qubits = cirq.LineQubit.range(2) + circuit = cirq.Circuit(cirq.H(qubits[0]), cirq.CNOT(qubits[0], qubits[1])) + decomposed_circuit = _decompose_unsupported_gates(circuit) + assert decomposed_circuit == circuit + assert circuits_allclose(decomposed_circuit, circuit) + + +def test_contains_unsupported_gates(): + qubits = cirq.LineQubit.range(2) + circuit = cirq.Circuit( + cirq.ops.ISwapPowGate(exponent=np.pi).on(*qubits), + ) + decomposed_circuit = _decompose_unsupported_gates(circuit) + assert decomposed_circuit != circuit + assert circuits_allclose(decomposed_circuit, circuit) + + +def test_empty_circuit(): + circuit = cirq.Circuit() + decomposed_circuit = _decompose_unsupported_gates(circuit) + assert decomposed_circuit == circuit + assert circuits_allclose(decomposed_circuit, circuit) + + +def test_custom_gate(): + class CustomGate(cirq.Gate): # pylint: disable=abstract-method + def _num_qubits_(self): + return 1 + + def _decompose_(self, qubits): + yield cirq.X(qubits[0]) + + custom_gate = CustomGate() + qubit = cirq.LineQubit(0) + circuit = cirq.Circuit(custom_gate.on(qubit)) + decomposed_circuit = _decompose_unsupported_gates(circuit) + assert decomposed_circuit != circuit + assert ( + any( + isinstance(op.gate, CustomGate) + for moment in decomposed_circuit + for op in moment + ) + is False + ) + assert circuits_allclose(decomposed_circuit, circuit) diff --git a/tests/test_preprocess.py b/tests/test_cirq_preprocess.py similarity index 87% rename from tests/test_preprocess.py rename to tests/test_cirq_preprocess.py index 1defb8b..9b54f84 100644 --- a/tests/test_preprocess.py +++ b/tests/test_cirq_preprocess.py @@ -15,7 +15,7 @@ import numpy as np import pytest -from qbraid_qir.cirq.convert import _preprocess_circuit +from qbraid_qir.cirq.passes import preprocess_circuit @pytest.fixture @@ -33,7 +33,7 @@ def namedqubit_circuit(): def test_convert_gridqubits_to_linequbits(gridqubit_circuit): - linequbit_circuit = _preprocess_circuit(gridqubit_circuit) + linequbit_circuit = preprocess_circuit(gridqubit_circuit) for qubit in linequbit_circuit.all_qubits(): assert isinstance(qubit, cirq.LineQubit), "Qubit is not a LineQubit" assert np.allclose( @@ -42,7 +42,7 @@ def test_convert_gridqubits_to_linequbits(gridqubit_circuit): def test_convert_namedqubits_to_linequbits(namedqubit_circuit): - linequbit_circuit = _preprocess_circuit(namedqubit_circuit) + linequbit_circuit = preprocess_circuit(namedqubit_circuit) for qubit in linequbit_circuit.all_qubits(): assert isinstance(qubit, cirq.LineQubit), "Qubit is not a LineQubit" assert np.allclose( @@ -52,7 +52,7 @@ def test_convert_namedqubits_to_linequbits(namedqubit_circuit): def test_empty_circuit_conversion(): circuit = cirq.Circuit() - converted_circuit = _preprocess_circuit(circuit) + converted_circuit = preprocess_circuit(circuit) assert ( len(converted_circuit.all_qubits()) == 0 ), "Converted empty circuit should have no qubits" diff --git a/tests/test_cirq_to_qir.py b/tests/test_cirq_to_qir.py index b4c2ac6..80f984a 100644 --- a/tests/test_cirq_to_qir.py +++ b/tests/test_cirq_to_qir.py @@ -16,8 +16,7 @@ import pytest import tests.test_utils as test_utils -from qbraid_qir.cirq.convert import cirq_to_qir, generate_module_id -from qbraid_qir.exceptions import QirConversionError +from qbraid_qir.cirq.convert import cirq_to_qir from tests.fixtures.basic_gates import single_op_tests from .qir_utils import assert_equal_qir @@ -29,45 +28,41 @@ def test_cirq_to_qir_type_error(): cirq_to_qir(None) -@pytest.mark.skip(reason="Not implemented yet") def test_cirq_to_qir_conversion_error(): """Test raising exception for conversion error.""" circuit = cirq.Circuit() - with pytest.raises(QirConversionError): + with pytest.raises(ValueError): cirq_to_qir(circuit) -@pytest.mark.skip(reason="Not implemented yet") @pytest.mark.parametrize("circuit_name", single_op_tests) def test_single_qubit_gates(circuit_name, request): qir_op, circuit = request.getfixturevalue(circuit_name) - generated_qir = str(cirq_to_qir(circuit)[0]).splitlines() - func = test_utils.get_entry_point_body(generated_qir) + qir_module = cirq_to_qir(circuit, record_output=False) + qir_str = str(qir_module).splitlines() + func = test_utils.get_entry_point_body(qir_str) assert func[0] == test_utils.initialize_call_string() assert func[1] == test_utils.single_op_call_string(qir_op, 0) assert func[2] == test_utils.return_string() assert len(func) == 3 -def test_cirq_workings(): - circuit = cirq.Circuit() - qubits = cirq.LineQubit.range(3) - circuit.append(cirq.CX(qubits[0], qubits[1])) - circuit.append(cirq.measure(qubits[0])) - circuit.append(cirq.H(qubits[0])) - circuit.append(cirq.H(qubits[1])) - circuit.append(cirq.H(qubits[2])) - print(circuit) - - def test_verify_qir_bell_fixture(pyqir_bell): """Test that pyqir fixture generates code equal to test_qir_bell.ll file.""" assert_equal_qir(pyqir_bell.ir(), "test_qir_bell") -@pytest.mark.skip(reason="Not implemented yet") +def test_entry_point_name(cirq_bell): + """Test that entry point name is consistent with module ID.""" + name = "quantum_123" + module = cirq_to_qir(cirq_bell, name=name) + assert module.source_filename == name + + def test_convert_bell_compare_file(cirq_bell): """Test converting Cirq bell circuit to QIR.""" test_name = "test_qir_bell" - generator = cirq_to_qir(cirq_bell, name=test_name) - assert_equal_qir(generator.ir(), test_name) + module = cirq_to_qir( + cirq_bell, name=test_name, initialize_runtime=False, record_output=False + ) + assert_equal_qir(str(module), test_name) diff --git a/tools/create_dev_build.sh b/tools/create_dev_build.sh index 1918fc5..06d5c10 100755 --- a/tools/create_dev_build.sh +++ b/tools/create_dev_build.sh @@ -40,7 +40,7 @@ OUT_DIR="${2}" # Constants REPO_DIR=$(git rev-parse --show-toplevel) -VERSION_FILE="${REPO_DIR}/qbraid/_version.py" +VERSION_FILE="${REPO_DIR}/qbraid_qir/_version.py" TMP_BRANCH="tmp_build_branch_$(date "+%Y%m%d%H%M%S")" # Cleanup function diff --git a/tools/stamp_dev_version.sh b/tools/stamp_dev_version.sh index bee6432..9d38d99 100755 --- a/tools/stamp_dev_version.sh +++ b/tools/stamp_dev_version.sh @@ -31,7 +31,7 @@ set -e # Constants -PROJECT_NAME="qbraid/qir" +PROJECT_NAME="qbraid_qir" VERSION_FILE_PATH="_version.py" TIMESTAMP_FORMAT="+%Y%m%d%H%M%S" diff --git a/tools/verify_headers.py b/tools/verify_headers.py index e723b37..feb11bc 100755 --- a/tools/verify_headers.py +++ b/tools/verify_headers.py @@ -46,6 +46,16 @@ def should_skip(file_path, content): 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 @@ -91,9 +101,29 @@ def process_files_in_directory(directory, fix=False): return count +def display_help(): + 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 len(sys.argv) < 2: - print("Please provide at least one directory as a command-line argument.") + 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__))