From 07af47d8e7b4aea604b26e4eb55eb69b2eddb63b Mon Sep 17 00:00:00 2001 From: eidrynbot Date: Tue, 3 Mar 2026 10:26:40 -0600 Subject: [PATCH 1/9] feat: add qbraid_qir.qiskit module for Qiskit to QIR conversion Port of microsoft/qiskit-qir (MIT License) with the following updates: - Compatible with Qiskit 2.x (new QuantumCircuit API) - Uses qiskit_to_qir() interface to match cirq_to_qir/qasm3_to_qir - Supports pyqir 0.10-0.12+ (typed and opaque pointers) - Follows qbraid-qir conventions and code style New files: - qbraid_qir/qiskit/ module with convert, visitor, elements, exceptions - tests/qiskit_qir/ with 44 unit tests - NOTICE.md for MIT license attribution - docs/api/qbraid_qir.qiskit.rst Updated files: - pyproject.toml: added qiskit extra - tox.ini: added qiskit to test environments - README.md: added qiskit usage example - docs/index.rst: added qiskit to API reference - docs/conf.py: added qiskit to autodoc_mock_imports --- CHANGELOG.md | 2 + NOTICE.md | 37 +++ README.md | 22 ++ docs/api/qbraid_qir.qiskit.rst | 8 + docs/conf.py | 2 +- docs/index.rst | 5 +- pyproject.toml | 5 +- qbraid_qir/__init__.py | 4 +- qbraid_qir/qiskit/__init__.py | 56 +++++ qbraid_qir/qiskit/convert.py | 87 +++++++ qbraid_qir/qiskit/elements.py | 187 ++++++++++++++ qbraid_qir/qiskit/exceptions.py | 24 ++ qbraid_qir/qiskit/visitor.py | 329 +++++++++++++++++++++++++ tests/qiskit_qir/__init__.py | 15 ++ tests/qiskit_qir/conftest.py | 166 +++++++++++++ tests/qiskit_qir/test_basic_gates.py | 206 ++++++++++++++++ tests/qiskit_qir/test_qiskit_to_qir.py | 224 +++++++++++++++++ tox.ini | 3 + 18 files changed, 1376 insertions(+), 6 deletions(-) create mode 100644 NOTICE.md create mode 100644 docs/api/qbraid_qir.qiskit.rst create mode 100644 qbraid_qir/qiskit/__init__.py create mode 100644 qbraid_qir/qiskit/convert.py create mode 100644 qbraid_qir/qiskit/elements.py create mode 100644 qbraid_qir/qiskit/exceptions.py create mode 100644 qbraid_qir/qiskit/visitor.py create mode 100644 tests/qiskit_qir/__init__.py create mode 100644 tests/qiskit_qir/conftest.py create mode 100644 tests/qiskit_qir/test_basic_gates.py create mode 100644 tests/qiskit_qir/test_qiskit_to_qir.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a78a54e..d563449b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ Types of changes: ### ➕ New Features +- Added `qbraid_qir.qiskit` module for Qiskit to QIR conversion, ported from the archived [microsoft/qiskit-qir](https://github.com/microsoft/qiskit-qir) repository (MIT License). The module has been updated for compatibility with Qiskit 2.x and follows qbraid-qir conventions. Main entry point is `qiskit_to_qir(circuit, name, **kwargs)` which converts a Qiskit `QuantumCircuit` to a PyQIR `Module`. ([#XXXX](https://github.com/qBraid/qbraid-qir/pull/XXXX)) + ### 🌟 Improvements ### 📜 Documentation diff --git a/NOTICE.md b/NOTICE.md new file mode 100644 index 00000000..abeb8355 --- /dev/null +++ b/NOTICE.md @@ -0,0 +1,37 @@ +# Third Party Notices + +This software includes code derived from third party sources. + +## qiskit-qir + +The `qbraid_qir.qiskit` module contains code derived from [microsoft/qiskit-qir](https://github.com/microsoft/qiskit-qir). + +``` +MIT License + +Copyright (c) Microsoft Corporation. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE +``` + +The qiskit-qir codebase has been adapted to: +- Work with Qiskit 2.x (updated for new QuantumCircuit API) +- Follow qbraid-qir conventions and coding style +- Support both pyqir 0.10.x and 0.12+ (typed and opaque pointers) +- Integrate with the qbraid-qir module structure diff --git a/README.md b/README.md index a937bc4d..770609af 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,12 @@ For Cirq to QIR conversions, install the `cirq` extra: pip install 'qbraid-qir[cirq]' ``` +For Qiskit to QIR conversions, install the `qiskit` extra: + +```shell +pip install 'qbraid-qir[qiskit]' +``` + For QIR to SQUIN conversions, install the `squin` extra: ```shell @@ -146,6 +152,22 @@ module = cirq_to_qir(circuit, name="my-circuit") ir = str(module) ``` +### Qiskit conversions + +```python +from qiskit import QuantumCircuit +from qbraid_qir import qiskit_to_qir + +circuit = QuantumCircuit(2, 2) +circuit.h(0) +circuit.cx(0, 1) +circuit.measure([0, 1], [0, 1]) + +module = qiskit_to_qir(circuit, name="bell") + +ir = str(module) +``` + ### SQUIN conversions ```python diff --git a/docs/api/qbraid_qir.qiskit.rst b/docs/api/qbraid_qir.qiskit.rst new file mode 100644 index 00000000..a73f969e --- /dev/null +++ b/docs/api/qbraid_qir.qiskit.rst @@ -0,0 +1,8 @@ +:orphan: + +qbraid_qir.qiskit +================== + +.. automodule:: qbraid_qir.qiskit + :undoc-members: + :show-inheritance: diff --git a/docs/conf.py b/docs/conf.py index 185680bd..155a2c21 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -41,7 +41,7 @@ # set_type_checking_flag = True autodoc_member_order = "bysource" autoclass_content = "both" -autodoc_mock_imports = ["cirq", "openqasm3", "pyqasm", "numpy", "numpy.typing", "kirin", "bloqade"] +autodoc_mock_imports = ["cirq", "openqasm3", "pyqasm", "numpy", "numpy.typing", "kirin", "bloqade", "qiskit"] napoleon_numpy_docstring = False todo_include_todos = True mathjax_path = "https://cdn.jsdelivr.net/npm/mathjax@2/MathJax.js?config=TeX-AMS-MML_HTMLorMML" diff --git a/docs/index.rst b/docs/index.rst index 977cd951..4831809c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -81,11 +81,11 @@ qBraid-QIR requires Python 3.10 or greater. The base package can be installed wi pip install qbraid-qir -To enable specific conversions such as OpenQASM 3 to QIR or Cirq to QIR, you can install one or both extras: +To enable specific conversions such as OpenQASM 3 to QIR, Cirq to QIR, or Qiskit to QIR, you can install the extras: .. code-block:: bash - pip install 'qbraid-qir[qasm3,cirq]' + pip install 'qbraid-qir[qasm3,cirq,qiskit]' Resources @@ -117,6 +117,7 @@ Resources api/qbraid_qir api/qbraid_qir.cirq api/qbraid_qir.qasm3 + api/qbraid_qir.qiskit api/qbraid_qir.squin .. toctree:: diff --git a/pyproject.toml b/pyproject.toml index 68e0a876..794c6012 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ description = "qBraid-SDK extension providing support for QIR conversions." readme = "README.md" authors = [{name = "qBraid Development Team"}, {email = "contact@qbraid.com"}] license = "Apache-2.0" -keywords = ["qbraid", "quantum", "qir", "llvm", "cirq", "openqasm", "squin"] +keywords = ["qbraid", "quantum", "qir", "llvm", "cirq", "openqasm", "qiskit", "squin"] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", @@ -40,10 +40,11 @@ Discord = "https://discord.gg/TPBU2sa8Et" [project.optional-dependencies] cirq = ["cirq-core>=1.3.0,<1.6.0"] qasm3 = ["pyqasm>=0.4.0,<1.1.0", "numpy"] +qiskit = ["qiskit>=2.0"] squin = ["kirin-toolchain>=0.17.33", "bloqade-circuit>=0.9.1"] [tool.setuptools] -packages = ["qbraid_qir", "qbraid_qir.cirq", "qbraid_qir.qasm3", "qbraid_qir.squin"] +packages = ["qbraid_qir", "qbraid_qir.cirq", "qbraid_qir.qasm3", "qbraid_qir.qiskit", "qbraid_qir.squin"] [tool.setuptools.dynamic] version = {attr = "qbraid_qir.__version__"} diff --git a/qbraid_qir/__init__.py b/qbraid_qir/__init__.py index 31c670ad..48cb7d21 100644 --- a/qbraid_qir/__init__.py +++ b/qbraid_qir/__init__.py @@ -51,13 +51,15 @@ "dumps", "qasm3_to_qir", "cirq_to_qir", + "qiskit_to_qir", ] -_lazy = {"cirq": "cirq_to_qir", "qasm3": "qasm3_to_qir"} +_lazy = {"cirq": "cirq_to_qir", "qasm3": "qasm3_to_qir", "qiskit": "qiskit_to_qir"} if TYPE_CHECKING: from .cirq import cirq_to_qir from .qasm3 import qasm3_to_qir + from .qiskit import qiskit_to_qir def __getattr__(name): diff --git a/qbraid_qir/qiskit/__init__.py b/qbraid_qir/qiskit/__init__.py new file mode 100644 index 00000000..83aa08bc --- /dev/null +++ b/qbraid_qir/qiskit/__init__.py @@ -0,0 +1,56 @@ +# Copyright 2025 qBraid +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This module is derived from microsoft/qiskit-qir (MIT License). +# See NOTICE.md for attribution details. + +""" +Module containing Qiskit QIR functionality. + +.. currentmodule:: qbraid_qir.qiskit + +Functions +----------- + +.. autosummary:: + :toctree: ../stubs/ + + qiskit_to_qir + + +Classes +--------- + +.. autosummary:: + :toctree: ../stubs/ + + QiskitModule + BasicQiskitVisitor + +Exceptions +----------- + +.. autosummary:: + :toctree: ../stubs/ + + QiskitConversionError + +""" + +from .convert import qiskit_to_qir +from .elements import QiskitModule +from .exceptions import QiskitConversionError +from .visitor import BasicQiskitVisitor + +__all__ = ["qiskit_to_qir", "QiskitModule", "QiskitConversionError", "BasicQiskitVisitor"] diff --git a/qbraid_qir/qiskit/convert.py b/qbraid_qir/qiskit/convert.py new file mode 100644 index 00000000..454056cb --- /dev/null +++ b/qbraid_qir/qiskit/convert.py @@ -0,0 +1,87 @@ +# Copyright 2025 qBraid +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This module is derived from microsoft/qiskit-qir (MIT License). +# Original code: Copyright (c) Microsoft Corporation. +# See NOTICE.md for attribution details. + +""" +Module containing Qiskit to QIR conversion functions. + +""" + +from typing import Optional + +from pyqir import Context, Module, qir_module +from qiskit.circuit import QuantumCircuit + +from .elements import QiskitModule, generate_module_id +from .exceptions import QiskitConversionError +from .visitor import BasicQiskitVisitor + + +def qiskit_to_qir(circuit: QuantumCircuit, name: Optional[str] = None, **kwargs) -> Module: + """ + Converts a Qiskit QuantumCircuit to a PyQIR module. + + Args: + circuit: The Qiskit QuantumCircuit to convert. + name: 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`. + emit_barrier_calls (bool): Whether to emit barrier calls in the QIR, default `False`. + + Returns: + The QIR ``pyqir.Module`` representation of the input Qiskit circuit. + + Raises: + TypeError: If the input is not a valid Qiskit QuantumCircuit. + ValueError: If the input circuit is empty. + QiskitConversionError: If the conversion fails. + + Example: + >>> from qiskit import QuantumCircuit + >>> from qbraid_qir.qiskit import qiskit_to_qir + >>> + >>> qc = QuantumCircuit(2, 2) + >>> qc.h(0) + >>> qc.cx(0, 1) + >>> qc.measure([0, 1], [0, 1]) + >>> + >>> module = qiskit_to_qir(qc, name="bell") + >>> ir = str(module) + """ + if not isinstance(circuit, QuantumCircuit): + raise TypeError("Input quantum program must be of type qiskit.QuantumCircuit.") + + if len(circuit.data) == 0: + raise ValueError("Input quantum circuit must consist of at least one operation.") + + if name is None: + name = generate_module_id(circuit) + + llvm_module = qir_module(Context(), name) + module = QiskitModule.from_circuit(circuit, llvm_module) + + visitor = BasicQiskitVisitor(**kwargs) + module.accept(visitor) + + error = llvm_module.verify() + if error is not None: + raise QiskitConversionError(error) + + return llvm_module diff --git a/qbraid_qir/qiskit/elements.py b/qbraid_qir/qiskit/elements.py new file mode 100644 index 00000000..29e2f494 --- /dev/null +++ b/qbraid_qir/qiskit/elements.py @@ -0,0 +1,187 @@ +# Copyright 2025 qBraid +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This module is derived from microsoft/qiskit-qir (MIT License). +# Original code: Copyright (c) Microsoft Corporation. +# See NOTICE.md for attribution details. + +""" +Module defining Qiskit circuit elements for QIR conversion. + +""" + +from abc import ABCMeta, abstractmethod +from typing import TYPE_CHECKING, Optional, Union + +from pyqir import Module +from qiskit import ClassicalRegister, QuantumRegister +from qiskit.circuit import Clbit, Qubit +from qiskit.circuit.quantumcircuit import QuantumCircuit + +if TYPE_CHECKING: + from qiskit.circuit.instruction import Instruction + + +class _QuantumCircuitElement(metaclass=ABCMeta): + """Abstract base class for quantum circuit elements.""" + + @classmethod + def from_element_list(cls, elements): + """Create a list of circuit elements from a list of raw elements.""" + return [cls(elem) for elem in elements] + + @abstractmethod + def accept(self, visitor): + """Accept a visitor to process this element.""" + + +class _Register(_QuantumCircuitElement): + """Wrapper for a Qiskit register element.""" + + def __init__(self, register: Union[QuantumRegister, ClassicalRegister]): + self._register: Union[QuantumRegister, ClassicalRegister] = register + + def accept(self, visitor): + """Accept a visitor to process this register.""" + visitor.visit_register(self._register) + + +class _Instruction(_QuantumCircuitElement): + """Wrapper for a Qiskit instruction element.""" + + def __init__( + self, + instruction: "Instruction", + qargs: tuple[Qubit, ...], + cargs: tuple[Clbit, ...], + ): + self._instruction: "Instruction" = instruction + self._qargs = qargs + self._cargs = cargs + + def accept(self, visitor): + """Accept a visitor to process this instruction.""" + visitor.visit_instruction(self._instruction, self._qargs, self._cargs) + + +def generate_module_id(circuit: QuantumCircuit) -> str: + """Generate a unique module ID for a circuit.""" + return circuit.name if circuit.name else "main" + + +class QiskitModule: + """Represents a Qiskit quantum circuit prepared for QIR conversion. + + Attributes: + circuit: The original Qiskit QuantumCircuit. + name: The name of the module. + module: The PyQIR Module being built. + num_qubits: Number of qubits in the circuit. + num_clbits: Number of classical bits in the circuit. + reg_sizes: List of sizes for each classical register. + """ + + def __init__( + self, + circuit: QuantumCircuit, + name: str, + module: Module, + num_qubits: int, + num_clbits: int, + reg_sizes: list[int], + elements: list[_QuantumCircuitElement], + ): + self._circuit = circuit + self._name = name + self._module = module + self._elements = elements + self._num_qubits = num_qubits + self._num_clbits = num_clbits + self.reg_sizes = reg_sizes + + @property + def circuit(self) -> QuantumCircuit: + """Return the underlying Qiskit circuit.""" + return self._circuit + + @property + def name(self) -> str: + """Return the module name.""" + return self._name + + @property + def module(self) -> Module: + """Return the PyQIR module.""" + return self._module + + @property + def num_qubits(self) -> int: + """Return the number of qubits.""" + return self._num_qubits + + @property + def num_clbits(self) -> int: + """Return the number of classical bits.""" + return self._num_clbits + + @classmethod + def from_circuit( + cls, circuit: QuantumCircuit, module: Optional[Module] = None + ) -> "QiskitModule": + """Create a new QiskitModule from a Qiskit QuantumCircuit. + + Args: + circuit: The Qiskit QuantumCircuit to convert. + module: An optional existing PyQIR Module to use. + + Returns: + A new QiskitModule instance. + """ + elements: list[_QuantumCircuitElement] = [] + reg_sizes = [len(creg) for creg in circuit.cregs] + + # Add registers + elements.extend(_Register.from_element_list(circuit.qregs)) + elements.extend(_Register.from_element_list(circuit.cregs)) + + # Add instructions (updated for qiskit 2.x) + for circuit_instruction in circuit.data: + instruction = circuit_instruction.operation + qargs = circuit_instruction.qubits + cargs = circuit_instruction.clbits + elements.append(_Instruction(instruction, qargs, cargs)) + + name = generate_module_id(circuit) + + return cls( + circuit=circuit, + name=name, + module=module, + num_qubits=circuit.num_qubits, + num_clbits=circuit.num_clbits, + reg_sizes=reg_sizes, + elements=elements, + ) + + def accept(self, visitor): + """Accept a visitor to process this module. + + Args: + visitor: The visitor to accept. + """ + visitor.visit_qiskit_module(self) + for element in self._elements: + element.accept(visitor) + visitor.record_output(self) + visitor.finalize() diff --git a/qbraid_qir/qiskit/exceptions.py b/qbraid_qir/qiskit/exceptions.py new file mode 100644 index 00000000..9c02bbd3 --- /dev/null +++ b/qbraid_qir/qiskit/exceptions.py @@ -0,0 +1,24 @@ +# Copyright 2025 qBraid +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Module defining exceptions for Qiskit to QIR conversion. + +""" + +from qbraid_qir.exceptions import QirConversionError + + +class QiskitConversionError(QirConversionError): + """Exception raised when a Qiskit circuit cannot be converted to QIR.""" diff --git a/qbraid_qir/qiskit/visitor.py b/qbraid_qir/qiskit/visitor.py new file mode 100644 index 00000000..868c3ae9 --- /dev/null +++ b/qbraid_qir/qiskit/visitor.py @@ -0,0 +1,329 @@ +# Copyright 2025 qBraid +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This module is derived from microsoft/qiskit-qir (MIT License). +# Original code: Copyright (c) Microsoft Corporation. +# See NOTICE.md for attribution details. + +""" +Module defining QiskitVisitor for QIR conversion. + +""" + +import logging +from abc import ABCMeta, abstractmethod +from typing import Union + +import pyqir +import pyqir.qis as qis +import pyqir.rt as rt +from pyqir import BasicBlock, Builder, Constant, IntType, PointerType, entry_point +from qiskit import ClassicalRegister, QuantumRegister +from qiskit.circuit import Clbit, Qubit +from qiskit.circuit.instruction import Instruction + +from qbraid_qir._pyqir_compat import pointer_id + +from .elements import QiskitModule +from .exceptions import QiskitConversionError + +logger = logging.getLogger(__name__) + +# Supported quantum instructions +SUPPORTED_INSTRUCTIONS = [ + "barrier", + "ccx", + "cx", + "cz", + "h", + "id", + "m", + "measure", + "mz", + "reset", + "rx", + "ry", + "rz", + "s", + "sdg", + "swap", + "t", + "tdg", + "x", + "y", + "z", +] + +# Instructions that are no-ops (ignored during conversion) +NOOP_INSTRUCTIONS = ["delay"] + + +class QuantumCircuitElementVisitor(metaclass=ABCMeta): + """Abstract base class for quantum circuit element visitors.""" + + @abstractmethod + def visit_register(self, register): + """Visit a register element.""" + + @abstractmethod + def visit_instruction(self, instruction, qargs, cargs): + """Visit an instruction element.""" + + +class BasicQiskitVisitor(QuantumCircuitElementVisitor): + """A visitor for basic Qiskit circuit elements. + + This class traverses and converts Qiskit circuit elements to QIR. + + Args: + initialize_runtime: If True, quantum runtime will be initialized. Defaults to True. + record_output: If True, output of the circuit will be recorded. Defaults to True. + emit_barrier_calls: If True, barrier instructions will be emitted. Defaults to False. + """ + + def __init__( + self, + initialize_runtime: bool = True, + record_output: bool = True, + emit_barrier_calls: bool = False, + ): + self._module: pyqir.Module + self._qiskit_module: QiskitModule | None = None + self._builder: pyqir.Builder + self._entry_point: str = "" + self._qubit_labels: dict[Qubit, int] = {} + self._clbit_labels: dict[Clbit, int] = {} + self._measured_qubits: dict[int, bool] = {} + self._initialize_runtime = initialize_runtime + self._record_output = record_output + self._emit_barrier_calls = emit_barrier_calls + + def visit_qiskit_module(self, module: QiskitModule) -> None: + """Visit a QiskitModule and initialize the QIR builder. + + Args: + module: The QiskitModule to visit. + """ + logger.debug( + "Visiting Qiskit module '%s' (%d qubits, %d clbits)", + module.name, + module.num_qubits, + module.num_clbits, + ) + self._module = module.module + self._qiskit_module = module + context = self._module.context + entry = entry_point(self._module, module.name, module.num_qubits, module.num_clbits) + + self._entry_point = entry.name + self._builder = Builder(context) + self._builder.insert_at_end(BasicBlock(context, "entry", entry)) + + if self._initialize_runtime: + i8p = PointerType(IntType(context, 8)) + nullptr = Constant.null(i8p) + rt.initialize(self._builder, nullptr) + + @property + def entry_point(self) -> str: + """Return the entry point name.""" + return self._entry_point + + def finalize(self) -> None: + """Finalize the QIR module by adding a return instruction.""" + self._builder.ret(None) + + def record_output(self, module: QiskitModule) -> None: + """Record output for classical registers. + + Args: + module: The QiskitModule containing register information. + """ + if not self._record_output: + return + + i8p = PointerType(IntType(self._module.context, 8)) + + # Qiskit inverts the ordering of results within each register + # but keeps the overall register ordering + logical_id_base = 0 + for size in module.reg_sizes: + rt.array_record_output( + self._builder, + pyqir.const(IntType(self._module.context, 64), size), + Constant.null(i8p), + ) + for index in range(size - 1, -1, -1): + result_ref = pyqir.result(self._module.context, logical_id_base + index) + rt.result_record_output(self._builder, result_ref, Constant.null(i8p)) + logical_id_base += size + + def visit_register(self, register: Union[QuantumRegister, ClassicalRegister]) -> None: + """Visit a register and assign labels to its bits. + + Args: + register: The quantum or classical register to visit. + """ + logger.debug("Visiting register '%s'", register.name) + if isinstance(register, QuantumRegister): + self._qubit_labels.update( + {bit: n + len(self._qubit_labels) for n, bit in enumerate(register)} + ) + logger.debug("Added labels for qubits %s", [bit for bit in register]) + elif isinstance(register, ClassicalRegister): + self._clbit_labels.update( + {bit: n + len(self._clbit_labels) for n, bit in enumerate(register)} + ) + else: + raise QiskitConversionError(f"Register of type {type(register)} not supported.") + + def _process_composite_instruction( + self, + instruction: Instruction, + qargs: tuple[Qubit, ...], + cargs: tuple[Clbit, ...], + ) -> None: + """Process a composite (decomposable) instruction. + + Args: + instruction: The composite instruction. + qargs: Qubit arguments. + cargs: Classical bit arguments. + """ + subcircuit = instruction.definition + logger.debug( + "Processing composite instruction %s with qubits %s", + instruction.name, + qargs, + ) + + if len(qargs) != subcircuit.num_qubits: + raise QiskitConversionError( + f"Composite instruction {instruction.name} called with wrong number of qubits; " + f"{subcircuit.num_qubits} expected, {len(qargs)} provided" + ) + if len(cargs) != subcircuit.num_clbits: + raise QiskitConversionError( + f"Composite instruction {instruction.name} called with wrong number of clbits; " + f"{subcircuit.num_clbits} expected, {len(cargs)} provided" + ) + + # Process sub-instructions with mapped bits + for circuit_instruction in subcircuit.data: + inst = circuit_instruction.operation + i_qargs = circuit_instruction.qubits + i_cargs = circuit_instruction.clbits + mapped_qbits = tuple(qargs[subcircuit.qubits.index(i)] for i in i_qargs) + mapped_clbits = tuple(cargs[subcircuit.clbits.index(i)] for i in i_cargs) + logger.debug( + "Processing sub-instruction %s with mapped qubits %s", + inst.name, + mapped_qbits, + ) + self.visit_instruction(inst, mapped_qbits, mapped_clbits) + + def visit_instruction( + self, + instruction: Instruction, + qargs: tuple[Qubit, ...], + cargs: tuple[Clbit, ...], + ) -> None: + """Visit and convert an instruction to QIR. + + Args: + instruction: The instruction to visit. + qargs: Qubit arguments. + cargs: Classical bit arguments. + """ + qlabels = [self._qubit_labels.get(bit) for bit in qargs] + clabels = [self._clbit_labels.get(bit) for bit in cargs] + qubits = [pyqir.qubit(self._module.context, n) for n in qlabels] + results = [pyqir.result(self._module.context, n) for n in clabels] + + labels_str = ", ".join([str(label) for label in qlabels + clabels]) + logger.debug("Visiting instruction '%s' (%s)", instruction.name, labels_str) + + op_name = instruction.name.lower() + + # Handle measurement + if op_name in ("measure", "m", "mz"): + for qubit, result in zip(qubits, results): + self._measured_qubits[pointer_id(qubit)] = True + qis.mz(self._builder, qubit, result) + # Handle barrier + elif op_name == "barrier": + if self._emit_barrier_calls: + qis.barrier(self._builder) + # Handle no-op instructions + elif op_name in NOOP_INSTRUCTIONS: + pass + # Handle standard gates + elif op_name == "swap": + qis.swap(self._builder, *qubits) + elif op_name == "ccx": + qis.ccx(self._builder, *qubits) + elif op_name == "cx": + qis.cx(self._builder, *qubits) + elif op_name == "cz": + qis.cz(self._builder, *qubits) + elif op_name == "h": + qis.h(self._builder, *qubits) + elif op_name == "reset": + qis.reset(self._builder, qubits[0]) + elif op_name == "rx": + qis.rx(self._builder, *instruction.params, *qubits) + elif op_name == "ry": + qis.ry(self._builder, *instruction.params, *qubits) + elif op_name == "rz": + qis.rz(self._builder, *instruction.params, *qubits) + elif op_name == "s": + qis.s(self._builder, *qubits) + elif op_name == "sdg": + qis.s_adj(self._builder, *qubits) + elif op_name == "t": + qis.t(self._builder, *qubits) + elif op_name == "tdg": + qis.t_adj(self._builder, *qubits) + elif op_name == "x": + qis.x(self._builder, *qubits) + elif op_name == "y": + qis.y(self._builder, *qubits) + elif op_name == "z": + qis.z(self._builder, *qubits) + elif op_name == "id": + # Identity: apply X twice (no-op effect) + qubit = qubits[0] + qis.x(self._builder, qubit) + qis.x(self._builder, qubit) + # Handle composite instructions (instructions with definitions) + elif instruction.definition is not None: + logger.debug( + "Processing composite instruction %s with qubits %s", + instruction.name, + qargs, + ) + self._process_composite_instruction(instruction, qargs, cargs) + else: + raise QiskitConversionError( + f"Gate '{instruction.name}' is not supported. " + f"Please transpile using supported gates: {SUPPORTED_INSTRUCTIONS}" + ) + + def ir(self) -> str: + """Return the QIR as a string.""" + return str(self._module) + + def bitcode(self) -> bytes: + """Return the QIR as bitcode.""" + return self._module.bitcode() diff --git a/tests/qiskit_qir/__init__.py b/tests/qiskit_qir/__init__.py new file mode 100644 index 00000000..cd1e5fa6 --- /dev/null +++ b/tests/qiskit_qir/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2025 qBraid +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Qiskit QIR tests package.""" diff --git a/tests/qiskit_qir/conftest.py b/tests/qiskit_qir/conftest.py new file mode 100644 index 00000000..bdc42abd --- /dev/null +++ b/tests/qiskit_qir/conftest.py @@ -0,0 +1,166 @@ +# Copyright 2025 qBraid +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Pytest fixtures for Qiskit QIR tests. + +""" + +import pytest +from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister + + +@pytest.fixture() +def bell_circuit(): + """A simple Bell state circuit.""" + circuit = QuantumCircuit(2, 2) + circuit.h(0) + circuit.cx(0, 1) + circuit.measure([0, 1], [0, 1]) + return circuit + + +@pytest.fixture() +def ghz_circuit(): + """A 3-qubit GHZ state circuit.""" + circuit = QuantumCircuit(3, 3) + circuit.h(0) + circuit.cx(0, 1) + circuit.cx(1, 2) + circuit.measure([0, 1, 2], [0, 1, 2]) + return circuit + + +@pytest.fixture() +def single_qubit_gates_circuit(): + """Circuit with various single-qubit gates.""" + circuit = QuantumCircuit(1) + circuit.h(0) + circuit.x(0) + circuit.y(0) + circuit.z(0) + circuit.s(0) + circuit.sdg(0) + circuit.t(0) + circuit.tdg(0) + return circuit + + +@pytest.fixture() +def rotation_gates_circuit(): + """Circuit with rotation gates.""" + circuit = QuantumCircuit(1) + circuit.rx(0.5, 0) + circuit.ry(1.0, 0) + circuit.rz(1.5, 0) + return circuit + + +@pytest.fixture() +def two_qubit_gates_circuit(): + """Circuit with two-qubit gates.""" + circuit = QuantumCircuit(2) + circuit.cx(0, 1) + circuit.cz(0, 1) + circuit.swap(0, 1) + return circuit + + +@pytest.fixture() +def three_qubit_gates_circuit(): + """Circuit with three-qubit gates.""" + circuit = QuantumCircuit(3) + circuit.ccx(0, 1, 2) + return circuit + + +@pytest.fixture() +def reset_circuit(): + """Circuit with reset gate.""" + circuit = QuantumCircuit(1) + circuit.reset(0) + circuit.h(0) + return circuit + + +@pytest.fixture() +def identity_circuit(): + """Circuit with identity gate.""" + circuit = QuantumCircuit(1) + circuit.id(0) + return circuit + + +@pytest.fixture() +def barrier_circuit(): + """Circuit with barrier.""" + circuit = QuantumCircuit(2) + circuit.h(0) + circuit.barrier() + circuit.cx(0, 1) + return circuit + + +@pytest.fixture() +def delay_circuit(): + """Circuit with delay instruction.""" + circuit = QuantumCircuit(1) + circuit.h(0) + circuit.delay(100, 0, "ns") + circuit.x(0) + return circuit + + +@pytest.fixture() +def named_registers_circuit(): + """Circuit with named quantum and classical registers.""" + qr = QuantumRegister(2, name="qreg") + cr = ClassicalRegister(2, name="creg") + circuit = QuantumCircuit(qr, cr, name="named_circuit") + circuit.h(qr[0]) + circuit.cx(qr[0], qr[1]) + circuit.measure(qr, cr) + return circuit + + +@pytest.fixture() +def multiple_registers_circuit(): + """Circuit with multiple registers.""" + qr1 = QuantumRegister(2, name="q1") + qr2 = QuantumRegister(1, name="q2") + cr1 = ClassicalRegister(2, name="c1") + cr2 = ClassicalRegister(1, name="c2") + circuit = QuantumCircuit(qr1, qr2, cr1, cr2, name="multi_reg") + circuit.h(qr1[0]) + circuit.cx(qr1[0], qr1[1]) + circuit.h(qr2[0]) + circuit.measure(qr1, cr1) + circuit.measure(qr2, cr2) + return circuit + + +@pytest.fixture() +def composite_gate_circuit(): + """Circuit with a composite (custom) gate.""" + # Create a custom gate from a circuit + sub_circuit = QuantumCircuit(2, name="bell_prep") + sub_circuit.h(0) + sub_circuit.cx(0, 1) + bell_gate = sub_circuit.to_gate() + + # Use the custom gate in a larger circuit + circuit = QuantumCircuit(2, 2) + circuit.append(bell_gate, [0, 1]) + circuit.measure([0, 1], [0, 1]) + return circuit diff --git a/tests/qiskit_qir/test_basic_gates.py b/tests/qiskit_qir/test_basic_gates.py new file mode 100644 index 00000000..e5d605f4 --- /dev/null +++ b/tests/qiskit_qir/test_basic_gates.py @@ -0,0 +1,206 @@ +# Copyright 2025 qBraid +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Unit tests for basic Qiskit gate conversions to QIR. + +""" + +import pytest +from qiskit import QuantumCircuit + +from qbraid_qir.qiskit import qiskit_to_qir +from tests.qir_utils import check_attributes_on_entrypoint, get_entry_point + +# Single qubit gates mapping: qiskit_name -> qir_name +SINGLE_QUBIT_GATES = { + "h": "h", + "x": "x", + "y": "y", + "z": "z", + "s": "s", + "t": "t", + "reset": "reset", +} + +# Adjoint gates mapping: qiskit_name -> qir_name (uses __adj suffix) +ADJOINT_GATES = { + "sdg": "s", + "tdg": "t", +} + +# Rotation gates +ROTATION_GATES = ["rx", "ry", "rz"] + +# Two qubit gates mapping: qiskit_name -> qir_name +TWO_QUBIT_GATES = { + "cx": "cnot", + "cz": "cz", + "swap": "swap", +} + + +class TestSingleQubitGates: + """Tests for single qubit gate conversions.""" + + @pytest.mark.parametrize("gate_name,qir_name", SINGLE_QUBIT_GATES.items()) + def test_single_qubit_gate(self, gate_name, qir_name): + """Test conversion of single qubit gates.""" + circuit = QuantumCircuit(1) + getattr(circuit, gate_name)(0) + + module = qiskit_to_qir(circuit) + func = get_entry_point(module) + + check_attributes_on_entrypoint(func, expected_qubits=1, expected_results=0) + + ir = str(module) + assert f"__quantum__qis__{qir_name}__body" in ir + + +class TestAdjointGates: + """Tests for adjoint gate conversions.""" + + @pytest.mark.parametrize("gate_name,qir_name", ADJOINT_GATES.items()) + def test_adjoint_gate(self, gate_name, qir_name): + """Test conversion of adjoint gates.""" + circuit = QuantumCircuit(1) + getattr(circuit, gate_name)(0) + + module = qiskit_to_qir(circuit) + func = get_entry_point(module) + + check_attributes_on_entrypoint(func, expected_qubits=1, expected_results=0) + + ir = str(module) + assert f"__quantum__qis__{qir_name}__adj" in ir + + +class TestRotationGates: + """Tests for rotation gate conversions.""" + + @pytest.mark.parametrize("gate_name", ROTATION_GATES) + def test_rotation_gate(self, gate_name): + """Test conversion of rotation gates.""" + circuit = QuantumCircuit(1) + getattr(circuit, gate_name)(0.5, 0) + + module = qiskit_to_qir(circuit) + func = get_entry_point(module) + + check_attributes_on_entrypoint(func, expected_qubits=1, expected_results=0) + + ir = str(module) + assert f"__quantum__qis__{gate_name}__body" in ir + # Check that the rotation angle is included + assert "0.5" in ir or "5.0" in ir or "double" in ir + + +class TestTwoQubitGates: + """Tests for two qubit gate conversions.""" + + @pytest.mark.parametrize("gate_name,qir_name", TWO_QUBIT_GATES.items()) + def test_two_qubit_gate(self, gate_name, qir_name): + """Test conversion of two qubit gates.""" + circuit = QuantumCircuit(2) + getattr(circuit, gate_name)(0, 1) + + module = qiskit_to_qir(circuit) + func = get_entry_point(module) + + check_attributes_on_entrypoint(func, expected_qubits=2, expected_results=0) + + ir = str(module) + assert f"__quantum__qis__{qir_name}__body" in ir + + +class TestThreeQubitGates: + """Tests for three qubit gate conversions.""" + + def test_ccx_gate(self): + """Test conversion of CCX (Toffoli) gate.""" + circuit = QuantumCircuit(3) + circuit.ccx(0, 1, 2) + + module = qiskit_to_qir(circuit) + func = get_entry_point(module) + + check_attributes_on_entrypoint(func, expected_qubits=3, expected_results=0) + + ir = str(module) + assert "__quantum__qis__ccx__body" in ir + + +class TestMeasurement: + """Tests for measurement operations.""" + + def test_single_measurement(self): + """Test conversion of single qubit measurement.""" + circuit = QuantumCircuit(1, 1) + circuit.measure(0, 0) + + module = qiskit_to_qir(circuit) + func = get_entry_point(module) + + check_attributes_on_entrypoint(func, expected_qubits=1, expected_results=1) + + ir = str(module) + assert "__quantum__qis__mz__body" in ir + + def test_multiple_measurements(self): + """Test conversion of multiple measurements.""" + circuit = QuantumCircuit(3, 3) + circuit.measure([0, 1, 2], [0, 1, 2]) + + module = qiskit_to_qir(circuit) + func = get_entry_point(module) + + check_attributes_on_entrypoint(func, expected_qubits=3, expected_results=3) + + ir = str(module) + # Count calls, not declarations + assert ir.count("call void @__quantum__qis__mz__body") == 3 + + +class TestOutputRecording: + """Tests for output recording functionality.""" + + def test_output_recording_single_register(self): + """Test that output recording is generated for single register.""" + circuit = QuantumCircuit(2, 2) + circuit.h(0) + circuit.cx(0, 1) + circuit.measure([0, 1], [0, 1]) + + module = qiskit_to_qir(circuit) + ir = str(module) + + # Should have array_record_output call + assert "__quantum__rt__array_record_output" in ir + # Should have result_record_output calls + assert "__quantum__rt__result_record_output" in ir + + def test_output_recording_disabled(self): + """Test that output recording can be disabled.""" + circuit = QuantumCircuit(2, 2) + circuit.h(0) + circuit.cx(0, 1) + circuit.measure([0, 1], [0, 1]) + + module = qiskit_to_qir(circuit, record_output=False) + ir = str(module) + + # Should NOT have output recording calls + assert "__quantum__rt__array_record_output" not in ir + assert "__quantum__rt__result_record_output" not in ir diff --git a/tests/qiskit_qir/test_qiskit_to_qir.py b/tests/qiskit_qir/test_qiskit_to_qir.py new file mode 100644 index 00000000..11f36121 --- /dev/null +++ b/tests/qiskit_qir/test_qiskit_to_qir.py @@ -0,0 +1,224 @@ +# Copyright 2025 qBraid +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Unit tests for Qiskit to QIR conversion. + +""" + +import pytest +from pyqir import Context, is_entry_point, qir_module, required_num_qubits, required_num_results +from qiskit import QuantumCircuit + +from qbraid_qir.qiskit import BasicQiskitVisitor, QiskitModule, qiskit_to_qir + + +class TestQiskitToQir: + """Tests for the qiskit_to_qir function.""" + + def test_bell_circuit(self, bell_circuit): + """Test conversion of a Bell state circuit.""" + module = qiskit_to_qir(bell_circuit) + assert module is not None + ir = str(module) + assert "__quantum__qis__h__body" in ir + assert "__quantum__qis__cnot__body" in ir + assert "__quantum__qis__mz__body" in ir + + def test_ghz_circuit(self, ghz_circuit): + """Test conversion of a GHZ state circuit.""" + module = qiskit_to_qir(ghz_circuit) + assert module is not None + # Verify the entry point has correct attributes + func = next(filter(is_entry_point, module.functions)) + assert required_num_qubits(func) == 3 + assert required_num_results(func) == 3 + + def test_single_qubit_gates(self, single_qubit_gates_circuit): + """Test conversion of single-qubit gates.""" + module = qiskit_to_qir(single_qubit_gates_circuit) + ir = str(module) + assert "__quantum__qis__h__body" in ir + assert "__quantum__qis__x__body" in ir + assert "__quantum__qis__y__body" in ir + assert "__quantum__qis__z__body" in ir + assert "__quantum__qis__s__body" in ir + assert "__quantum__qis__s__adj" in ir + assert "__quantum__qis__t__body" in ir + assert "__quantum__qis__t__adj" in ir + + def test_rotation_gates(self, rotation_gates_circuit): + """Test conversion of rotation gates.""" + module = qiskit_to_qir(rotation_gates_circuit) + ir = str(module) + assert "__quantum__qis__rx__body" in ir + assert "__quantum__qis__ry__body" in ir + assert "__quantum__qis__rz__body" in ir + + def test_two_qubit_gates(self, two_qubit_gates_circuit): + """Test conversion of two-qubit gates.""" + module = qiskit_to_qir(two_qubit_gates_circuit) + ir = str(module) + assert "__quantum__qis__cnot__body" in ir + assert "__quantum__qis__cz__body" in ir + assert "__quantum__qis__swap__body" in ir + + def test_three_qubit_gates(self, three_qubit_gates_circuit): + """Test conversion of three-qubit gates (CCX/Toffoli).""" + module = qiskit_to_qir(three_qubit_gates_circuit) + ir = str(module) + assert "__quantum__qis__ccx__body" in ir + + def test_reset_gate(self, reset_circuit): + """Test conversion of reset gate.""" + module = qiskit_to_qir(reset_circuit) + ir = str(module) + assert "__quantum__qis__reset__body" in ir + + def test_identity_gate(self, identity_circuit): + """Test conversion of identity gate (implemented as X.X).""" + module = qiskit_to_qir(identity_circuit) + ir = str(module) + # Identity is implemented as two X gates (count calls, not declarations) + assert ir.count("call void @__quantum__qis__x__body") == 2 + + def test_barrier_not_emitted_by_default(self, barrier_circuit): + """Test that barrier is not emitted by default.""" + module = qiskit_to_qir(barrier_circuit) + ir = str(module) + assert "__quantum__qis__barrier__body" not in ir + + def test_barrier_emitted_when_enabled(self, barrier_circuit): + """Test that barrier is emitted when emit_barrier_calls=True.""" + module = qiskit_to_qir(barrier_circuit, emit_barrier_calls=True) + ir = str(module) + assert "__quantum__qis__barrier__body" in ir + + def test_delay_ignored(self, delay_circuit): + """Test that delay instruction is ignored (no-op).""" + module = qiskit_to_qir(delay_circuit) + ir = str(module) + # Delay should not produce any output + assert "delay" not in ir.lower() + # But the other gates should be present + assert "__quantum__qis__h__body" in ir + assert "__quantum__qis__x__body" in ir + + def test_named_registers(self, named_registers_circuit): + """Test conversion with named quantum and classical registers.""" + module = qiskit_to_qir(named_registers_circuit, name="test_named") + assert module is not None + ir = str(module) + assert "test_named" in ir or "@test_named" in ir + + def test_multiple_registers(self, multiple_registers_circuit): + """Test conversion with multiple quantum and classical registers.""" + module = qiskit_to_qir(multiple_registers_circuit) + func = next(filter(is_entry_point, module.functions)) + # Total: 2 + 1 = 3 qubits, 2 + 1 = 3 classical bits + assert required_num_qubits(func) == 3 + assert required_num_results(func) == 3 + + def test_composite_gate(self, composite_gate_circuit): + """Test conversion of composite (custom) gates.""" + module = qiskit_to_qir(composite_gate_circuit) + ir = str(module) + # The composite gate should be decomposed + assert "__quantum__qis__h__body" in ir + assert "__quantum__qis__cnot__body" in ir + assert "__quantum__qis__mz__body" in ir + + def test_custom_name(self, bell_circuit): + """Test that custom module name is used.""" + module = qiskit_to_qir(bell_circuit, name="custom_bell") + ir = str(module) + assert "custom_bell" in ir or "@custom_bell" in ir + + def test_no_runtime_init(self, bell_circuit): + """Test conversion without runtime initialization.""" + module = qiskit_to_qir(bell_circuit, initialize_runtime=False) + ir = str(module) + assert "__quantum__rt__initialize" not in ir + + def test_no_output_recording(self, bell_circuit): + """Test conversion without output recording.""" + module = qiskit_to_qir(bell_circuit, record_output=False) + ir = str(module) + assert "__quantum__rt__result_record_output" not in ir + assert "__quantum__rt__array_record_output" not in ir + + +class TestQiskitToQirErrors: + """Tests for error handling in qiskit_to_qir.""" + + def test_invalid_input_type(self): + """Test that TypeError is raised for non-QuantumCircuit input.""" + with pytest.raises(TypeError, match="must be of type qiskit.QuantumCircuit"): + qiskit_to_qir("not a circuit") + + def test_empty_circuit(self): + """Test that ValueError is raised for empty circuit.""" + circuit = QuantumCircuit(1) + with pytest.raises(ValueError, match="at least one operation"): + qiskit_to_qir(circuit) + + @pytest.mark.skip(reason="Most gates in qiskit 2.x have decompositions to base gates") + def test_unsupported_gate(self): + """Test that QiskitConversionError is raised for unsupported gates.""" + # In qiskit 2.x, most gates have decompositions, so it's hard to find + # a gate that will truly fail. This test is skipped for now. + + +class TestQiskitModule: + """Tests for QiskitModule class.""" + + def test_from_circuit(self, bell_circuit): + """Test QiskitModule.from_circuit factory method.""" + module = QiskitModule.from_circuit(bell_circuit, None) + assert module.num_qubits == 2 + assert module.num_clbits == 2 + assert module.name == bell_circuit.name or module.name == "main" + + def test_circuit_property(self, bell_circuit): + """Test that circuit property returns the original circuit.""" + module = QiskitModule.from_circuit(bell_circuit, None) + assert module.circuit is bell_circuit + + +class TestBasicQiskitVisitor: + """Tests for BasicQiskitVisitor class.""" + + def test_visitor_entry_point(self, bell_circuit): + """Test that visitor generates correct entry point.""" + llvm_module = qir_module(Context(), "test") + qiskit_module = QiskitModule.from_circuit(bell_circuit, llvm_module) + + visitor = BasicQiskitVisitor() + qiskit_module.accept(visitor) + + assert visitor.entry_point is not None + assert len(visitor.entry_point) > 0 + + def test_visitor_ir_output(self, bell_circuit): + """Test that visitor can generate IR string.""" + llvm_module = qir_module(Context(), "test") + qiskit_module = QiskitModule.from_circuit(bell_circuit, llvm_module) + + visitor = BasicQiskitVisitor() + qiskit_module.accept(visitor) + + ir = visitor.ir() + assert ir is not None + assert len(ir) > 0 + assert "__quantum__qis__h__body" in ir diff --git a/tox.ini b/tox.ini index fbd683b2..d640cf7e 100644 --- a/tox.ini +++ b/tox.ini @@ -19,6 +19,7 @@ deps = -r {toxinidir}/requirements-test.txt extras = cirq qasm3 + qiskit squin commands = pytest tests --cov=qbraid_qir --cov-config=pyproject.toml --cov-report=term --cov-report=xml {posargs} @@ -29,6 +30,7 @@ deps = -r {toxinidir}/requirements-test.txt extras = cirq qasm3 + qiskit squin test commands_pre = @@ -42,6 +44,7 @@ deps = -r {toxinidir}/requirements-test.txt extras = cirq qasm3 + qiskit squin test commands_pre = From a44cba2acc7654bed8a9a4d2bd50a2d9d9e0e16b Mon Sep 17 00:00:00 2001 From: eidrynbot Date: Tue, 3 Mar 2026 10:33:10 -0600 Subject: [PATCH 2/9] fix: update license headers with proper MIT attribution format Follow the pattern from qBraid SDK (azure/result_builder.py): - Keep Apache 2.0 as the primary license - Add pylint comments and attribution block referencing microsoft/qiskit-qir - Reference NOTICE.md for full MIT license text --- qbraid_qir/qiskit/__init__.py | 7 +++++-- qbraid_qir/qiskit/convert.py | 8 +++++--- qbraid_qir/qiskit/elements.py | 8 +++++--- qbraid_qir/qiskit/visitor.py | 8 +++++--- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/qbraid_qir/qiskit/__init__.py b/qbraid_qir/qiskit/__init__.py index 83aa08bc..b879ba9a 100644 --- a/qbraid_qir/qiskit/__init__.py +++ b/qbraid_qir/qiskit/__init__.py @@ -12,8 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -# This module is derived from microsoft/qiskit-qir (MIT License). -# See NOTICE.md for attribution details. +# pylint: disable=line-too-long +# Portions of this module are adapted from microsoft/qiskit-qir +# (https://github.com/microsoft/qiskit-qir), with modifications by qBraid. +# The original MIT license notice is reproduced in NOTICE.md. +# pylint: enable=line-too-long """ Module containing Qiskit QIR functionality. diff --git a/qbraid_qir/qiskit/convert.py b/qbraid_qir/qiskit/convert.py index 454056cb..d7e14683 100644 --- a/qbraid_qir/qiskit/convert.py +++ b/qbraid_qir/qiskit/convert.py @@ -12,9 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -# This module is derived from microsoft/qiskit-qir (MIT License). -# Original code: Copyright (c) Microsoft Corporation. -# See NOTICE.md for attribution details. +# pylint: disable=line-too-long +# Portions of this module are adapted from microsoft/qiskit-qir +# (https://github.com/microsoft/qiskit-qir), with modifications by qBraid. +# The original MIT license notice is reproduced in NOTICE.md. +# pylint: enable=line-too-long """ Module containing Qiskit to QIR conversion functions. diff --git a/qbraid_qir/qiskit/elements.py b/qbraid_qir/qiskit/elements.py index 29e2f494..e2b030b4 100644 --- a/qbraid_qir/qiskit/elements.py +++ b/qbraid_qir/qiskit/elements.py @@ -12,9 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -# This module is derived from microsoft/qiskit-qir (MIT License). -# Original code: Copyright (c) Microsoft Corporation. -# See NOTICE.md for attribution details. +# pylint: disable=line-too-long +# Portions of this module are adapted from microsoft/qiskit-qir +# (https://github.com/microsoft/qiskit-qir), with modifications by qBraid. +# The original MIT license notice is reproduced in NOTICE.md. +# pylint: enable=line-too-long """ Module defining Qiskit circuit elements for QIR conversion. diff --git a/qbraid_qir/qiskit/visitor.py b/qbraid_qir/qiskit/visitor.py index 868c3ae9..4bfa3208 100644 --- a/qbraid_qir/qiskit/visitor.py +++ b/qbraid_qir/qiskit/visitor.py @@ -12,9 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -# This module is derived from microsoft/qiskit-qir (MIT License). -# Original code: Copyright (c) Microsoft Corporation. -# See NOTICE.md for attribution details. +# pylint: disable=line-too-long +# Portions of this module are adapted from microsoft/qiskit-qir +# (https://github.com/microsoft/qiskit-qir), with modifications by qBraid. +# The original MIT license notice is reproduced in NOTICE.md. +# pylint: enable=line-too-long """ Module defining QiskitVisitor for QIR conversion. From 9f3090327122d278f534236948ed17cf474c2db7 Mon Sep 17 00:00:00 2001 From: eidrynbot Date: Tue, 3 Mar 2026 10:34:19 -0600 Subject: [PATCH 3/9] docs: link CHANGELOG to tracking issue #272 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d563449b..e3bc3603 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ Types of changes: ### ➕ New Features -- Added `qbraid_qir.qiskit` module for Qiskit to QIR conversion, ported from the archived [microsoft/qiskit-qir](https://github.com/microsoft/qiskit-qir) repository (MIT License). The module has been updated for compatibility with Qiskit 2.x and follows qbraid-qir conventions. Main entry point is `qiskit_to_qir(circuit, name, **kwargs)` which converts a Qiskit `QuantumCircuit` to a PyQIR `Module`. ([#XXXX](https://github.com/qBraid/qbraid-qir/pull/XXXX)) +- Added `qbraid_qir.qiskit` module for Qiskit to QIR conversion, ported from the archived [microsoft/qiskit-qir](https://github.com/microsoft/qiskit-qir) repository (MIT License). The module has been updated for compatibility with Qiskit 2.x and follows qbraid-qir conventions. Main entry point is `qiskit_to_qir(circuit, name, **kwargs)` which converts a Qiskit `QuantumCircuit` to a PyQIR `Module`. ([#272](https://github.com/qBraid/qbraid-qir/issues/272), [#271](https://github.com/qBraid/qbraid-qir/pull/271)) ### 🌟 Improvements From b69c5d9ee084bbd55c8fea97d8c976a3485738ac Mon Sep 17 00:00:00 2001 From: eidrynbot Date: Wed, 4 Mar 2026 19:40:27 -0600 Subject: [PATCH 4/9] fix: resolve CI lint errors and add missing qiskit extra to test workflow The CI was failing for two reasons: 1. Pylint violations in the new qiskit module (too-many-arguments, too-many-instance-attributes, too-many-branches, too-many-statements, consider-using-from-import, unnecessary-comprehension, consider-using-in) 2. Missing `--extra qiskit` in the test workflow, causing ModuleNotFoundError for qiskit during test collection Co-Authored-By: Claude Opus 4.6 --- .github/workflows/main.yml | 2 +- qbraid_qir/qiskit/elements.py | 2 +- qbraid_qir/qiskit/visitor.py | 11 ++++++----- tests/qiskit_qir/test_qiskit_to_qir.py | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a40d3f8c..50ee3eb8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -61,7 +61,7 @@ jobs: pip install --upgrade pip pip install setuptools pip install -r requirements-test.txt - bin/install_wheel_extras.sh dist --extra qasm3 --extra cirq --extra squin + bin/install_wheel_extras.sh dist --extra qasm3 --extra cirq --extra qiskit --extra squin - name: Run tests with pyqir 0.11.x (typed pointers) run: | pip install "pyqir>=0.10.0,<0.12" diff --git a/qbraid_qir/qiskit/elements.py b/qbraid_qir/qiskit/elements.py index e2b030b4..3ff9fa16 100644 --- a/qbraid_qir/qiskit/elements.py +++ b/qbraid_qir/qiskit/elements.py @@ -94,7 +94,7 @@ class QiskitModule: reg_sizes: List of sizes for each classical register. """ - def __init__( + def __init__( # pylint: disable=too-many-arguments self, circuit: QuantumCircuit, name: str, diff --git a/qbraid_qir/qiskit/visitor.py b/qbraid_qir/qiskit/visitor.py index 4bfa3208..d64fdeb0 100644 --- a/qbraid_qir/qiskit/visitor.py +++ b/qbraid_qir/qiskit/visitor.py @@ -28,8 +28,7 @@ from typing import Union import pyqir -import pyqir.qis as qis -import pyqir.rt as rt +from pyqir import qis, rt from pyqir import BasicBlock, Builder, Constant, IntType, PointerType, entry_point from qiskit import ClassicalRegister, QuantumRegister from qiskit.circuit import Clbit, Qubit @@ -83,7 +82,9 @@ def visit_instruction(self, instruction, qargs, cargs): """Visit an instruction element.""" -class BasicQiskitVisitor(QuantumCircuitElementVisitor): +class BasicQiskitVisitor( # pylint: disable=too-many-instance-attributes + QuantumCircuitElementVisitor +): """A visitor for basic Qiskit circuit elements. This class traverses and converts Qiskit circuit elements to QIR. @@ -182,7 +183,7 @@ def visit_register(self, register: Union[QuantumRegister, ClassicalRegister]) -> self._qubit_labels.update( {bit: n + len(self._qubit_labels) for n, bit in enumerate(register)} ) - logger.debug("Added labels for qubits %s", [bit for bit in register]) + logger.debug("Added labels for qubits %s", list(register)) elif isinstance(register, ClassicalRegister): self._clbit_labels.update( {bit: n + len(self._clbit_labels) for n, bit in enumerate(register)} @@ -235,7 +236,7 @@ def _process_composite_instruction( ) self.visit_instruction(inst, mapped_qbits, mapped_clbits) - def visit_instruction( + def visit_instruction( # pylint: disable=too-many-branches,too-many-statements self, instruction: Instruction, qargs: tuple[Qubit, ...], diff --git a/tests/qiskit_qir/test_qiskit_to_qir.py b/tests/qiskit_qir/test_qiskit_to_qir.py index 11f36121..71306092 100644 --- a/tests/qiskit_qir/test_qiskit_to_qir.py +++ b/tests/qiskit_qir/test_qiskit_to_qir.py @@ -188,7 +188,7 @@ def test_from_circuit(self, bell_circuit): module = QiskitModule.from_circuit(bell_circuit, None) assert module.num_qubits == 2 assert module.num_clbits == 2 - assert module.name == bell_circuit.name or module.name == "main" + assert module.name in (bell_circuit.name, "main") def test_circuit_property(self, bell_circuit): """Test that circuit property returns the original circuit.""" From 8cb78ecc84945dbe8b8f8ab5a561524aaf4a76e1 Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Tue, 19 May 2026 12:32:34 +0530 Subject: [PATCH 5/9] feat: refactor qiskit visitor to map-based dispatch, add transpile flag, expand tests - Extract gate mappings into qbraid_qir/qiskit/maps.py (mirrors qasm3/maps.py pattern) - Refactor visitor.py if-else chain to dict-based lookup (~60 lines -> ~15 lines) - Add transpile=bool parameter to qiskit_to_qir() for pre-conversion transpilation - Tighten qiskit version pin to >=2.0,<3.0 - Identity gate is now a true no-op (no QIR emitted) - Rewrite test_basic_gates.py with sequential equivalence checks (47 tests) - Rewrite test_qiskit_to_qir.py with integration, error, visitor, maps, and API tests - Add test_complex_circuits.py: teleportation, Grover, parameterized, custom gates, large circuits, transpilation, edge cases, and 10 random circuit tests - Fix mypy type errors in elements.py and visitor.py Co-Authored-By: Claude Opus 4.6 --- pyproject.toml | 2 +- qbraid_qir/qiskit/__init__.py | 2 +- qbraid_qir/qiskit/convert.py | 17 +- qbraid_qir/qiskit/elements.py | 6 +- qbraid_qir/qiskit/exceptions.py | 2 +- qbraid_qir/qiskit/maps.py | 112 +++++ qbraid_qir/qiskit/visitor.py | 128 ++---- tests/qiskit_qir/__init__.py | 2 +- tests/qiskit_qir/conftest.py | 2 +- tests/qiskit_qir/test_basic_gates.py | 486 ++++++++++++++++++---- tests/qiskit_qir/test_complex_circuits.py | 465 +++++++++++++++++++++ tests/qiskit_qir/test_qiskit_to_qir.py | 307 ++++++++++---- 12 files changed, 1264 insertions(+), 267 deletions(-) create mode 100644 qbraid_qir/qiskit/maps.py create mode 100644 tests/qiskit_qir/test_complex_circuits.py diff --git a/pyproject.toml b/pyproject.toml index 794c6012..b4a391bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ Discord = "https://discord.gg/TPBU2sa8Et" [project.optional-dependencies] cirq = ["cirq-core>=1.3.0,<1.6.0"] qasm3 = ["pyqasm>=0.4.0,<1.1.0", "numpy"] -qiskit = ["qiskit>=2.0"] +qiskit = ["qiskit>=2.0,<3.0"] squin = ["kirin-toolchain>=0.17.33", "bloqade-circuit>=0.9.1"] [tool.setuptools] diff --git a/qbraid_qir/qiskit/__init__.py b/qbraid_qir/qiskit/__init__.py index b879ba9a..8430e1f1 100644 --- a/qbraid_qir/qiskit/__init__.py +++ b/qbraid_qir/qiskit/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2025 qBraid +# Copyright 2026 qBraid # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/qbraid_qir/qiskit/convert.py b/qbraid_qir/qiskit/convert.py index d7e14683..9c4c14c9 100644 --- a/qbraid_qir/qiskit/convert.py +++ b/qbraid_qir/qiskit/convert.py @@ -1,4 +1,4 @@ -# Copyright 2025 qBraid +# Copyright 2026 qBraid # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -27,19 +27,29 @@ from pyqir import Context, Module, qir_module from qiskit.circuit import QuantumCircuit +from qiskit.compiler import transpile as qiskit_transpile from .elements import QiskitModule, generate_module_id from .exceptions import QiskitConversionError +from .maps import QISKIT_BASIS_GATES from .visitor import BasicQiskitVisitor -def qiskit_to_qir(circuit: QuantumCircuit, name: Optional[str] = None, **kwargs) -> Module: +def qiskit_to_qir( + circuit: QuantumCircuit, + name: Optional[str] = None, + transpile: bool = False, + **kwargs, +) -> Module: """ Converts a Qiskit QuantumCircuit to a PyQIR module. Args: circuit: The Qiskit QuantumCircuit to convert. name: Identifier for created QIR module. Auto-generated if not provided. + transpile: If True, transpile the circuit to the supported basis gate set + before conversion. This enables conversion of circuits containing + gates not directly supported in QIR. Defaults to False. Keyword Args: initialize_runtime (bool): Whether to perform quantum runtime environment initialization, @@ -73,6 +83,9 @@ def qiskit_to_qir(circuit: QuantumCircuit, name: Optional[str] = None, **kwargs) if len(circuit.data) == 0: raise ValueError("Input quantum circuit must consist of at least one operation.") + if transpile: + circuit = qiskit_transpile(circuit, basis_gates=QISKIT_BASIS_GATES) + if name is None: name = generate_module_id(circuit) diff --git a/qbraid_qir/qiskit/elements.py b/qbraid_qir/qiskit/elements.py index 3ff9fa16..518dce96 100644 --- a/qbraid_qir/qiskit/elements.py +++ b/qbraid_qir/qiskit/elements.py @@ -1,4 +1,4 @@ -# Copyright 2025 qBraid +# Copyright 2026 qBraid # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -98,7 +98,7 @@ def __init__( # pylint: disable=too-many-arguments self, circuit: QuantumCircuit, name: str, - module: Module, + module: Optional[Module], num_qubits: int, num_clbits: int, reg_sizes: list[int], @@ -123,7 +123,7 @@ def name(self) -> str: return self._name @property - def module(self) -> Module: + def module(self) -> Optional[Module]: """Return the PyQIR module.""" return self._module diff --git a/qbraid_qir/qiskit/exceptions.py b/qbraid_qir/qiskit/exceptions.py index 9c02bbd3..80ce5587 100644 --- a/qbraid_qir/qiskit/exceptions.py +++ b/qbraid_qir/qiskit/exceptions.py @@ -1,4 +1,4 @@ -# Copyright 2025 qBraid +# Copyright 2026 qBraid # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/qbraid_qir/qiskit/maps.py b/qbraid_qir/qiskit/maps.py new file mode 100644 index 00000000..5a93fee6 --- /dev/null +++ b/qbraid_qir/qiskit/maps.py @@ -0,0 +1,112 @@ +# Copyright 2026 qBraid +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Module mapping supported Qiskit gates/operations to pyqir functions. + +""" + +from typing import Callable + +import pyqir._native + +from .exceptions import QiskitConversionError + + +def _identity(_builder, _qubit): + """Identity gate — no operation.""" + + +PYQIR_ONE_QUBIT_OP_MAP: dict[str, Callable] = { + "h": pyqir._native.h, + "x": pyqir._native.x, + "y": pyqir._native.y, + "z": pyqir._native.z, + "s": pyqir._native.s, + "sdg": pyqir._native.s_adj, + "t": pyqir._native.t, + "tdg": pyqir._native.t_adj, + "id": _identity, + "reset": pyqir._native.reset, +} + +PYQIR_ONE_QUBIT_ROTATION_MAP: dict[str, Callable] = { + "rx": pyqir._native.rx, + "ry": pyqir._native.ry, + "rz": pyqir._native.rz, +} + +PYQIR_TWO_QUBIT_OP_MAP: dict[str, Callable] = { + "cx": pyqir._native.cx, + "cz": pyqir._native.cz, + "swap": pyqir._native.swap, +} + +PYQIR_THREE_QUBIT_OP_MAP: dict[str, Callable] = { + "ccx": pyqir._native.ccx, +} + +PYQIR_MEASUREMENT_OP_MAP: dict[str, Callable] = { + "measure": pyqir._native.mz, + "m": pyqir._native.mz, + "mz": pyqir._native.mz, +} + +NOOP_INSTRUCTIONS: set[str] = {"delay"} + +SUPPORTED_INSTRUCTIONS: list[str] = sorted( + set(PYQIR_ONE_QUBIT_OP_MAP) + | set(PYQIR_ONE_QUBIT_ROTATION_MAP) + | set(PYQIR_TWO_QUBIT_OP_MAP) + | set(PYQIR_THREE_QUBIT_OP_MAP) + | set(PYQIR_MEASUREMENT_OP_MAP) + | {"barrier"} + | NOOP_INSTRUCTIONS +) + +QISKIT_BASIS_GATES: list[str] = sorted( + (set(PYQIR_ONE_QUBIT_OP_MAP) - {"reset"}) + | set(PYQIR_ONE_QUBIT_ROTATION_MAP) + | set(PYQIR_TWO_QUBIT_OP_MAP) + | set(PYQIR_THREE_QUBIT_OP_MAP) + | {"measure", "reset"} +) + + +def map_qiskit_op_to_pyqir_callable(op_name: str) -> tuple[Callable, int]: + """Map a Qiskit operation to a PyQIR callable and expected qubit count. + + Args: + op_name: The Qiskit operation name. + + Returns: + A tuple of (callable, num_qubits). + + Raises: + QiskitConversionError: If the operation is unsupported. + """ + op_name_lower = op_name.lower() + + op_mappings: list[tuple[dict, int]] = [ + (PYQIR_ONE_QUBIT_OP_MAP, 1), + (PYQIR_ONE_QUBIT_ROTATION_MAP, 1), + (PYQIR_TWO_QUBIT_OP_MAP, 2), + (PYQIR_THREE_QUBIT_OP_MAP, 3), + ] + + for mapping, num_qubits in op_mappings: + if op_name_lower in mapping: + return mapping[op_name_lower], num_qubits + + raise QiskitConversionError(f"Unsupported Qiskit operation: {op_name}") diff --git a/qbraid_qir/qiskit/visitor.py b/qbraid_qir/qiskit/visitor.py index d64fdeb0..f4804215 100644 --- a/qbraid_qir/qiskit/visitor.py +++ b/qbraid_qir/qiskit/visitor.py @@ -1,4 +1,4 @@ -# Copyright 2025 qBraid +# Copyright 2026 qBraid # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -28,8 +28,16 @@ from typing import Union import pyqir -from pyqir import qis, rt -from pyqir import BasicBlock, Builder, Constant, IntType, PointerType, entry_point +from pyqir import ( + BasicBlock, + Builder, + Constant, + IntType, + PointerType, + entry_point, + qis, + rt, +) from qiskit import ClassicalRegister, QuantumRegister from qiskit.circuit import Clbit, Qubit from qiskit.circuit.instruction import Instruction @@ -38,37 +46,18 @@ from .elements import QiskitModule from .exceptions import QiskitConversionError +from .maps import ( + NOOP_INSTRUCTIONS, + PYQIR_MEASUREMENT_OP_MAP, + PYQIR_ONE_QUBIT_OP_MAP, + PYQIR_ONE_QUBIT_ROTATION_MAP, + PYQIR_THREE_QUBIT_OP_MAP, + PYQIR_TWO_QUBIT_OP_MAP, + SUPPORTED_INSTRUCTIONS, +) logger = logging.getLogger(__name__) -# Supported quantum instructions -SUPPORTED_INSTRUCTIONS = [ - "barrier", - "ccx", - "cx", - "cz", - "h", - "id", - "m", - "measure", - "mz", - "reset", - "rx", - "ry", - "rz", - "s", - "sdg", - "swap", - "t", - "tdg", - "x", - "y", - "z", -] - -# Instructions that are no-ops (ignored during conversion) -NOOP_INSTRUCTIONS = ["delay"] - class QuantumCircuitElementVisitor(metaclass=ABCMeta): """Abstract base class for quantum circuit element visitors.""" @@ -124,6 +113,7 @@ def visit_qiskit_module(self, module: QiskitModule) -> None: module.num_qubits, module.num_clbits, ) + assert module.module is not None, "QiskitModule must have a PyQIR module set" self._module = module.module self._qiskit_module = module context = self._module.context @@ -236,7 +226,7 @@ def _process_composite_instruction( ) self.visit_instruction(inst, mapped_qbits, mapped_clbits) - def visit_instruction( # pylint: disable=too-many-branches,too-many-statements + def visit_instruction( self, instruction: Instruction, qargs: tuple[Qubit, ...], @@ -249,8 +239,8 @@ def visit_instruction( # pylint: disable=too-many-branches,too-many-statements qargs: Qubit arguments. cargs: Classical bit arguments. """ - qlabels = [self._qubit_labels.get(bit) for bit in qargs] - clabels = [self._clbit_labels.get(bit) for bit in cargs] + qlabels = [self._qubit_labels[bit] for bit in qargs] + clabels = [self._clbit_labels[bit] for bit in cargs] qubits = [pyqir.qubit(self._module.context, n) for n in qlabels] results = [pyqir.result(self._module.context, n) for n in clabels] @@ -259,63 +249,29 @@ def visit_instruction( # pylint: disable=too-many-branches,too-many-statements op_name = instruction.name.lower() - # Handle measurement - if op_name in ("measure", "m", "mz"): + if op_name in NOOP_INSTRUCTIONS: + return + + if op_name in PYQIR_MEASUREMENT_OP_MAP: for qubit, result in zip(qubits, results): - self._measured_qubits[pointer_id(qubit)] = True + qubit_id = pointer_id(qubit) + if qubit_id is not None: + self._measured_qubits[qubit_id] = True qis.mz(self._builder, qubit, result) - # Handle barrier elif op_name == "barrier": if self._emit_barrier_calls: qis.barrier(self._builder) - # Handle no-op instructions - elif op_name in NOOP_INSTRUCTIONS: - pass - # Handle standard gates - elif op_name == "swap": - qis.swap(self._builder, *qubits) - elif op_name == "ccx": - qis.ccx(self._builder, *qubits) - elif op_name == "cx": - qis.cx(self._builder, *qubits) - elif op_name == "cz": - qis.cz(self._builder, *qubits) - elif op_name == "h": - qis.h(self._builder, *qubits) - elif op_name == "reset": - qis.reset(self._builder, qubits[0]) - elif op_name == "rx": - qis.rx(self._builder, *instruction.params, *qubits) - elif op_name == "ry": - qis.ry(self._builder, *instruction.params, *qubits) - elif op_name == "rz": - qis.rz(self._builder, *instruction.params, *qubits) - elif op_name == "s": - qis.s(self._builder, *qubits) - elif op_name == "sdg": - qis.s_adj(self._builder, *qubits) - elif op_name == "t": - qis.t(self._builder, *qubits) - elif op_name == "tdg": - qis.t_adj(self._builder, *qubits) - elif op_name == "x": - qis.x(self._builder, *qubits) - elif op_name == "y": - qis.y(self._builder, *qubits) - elif op_name == "z": - qis.z(self._builder, *qubits) - elif op_name == "id": - # Identity: apply X twice (no-op effect) - qubit = qubits[0] - qis.x(self._builder, qubit) - qis.x(self._builder, qubit) - # Handle composite instructions (instructions with definitions) - elif instruction.definition is not None: - logger.debug( - "Processing composite instruction %s with qubits %s", - instruction.name, - qargs, + elif op_name in PYQIR_ONE_QUBIT_OP_MAP: + PYQIR_ONE_QUBIT_OP_MAP[op_name](self._builder, qubits[0]) + elif op_name in PYQIR_ONE_QUBIT_ROTATION_MAP: + PYQIR_ONE_QUBIT_ROTATION_MAP[op_name]( + self._builder, float(instruction.params[0]), qubits[0] ) + elif op_name in PYQIR_TWO_QUBIT_OP_MAP: + PYQIR_TWO_QUBIT_OP_MAP[op_name](self._builder, qubits[0], qubits[1]) + elif op_name in PYQIR_THREE_QUBIT_OP_MAP: + PYQIR_THREE_QUBIT_OP_MAP[op_name](self._builder, *qubits) + elif instruction.definition is not None: self._process_composite_instruction(instruction, qargs, cargs) else: raise QiskitConversionError( @@ -329,4 +285,4 @@ def ir(self) -> str: def bitcode(self) -> bytes: """Return the QIR as bitcode.""" - return self._module.bitcode() + return self._module.bitcode diff --git a/tests/qiskit_qir/__init__.py b/tests/qiskit_qir/__init__.py index cd1e5fa6..3cbf1654 100644 --- a/tests/qiskit_qir/__init__.py +++ b/tests/qiskit_qir/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2025 qBraid +# Copyright 2026 qBraid # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/qiskit_qir/conftest.py b/tests/qiskit_qir/conftest.py index bdc42abd..6deaccd9 100644 --- a/tests/qiskit_qir/conftest.py +++ b/tests/qiskit_qir/conftest.py @@ -1,4 +1,4 @@ -# Copyright 2025 qBraid +# Copyright 2026 qBraid # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/qiskit_qir/test_basic_gates.py b/tests/qiskit_qir/test_basic_gates.py index e5d605f4..df45d878 100644 --- a/tests/qiskit_qir/test_basic_gates.py +++ b/tests/qiskit_qir/test_basic_gates.py @@ -1,4 +1,4 @@ -# Copyright 2025 qBraid +# Copyright 2026 qBraid # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,186 +13,365 @@ # limitations under the License. """ -Unit tests for basic Qiskit gate conversions to QIR. +Unit tests for basic Qiskit gate conversions to QIR with sequential equivalence checks. """ +import math + import pytest +from pyqir import is_entry_point, required_num_qubits, required_num_results from qiskit import QuantumCircuit from qbraid_qir.qiskit import qiskit_to_qir -from tests.qir_utils import check_attributes_on_entrypoint, get_entry_point - -# Single qubit gates mapping: qiskit_name -> qir_name -SINGLE_QUBIT_GATES = { - "h": "h", - "x": "x", - "y": "y", - "z": "z", - "s": "s", - "t": "t", - "reset": "reset", -} - -# Adjoint gates mapping: qiskit_name -> qir_name (uses __adj suffix) -ADJOINT_GATES = { - "sdg": "s", - "tdg": "t", -} -# Rotation gates -ROTATION_GATES = ["rx", "ry", "rz"] -# Two qubit gates mapping: qiskit_name -> qir_name -TWO_QUBIT_GATES = { - "cx": "cnot", - "cz": "cz", - "swap": "swap", -} +def _get_body(module): + """Extract the entry point body as a list of stripped instruction lines.""" + func = next(filter(is_entry_point, module.functions)) + lines = str(func).splitlines()[2:-1] + return [line.strip() for line in lines] + + +def _get_entry_point(module): + """Get the entry point function from a module.""" + return next(filter(is_entry_point, module.functions)) + + +def _op_call(name, qubit=0): + """Generate expected single-qubit gate call string.""" + qb = "ptr null" if qubit == 0 else f"ptr inttoptr (i64 {qubit} to ptr)" + return f"call void @__quantum__qis__{name}__body({qb})" + + +def _adj_call(name, qubit=0): + """Generate expected adjoint gate call string.""" + qb = "ptr null" if qubit == 0 else f"ptr inttoptr (i64 {qubit} to ptr)" + return f"call void @__quantum__qis__{name}__adj({qb})" + + +def _rot_call(name, angle, qubit=0): + """Generate expected rotation gate call string.""" + qb = "ptr null" if qubit == 0 else f"ptr inttoptr (i64 {qubit} to ptr)" + return f"call void @__quantum__qis__{name}__body(double {angle:#e}, {qb})" + + +def _two_qubit_call(name, qb1=0, qb2=1): + """Generate expected two-qubit gate call string.""" + q1 = "ptr null" if qb1 == 0 else f"ptr inttoptr (i64 {qb1} to ptr)" + q2 = "ptr null" if qb2 == 0 else f"ptr inttoptr (i64 {qb2} to ptr)" + return f"call void @__quantum__qis__{name}__body({q1}, {q2})" + + +def _three_qubit_call(name, qb1=0, qb2=1, qb3=2): + """Generate expected three-qubit gate call string.""" + qubits = [] + for q in [qb1, qb2, qb3]: + qubits.append("ptr null" if q == 0 else f"ptr inttoptr (i64 {q} to ptr)") + return f"call void @__quantum__qis__{name}__body({', '.join(qubits)})" + + +def _mz_call(qubit=0, result=0): + """Generate expected measurement call string.""" + qb = "ptr null" if qubit == 0 else f"ptr inttoptr (i64 {qubit} to ptr)" + res = "ptr null" if result == 0 else f"ptr inttoptr (i64 {result} to ptr)" + return f"call void @__quantum__qis__mz__body({qb}, {res})" + + +def _assert_body_ops(module, expected_ops): + """Assert that the QIR body contains exactly the expected operations in order. + + Args: + module: PyQIR Module. + expected_ops: List of expected QIR instruction strings. + """ + body = _get_body(module) + + # Filter to only "call" lines and "ret" for comparison + gate_lines = [l for l in body if l.startswith("call void @__quantum__qis__")] + + expected_str = "\n ".join(expected_ops) + actual_str = "\n ".join(gate_lines) + assert len(gate_lines) == len(expected_ops), ( + f"Expected {len(expected_ops)} gate ops, got {len(gate_lines)}.\n" + f"Expected:\n {expected_str}\n" + f"Got:\n {actual_str}" + ) + for i, (actual, expected) in enumerate(zip(gate_lines, expected_ops)): + assert ( + actual == expected + ), f"Op mismatch at index {i}:\n expected: {expected}\n actual: {actual}" + + +# --------------------------------------------------------------------------- +# Single-qubit gates +# --------------------------------------------------------------------------- class TestSingleQubitGates: - """Tests for single qubit gate conversions.""" + """Tests for single qubit gate conversions with sequential equivalence.""" + + # Gate name maps to itself for single-qubit gates + GATES = {"h", "x", "y", "z", "s", "t", "reset"} - @pytest.mark.parametrize("gate_name,qir_name", SINGLE_QUBIT_GATES.items()) - def test_single_qubit_gate(self, gate_name, qir_name): - """Test conversion of single qubit gates.""" + @pytest.mark.parametrize("gate_name", GATES) + def test_single_qubit_gate(self, gate_name): circuit = QuantumCircuit(1) getattr(circuit, gate_name)(0) module = qiskit_to_qir(circuit) - func = get_entry_point(module) + func = _get_entry_point(module) + assert required_num_qubits(func) == 1 + assert required_num_results(func) == 0 - check_attributes_on_entrypoint(func, expected_qubits=1, expected_results=0) + _assert_body_ops(module, [_op_call(gate_name, 0)]) - ir = str(module) - assert f"__quantum__qis__{qir_name}__body" in ir + @pytest.mark.parametrize("gate_name", GATES) + def test_single_qubit_gate_on_qubit_1(self, gate_name): + """Test gate applied to qubit index 1 (not 0).""" + circuit = QuantumCircuit(2) + getattr(circuit, gate_name)(1) + + module = qiskit_to_qir(circuit) + _assert_body_ops(module, [_op_call(gate_name, 1)]) class TestAdjointGates: - """Tests for adjoint gate conversions.""" + """Tests for adjoint gate conversions with sequential equivalence.""" - @pytest.mark.parametrize("gate_name,qir_name", ADJOINT_GATES.items()) + GATES = {"sdg": "s", "tdg": "t"} + + @pytest.mark.parametrize("gate_name,qir_name", GATES.items()) def test_adjoint_gate(self, gate_name, qir_name): - """Test conversion of adjoint gates.""" circuit = QuantumCircuit(1) getattr(circuit, gate_name)(0) module = qiskit_to_qir(circuit) - func = get_entry_point(module) + func = _get_entry_point(module) + assert required_num_qubits(func) == 1 + assert required_num_results(func) == 0 - check_attributes_on_entrypoint(func, expected_qubits=1, expected_results=0) + _assert_body_ops(module, [_adj_call(qir_name, 0)]) - ir = str(module) - assert f"__quantum__qis__{qir_name}__adj" in ir + +class TestIdentityGate: + """Tests for identity gate (should be a no-op).""" + + def test_identity_is_noop(self): + circuit = QuantumCircuit(1) + circuit.id(0) + + module = qiskit_to_qir(circuit) + _assert_body_ops(module, []) + + def test_identity_between_gates(self): + """Identity between real gates should not affect the sequence.""" + circuit = QuantumCircuit(1) + circuit.h(0) + circuit.id(0) + circuit.x(0) + + module = qiskit_to_qir(circuit) + _assert_body_ops(module, [_op_call("h", 0), _op_call("x", 0)]) + + +# --------------------------------------------------------------------------- +# Rotation gates +# --------------------------------------------------------------------------- class TestRotationGates: - """Tests for rotation gate conversions.""" + """Tests for rotation gate conversions with sequential equivalence.""" - @pytest.mark.parametrize("gate_name", ROTATION_GATES) + @pytest.mark.parametrize("gate_name", ["rx", "ry", "rz"]) def test_rotation_gate(self, gate_name): - """Test conversion of rotation gates.""" circuit = QuantumCircuit(1) getattr(circuit, gate_name)(0.5, 0) module = qiskit_to_qir(circuit) - func = get_entry_point(module) + func = _get_entry_point(module) + assert required_num_qubits(func) == 1 + assert required_num_results(func) == 0 - check_attributes_on_entrypoint(func, expected_qubits=1, expected_results=0) + _assert_body_ops(module, [_rot_call(gate_name, 0.5, 0)]) - ir = str(module) - assert f"__quantum__qis__{gate_name}__body" in ir - # Check that the rotation angle is included - assert "0.5" in ir or "5.0" in ir or "double" in ir + @pytest.mark.parametrize("angle", [0.0, math.pi, 2 * math.pi, -math.pi / 4]) + def test_rotation_edge_angles(self, angle): + """Test rotation gates with edge-case angles.""" + circuit = QuantumCircuit(1) + circuit.rx(angle, 0) + + module = qiskit_to_qir(circuit) + body = _get_body(module) + gate_ops = [l for l in body if "qis__" in l] + assert len(gate_ops) == 1 + assert "qis__rx__body" in gate_ops[0] + + def test_rotation_sequence(self): + """Test a sequence of different rotation gates on the same qubit.""" + circuit = QuantumCircuit(1) + circuit.rx(0.5, 0) + circuit.ry(1.0, 0) + circuit.rz(1.5, 0) + + module = qiskit_to_qir(circuit) + _assert_body_ops( + module, + [ + _rot_call("rx", 0.5, 0), + _rot_call("ry", 1.0, 0), + _rot_call("rz", 1.5, 0), + ], + ) + + +# --------------------------------------------------------------------------- +# Two-qubit gates +# --------------------------------------------------------------------------- class TestTwoQubitGates: - """Tests for two qubit gate conversions.""" + """Tests for two qubit gate conversions with sequential equivalence.""" - @pytest.mark.parametrize("gate_name,qir_name", TWO_QUBIT_GATES.items()) + GATES = {"cx": "cnot", "cz": "cz", "swap": "swap"} + + @pytest.mark.parametrize("gate_name,qir_name", GATES.items()) def test_two_qubit_gate(self, gate_name, qir_name): - """Test conversion of two qubit gates.""" circuit = QuantumCircuit(2) getattr(circuit, gate_name)(0, 1) module = qiskit_to_qir(circuit) - func = get_entry_point(module) + func = _get_entry_point(module) + assert required_num_qubits(func) == 2 + assert required_num_results(func) == 0 - check_attributes_on_entrypoint(func, expected_qubits=2, expected_results=0) + _assert_body_ops(module, [_two_qubit_call(qir_name, 0, 1)]) + + @pytest.mark.parametrize("gate_name,qir_name", GATES.items()) + def test_two_qubit_gate_reversed(self, gate_name, qir_name): + """Test two-qubit gate with reversed qubit order.""" + circuit = QuantumCircuit(2) + getattr(circuit, gate_name)(1, 0) + + module = qiskit_to_qir(circuit) + _assert_body_ops(module, [_two_qubit_call(qir_name, 1, 0)]) + + def test_two_qubit_sequence(self): + """All two-qubit gates in sequence.""" + circuit = QuantumCircuit(2) + circuit.cx(0, 1) + circuit.cz(0, 1) + circuit.swap(0, 1) + + module = qiskit_to_qir(circuit) + _assert_body_ops( + module, + [ + _two_qubit_call("cnot", 0, 1), + _two_qubit_call("cz", 0, 1), + _two_qubit_call("swap", 0, 1), + ], + ) - ir = str(module) - assert f"__quantum__qis__{qir_name}__body" in ir + +# --------------------------------------------------------------------------- +# Three-qubit gates +# --------------------------------------------------------------------------- class TestThreeQubitGates: - """Tests for three qubit gate conversions.""" + """Tests for three qubit gate conversions with sequential equivalence.""" def test_ccx_gate(self): - """Test conversion of CCX (Toffoli) gate.""" circuit = QuantumCircuit(3) circuit.ccx(0, 1, 2) module = qiskit_to_qir(circuit) - func = get_entry_point(module) + func = _get_entry_point(module) + assert required_num_qubits(func) == 3 + assert required_num_results(func) == 0 - check_attributes_on_entrypoint(func, expected_qubits=3, expected_results=0) + _assert_body_ops(module, [_three_qubit_call("ccx", 0, 1, 2)]) - ir = str(module) - assert "__quantum__qis__ccx__body" in ir + def test_ccx_different_qubits(self): + """CCX with non-sequential qubit ordering.""" + circuit = QuantumCircuit(4) + circuit.ccx(3, 1, 0) + + module = qiskit_to_qir(circuit) + _assert_body_ops(module, [_three_qubit_call("ccx", 3, 1, 0)]) + + +# --------------------------------------------------------------------------- +# Measurement +# --------------------------------------------------------------------------- class TestMeasurement: - """Tests for measurement operations.""" + """Tests for measurement operations with sequential equivalence.""" def test_single_measurement(self): - """Test conversion of single qubit measurement.""" circuit = QuantumCircuit(1, 1) circuit.measure(0, 0) module = qiskit_to_qir(circuit) - func = get_entry_point(module) + func = _get_entry_point(module) + assert required_num_qubits(func) == 1 + assert required_num_results(func) == 1 - check_attributes_on_entrypoint(func, expected_qubits=1, expected_results=1) - - ir = str(module) - assert "__quantum__qis__mz__body" in ir + _assert_body_ops(module, [_mz_call(0, 0)]) def test_multiple_measurements(self): - """Test conversion of multiple measurements.""" circuit = QuantumCircuit(3, 3) circuit.measure([0, 1, 2], [0, 1, 2]) module = qiskit_to_qir(circuit) - func = get_entry_point(module) + func = _get_entry_point(module) + assert required_num_qubits(func) == 3 + assert required_num_results(func) == 3 + + _assert_body_ops( + module, + [ + _mz_call(0, 0), + _mz_call(1, 1), + _mz_call(2, 2), + ], + ) + + def test_gates_then_measurement(self): + """H gate then measurement — verify ordering.""" + circuit = QuantumCircuit(1, 1) + circuit.h(0) + circuit.measure(0, 0) + + module = qiskit_to_qir(circuit) + _assert_body_ops(module, [_op_call("h", 0), _mz_call(0, 0)]) - check_attributes_on_entrypoint(func, expected_qubits=3, expected_results=3) - ir = str(module) - # Count calls, not declarations - assert ir.count("call void @__quantum__qis__mz__body") == 3 +# --------------------------------------------------------------------------- +# Output recording +# --------------------------------------------------------------------------- class TestOutputRecording: """Tests for output recording functionality.""" def test_output_recording_single_register(self): - """Test that output recording is generated for single register.""" circuit = QuantumCircuit(2, 2) circuit.h(0) circuit.cx(0, 1) circuit.measure([0, 1], [0, 1]) module = qiskit_to_qir(circuit) - ir = str(module) + body = _get_body(module) - # Should have array_record_output call - assert "__quantum__rt__array_record_output" in ir - # Should have result_record_output calls - assert "__quantum__rt__result_record_output" in ir + rt_calls = [l for l in body if "__quantum__rt__" in l] + # init + array_record_output + 2 result_record_output + assert any("array_record_output" in l for l in rt_calls) + assert sum("result_record_output" in l for l in rt_calls) == 2 def test_output_recording_disabled(self): - """Test that output recording can be disabled.""" circuit = QuantumCircuit(2, 2) circuit.h(0) circuit.cx(0, 1) @@ -200,7 +379,140 @@ def test_output_recording_disabled(self): module = qiskit_to_qir(circuit, record_output=False) ir = str(module) - - # Should NOT have output recording calls assert "__quantum__rt__array_record_output" not in ir assert "__quantum__rt__result_record_output" not in ir + + +# --------------------------------------------------------------------------- +# Barrier and delay +# --------------------------------------------------------------------------- + + +class TestBarrierAndDelay: + """Tests for barrier and delay instruction handling.""" + + def test_barrier_not_emitted_by_default(self): + circuit = QuantumCircuit(2) + circuit.h(0) + circuit.barrier() + circuit.cx(0, 1) + + module = qiskit_to_qir(circuit) + _assert_body_ops( + module, + [ + _op_call("h", 0), + _two_qubit_call("cnot", 0, 1), + ], + ) + + def test_barrier_emitted_when_enabled(self): + circuit = QuantumCircuit(2) + circuit.h(0) + circuit.barrier() + circuit.cx(0, 1) + + module = qiskit_to_qir(circuit, emit_barrier_calls=True) + body = _get_body(module) + gate_and_barrier = [l for l in body if l.startswith("call void @__quantum__qis__")] + + assert len(gate_and_barrier) == 3 + assert "barrier" in gate_and_barrier[1] + + def test_delay_ignored(self): + circuit = QuantumCircuit(1) + circuit.h(0) + circuit.delay(100, 0, "ns") + circuit.x(0) + + module = qiskit_to_qir(circuit) + _assert_body_ops(module, [_op_call("h", 0), _op_call("x", 0)]) + + +# --------------------------------------------------------------------------- +# Multi-gate sequences (sequential equivalence) +# --------------------------------------------------------------------------- + + +class TestMultiGateSequences: + """Tests that verify exact gate ordering in multi-gate circuits.""" + + def test_bell_state_preparation(self): + """H then CX — canonical Bell state prep.""" + circuit = QuantumCircuit(2) + circuit.h(0) + circuit.cx(0, 1) + + module = qiskit_to_qir(circuit) + _assert_body_ops( + module, + [ + _op_call("h", 0), + _two_qubit_call("cnot", 0, 1), + ], + ) + + def test_all_single_qubit_gates_sequence(self): + """All single-qubit gates in a defined order.""" + circuit = QuantumCircuit(1) + circuit.h(0) + circuit.x(0) + circuit.y(0) + circuit.z(0) + circuit.s(0) + circuit.sdg(0) + circuit.t(0) + circuit.tdg(0) + + module = qiskit_to_qir(circuit) + _assert_body_ops( + module, + [ + _op_call("h", 0), + _op_call("x", 0), + _op_call("y", 0), + _op_call("z", 0), + _op_call("s", 0), + _adj_call("s", 0), + _op_call("t", 0), + _adj_call("t", 0), + ], + ) + + def test_mixed_single_and_two_qubit(self): + """Interleaved single and two-qubit gates.""" + circuit = QuantumCircuit(3) + circuit.h(0) + circuit.cx(0, 1) + circuit.h(2) + circuit.cz(1, 2) + + module = qiskit_to_qir(circuit) + _assert_body_ops( + module, + [ + _op_call("h", 0), + _two_qubit_call("cnot", 0, 1), + _op_call("h", 2), + _two_qubit_call("cz", 1, 2), + ], + ) + + def test_gates_interleaved_with_measurements(self): + """Gates and measurements interleaved.""" + circuit = QuantumCircuit(2, 2) + circuit.h(0) + circuit.measure(0, 0) + circuit.h(1) + circuit.measure(1, 1) + + module = qiskit_to_qir(circuit) + _assert_body_ops( + module, + [ + _op_call("h", 0), + _mz_call(0, 0), + _op_call("h", 1), + _mz_call(1, 1), + ], + ) diff --git a/tests/qiskit_qir/test_complex_circuits.py b/tests/qiskit_qir/test_complex_circuits.py new file mode 100644 index 00000000..faa54c7c --- /dev/null +++ b/tests/qiskit_qir/test_complex_circuits.py @@ -0,0 +1,465 @@ +# Copyright 2026 qBraid +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Tests for complex Qiskit circuits, transpilation support, and edge cases. + +""" + +import math + +import pytest +from pyqir import is_entry_point, required_num_qubits, required_num_results +from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister +from qiskit.circuit import Gate, Parameter +from qiskit.circuit.random import random_circuit + +from qbraid_qir.qiskit import qiskit_to_qir +from qbraid_qir.qiskit.exceptions import QiskitConversionError + + +def _get_body(module): + func = next(filter(is_entry_point, module.functions)) + lines = str(func).splitlines()[2:-1] + return [line.strip() for line in lines] + + +def _gate_ops(module): + """Return only qis__ gate call lines from the body.""" + return [l for l in _get_body(module) if "qis__" in l] + + +# --------------------------------------------------------------------------- +# Complex circuit tests +# --------------------------------------------------------------------------- + + +class TestQuantumTeleportation: + """Test quantum teleportation circuit.""" + + def test_teleportation_circuit(self): + qr = QuantumRegister(3, "q") + cr = ClassicalRegister(2, "c") + circuit = QuantumCircuit(qr, cr) + + # Create Bell pair between q1 and q2 + circuit.h(1) + circuit.cx(1, 2) + + # Alice's operations + circuit.cx(0, 1) + circuit.h(0) + + # Measure + circuit.measure(0, 0) + circuit.measure(1, 1) + + module = qiskit_to_qir(circuit) + func = next(filter(is_entry_point, module.functions)) + assert required_num_qubits(func) == 3 + assert required_num_results(func) == 2 + + ops = _gate_ops(module) + gate_names = [] + for op in ops: + if "qis__h__body" in op: + gate_names.append("h") + elif "qis__cnot__body" in op: + gate_names.append("cnot") + elif "qis__mz__body" in op: + gate_names.append("mz") + + # H, CX, CX, H, MZ, MZ + assert gate_names == ["h", "cnot", "cnot", "h", "mz", "mz"] + + +class TestGroverTwoQubit: + """Test 2-qubit Grover's algorithm circuit.""" + + def test_grover_circuit(self): + circuit = QuantumCircuit(2, 2) + + # Initialize superposition + circuit.h(0) + circuit.h(1) + + # Oracle (mark |11>) + circuit.cz(0, 1) + + # Diffusion operator + circuit.h(0) + circuit.h(1) + circuit.x(0) + circuit.x(1) + circuit.cz(0, 1) + circuit.x(0) + circuit.x(1) + circuit.h(0) + circuit.h(1) + + circuit.measure([0, 1], [0, 1]) + + module = qiskit_to_qir(circuit) + ops = _gate_ops(module) + assert len(ops) == 14 # 2H + CZ + 2H + 2X + CZ + 2X + 2H + 2MZ + + +class TestParameterizedCircuits: + """Test parameterized circuits with bound parameters.""" + + def test_bound_parameter(self): + theta = Parameter("theta") + circuit = QuantumCircuit(1) + circuit.rx(theta, 0) + bound = circuit.assign_parameters({theta: math.pi / 4}) + + module = qiskit_to_qir(bound) + ops = _gate_ops(module) + assert len(ops) == 1 + assert "qis__rx__body" in ops[0] + assert "double" in ops[0] + + def test_multiple_bound_parameters(self): + a = Parameter("a") + b = Parameter("b") + circuit = QuantumCircuit(1) + circuit.rx(a, 0) + circuit.ry(b, 0) + bound = circuit.assign_parameters({a: 0.5, b: 1.0}) + + module = qiskit_to_qir(bound) + ops = _gate_ops(module) + assert len(ops) == 2 + assert "qis__rx__body" in ops[0] + assert "qis__ry__body" in ops[1] + + +class TestCustomGates: + """Test custom (composite) gate decomposition.""" + + def test_simple_custom_gate(self): + """Custom gate that decomposes to H + CX.""" + sub = QuantumCircuit(2, name="my_bell") + sub.h(0) + sub.cx(0, 1) + bell_gate = sub.to_gate() + + circuit = QuantumCircuit(2) + circuit.append(bell_gate, [0, 1]) + + module = qiskit_to_qir(circuit) + ops = _gate_ops(module) + assert len(ops) == 2 + assert "qis__h__body" in ops[0] + assert "qis__cnot__body" in ops[1] + + def test_nested_custom_gate(self): + """Custom gate containing another custom gate.""" + inner = QuantumCircuit(1, name="inner_gate") + inner.h(0) + inner.z(0) + inner_gate = inner.to_gate() + + outer = QuantumCircuit(2, name="outer_gate") + outer.append(inner_gate, [0]) + outer.cx(0, 1) + outer_gate = outer.to_gate() + + circuit = QuantumCircuit(2) + circuit.append(outer_gate, [0, 1]) + + module = qiskit_to_qir(circuit) + ops = _gate_ops(module) + assert len(ops) == 3 + assert "qis__h__body" in ops[0] + assert "qis__z__body" in ops[1] + assert "qis__cnot__body" in ops[2] + + +class TestLargeCircuits: + """Test with larger circuits for regression.""" + + def test_10_qubit_circuit(self): + n = 10 + circuit = QuantumCircuit(n) + for i in range(n): + circuit.h(i) + for i in range(n - 1): + circuit.cx(i, i + 1) + + module = qiskit_to_qir(circuit) + func = next(filter(is_entry_point, module.functions)) + assert required_num_qubits(func) == n + + ops = _gate_ops(module) + h_count = sum(1 for l in ops if "qis__h__body" in l) + cx_count = sum(1 for l in ops if "qis__cnot__body" in l) + assert h_count == n + assert cx_count == n - 1 + + def test_20_qubit_ghz(self): + n = 20 + circuit = QuantumCircuit(n, n) + circuit.h(0) + for i in range(n - 1): + circuit.cx(i, i + 1) + circuit.measure(range(n), range(n)) + + module = qiskit_to_qir(circuit) + func = next(filter(is_entry_point, module.functions)) + assert required_num_qubits(func) == n + assert required_num_results(func) == n + + +class TestMultipleClassicalRegisters: + """Test circuits with multiple classical registers.""" + + def test_selective_measurement(self): + """Measure only some qubits into specific registers.""" + qr = QuantumRegister(3, "q") + cr1 = ClassicalRegister(1, "c1") + cr2 = ClassicalRegister(2, "c2") + circuit = QuantumCircuit(qr, cr1, cr2) + circuit.h(0) + circuit.h(1) + circuit.h(2) + circuit.measure(0, cr1[0]) + circuit.measure(1, cr2[0]) + circuit.measure(2, cr2[1]) + + module = qiskit_to_qir(circuit) + func = next(filter(is_entry_point, module.functions)) + assert required_num_qubits(func) == 3 + assert required_num_results(func) == 3 + + body = _get_body(module) + array_records = [l for l in body if "array_record_output" in l] + assert len(array_records) == 2 # Two classical registers + + def test_three_classical_registers(self): + qr = QuantumRegister(3) + cr1 = ClassicalRegister(1, "a") + cr2 = ClassicalRegister(1, "b") + cr3 = ClassicalRegister(1, "c") + circuit = QuantumCircuit(qr, cr1, cr2, cr3) + circuit.h(0) + circuit.h(1) + circuit.h(2) + circuit.measure(0, cr1[0]) + circuit.measure(1, cr2[0]) + circuit.measure(2, cr3[0]) + + module = qiskit_to_qir(circuit) + body = _get_body(module) + array_records = [l for l in body if "array_record_output" in l] + assert len(array_records) == 3 + + +# --------------------------------------------------------------------------- +# Transpilation tests +# --------------------------------------------------------------------------- + + +class TestTranspilation: + """Test the transpile=True flag for converting unsupported gates.""" + + def test_ecr_gate_with_transpile(self): + """ECR gate is not natively supported but can be transpiled.""" + circuit = QuantumCircuit(2) + circuit.ecr(0, 1) + + module = qiskit_to_qir(circuit, transpile=True) + ops = _gate_ops(module) + assert len(ops) > 0 + # Should not contain ECR in the output + assert not any("ecr" in l.lower() for l in ops) + + def test_u_gate_with_transpile(self): + """U gate transpiled to basis gates.""" + circuit = QuantumCircuit(1) + circuit.u(math.pi / 4, math.pi / 2, math.pi, 0) + + module = qiskit_to_qir(circuit, transpile=True) + ops = _gate_ops(module) + assert len(ops) > 0 + + def test_cswap_with_transpile(self): + """CSWAP (Fredkin) gate transpiled to basis gates.""" + circuit = QuantumCircuit(3) + circuit.cswap(0, 1, 2) + + module = qiskit_to_qir(circuit, transpile=True) + ops = _gate_ops(module) + assert len(ops) > 0 + + def test_transpile_preserves_simple_circuit(self): + """Transpiling a circuit that's already in basis gates shouldn't break it.""" + circuit = QuantumCircuit(2, 2) + circuit.h(0) + circuit.cx(0, 1) + circuit.measure([0, 1], [0, 1]) + + module_no_transpile = qiskit_to_qir(circuit, transpile=False) + module_transpile = qiskit_to_qir(circuit, transpile=True) + + ops_no = _gate_ops(module_no_transpile) + ops_yes = _gate_ops(module_transpile) + + # Both should have the same gate types + no_names = [l.split("qis__")[1].split("__")[0] for l in ops_no] + yes_names = [l.split("qis__")[1].split("__")[0] for l in ops_yes] + assert set(no_names) == set(yes_names) + + def test_ecr_without_transpile_uses_decomposition(self): + """ECR without transpile should still work via composite decomposition.""" + circuit = QuantumCircuit(2) + circuit.ecr(0, 1) + + # Should work because ECR has a definition (decomposition) + module = qiskit_to_qir(circuit, transpile=False) + ops = _gate_ops(module) + assert len(ops) > 0 + + def test_unsupported_gate_no_definition_without_transpile(self): + """Gate with no decomposition and transpile=False should raise.""" + + class BadGate(Gate): + def __init__(self): + super().__init__("bad_gate", 1, []) + + circuit = QuantumCircuit(1) + circuit.append(BadGate(), [0]) + + with pytest.raises(QiskitConversionError, match="not supported"): + qiskit_to_qir(circuit, transpile=False) + + def test_circuit_with_barrier_and_transpile(self): + """Barriers should be preserved through transpilation.""" + circuit = QuantumCircuit(2) + circuit.h(0) + circuit.barrier() + circuit.ecr(0, 1) + + module = qiskit_to_qir(circuit, transpile=True, emit_barrier_calls=True) + body = _get_body(module) + assert any("barrier" in l for l in body) + + +# --------------------------------------------------------------------------- +# Edge cases +# --------------------------------------------------------------------------- + + +class TestEdgeCases: + """Edge case tests.""" + + def test_single_gate_circuit(self): + """Minimal circuit with exactly one gate.""" + circuit = QuantumCircuit(1) + circuit.h(0) + module = qiskit_to_qir(circuit) + ops = _gate_ops(module) + assert len(ops) == 1 + + def test_measurement_only_circuit(self): + """Circuit with only measurements.""" + circuit = QuantumCircuit(1, 1) + circuit.measure(0, 0) + module = qiskit_to_qir(circuit) + ops = _gate_ops(module) + assert len(ops) == 1 + assert "mz" in ops[0] + + def test_reset_then_gate(self): + """Reset followed by a gate.""" + circuit = QuantumCircuit(1) + circuit.reset(0) + circuit.h(0) + module = qiskit_to_qir(circuit) + ops = _gate_ops(module) + assert len(ops) == 2 + assert "reset" in ops[0] + assert "h" in ops[1] + + def test_many_identity_gates(self): + """Multiple identity gates should all be no-ops.""" + circuit = QuantumCircuit(1) + circuit.id(0) + circuit.id(0) + circuit.id(0) + circuit.h(0) + module = qiskit_to_qir(circuit) + ops = _gate_ops(module) + assert len(ops) == 1 + assert "h" in ops[0] + + def test_all_runtime_options_disabled(self): + """No init, no output recording, no barrier emission.""" + circuit = QuantumCircuit(2, 2) + circuit.h(0) + circuit.barrier() + circuit.cx(0, 1) + circuit.measure([0, 1], [0, 1]) + + module = qiskit_to_qir( + circuit, initialize_runtime=False, record_output=False, emit_barrier_calls=False + ) + body = _get_body(module) + + assert not any("rt__initialize" in l for l in body) + assert not any("array_record_output" in l for l in body) + assert not any("result_record_output" in l for l in body) + assert not any("barrier" in l for l in body) + + def test_circuit_with_no_classical_bits(self): + """Circuit without classical bits should have no measurement output.""" + circuit = QuantumCircuit(2) + circuit.h(0) + circuit.cx(0, 1) + module = qiskit_to_qir(circuit) + func = next(filter(is_entry_point, module.functions)) + assert required_num_results(func) == 0 + + +# --------------------------------------------------------------------------- +# Random circuit tests +# --------------------------------------------------------------------------- + + +class TestRandomCircuits: + """Test conversion of randomly generated Qiskit circuits.""" + + @pytest.mark.parametrize("seed", range(10)) + def test_random_circuit_with_transpile(self, seed): + """Generate a random circuit and convert to QIR using transpile=True. + + Random circuits may contain gates outside the supported set, + so transpilation is used to decompose them first. + """ + num_qubits = (seed % 4) + 2 # 2..5 qubits + depth = (seed % 3) + 2 # 2..4 depth + + circuit = random_circuit(num_qubits, depth, seed=seed, measure=True) + + module = qiskit_to_qir(circuit, transpile=True) + + func = next(filter(is_entry_point, module.functions)) + assert required_num_qubits(func) == num_qubits + assert required_num_results(func) == num_qubits + + ops = _gate_ops(module) + assert len(ops) > 0 + + ir = str(module) + assert len(ir) > 0 diff --git a/tests/qiskit_qir/test_qiskit_to_qir.py b/tests/qiskit_qir/test_qiskit_to_qir.py index 71306092..670c0880 100644 --- a/tests/qiskit_qir/test_qiskit_to_qir.py +++ b/tests/qiskit_qir/test_qiskit_to_qir.py @@ -1,4 +1,4 @@ -# Copyright 2025 qBraid +# Copyright 2026 qBraid # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,194 +13,266 @@ # limitations under the License. """ -Unit tests for Qiskit to QIR conversion. +Unit tests for Qiskit to QIR conversion — integration, error handling, API contract. """ +import pyqir import pytest -from pyqir import Context, is_entry_point, qir_module, required_num_qubits, required_num_results -from qiskit import QuantumCircuit +from pyqir import ( + Context, + is_entry_point, + qir_module, + required_num_qubits, + required_num_results, +) +from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister +from qiskit.circuit import Gate from qbraid_qir.qiskit import BasicQiskitVisitor, QiskitModule, qiskit_to_qir +from qbraid_qir.qiskit.exceptions import QiskitConversionError +from qbraid_qir.qiskit.maps import map_qiskit_op_to_pyqir_callable + + +def _get_body(module): + func = next(filter(is_entry_point, module.functions)) + lines = str(func).splitlines()[2:-1] + return [line.strip() for line in lines] + + +def _get_entry_point(module): + return next(filter(is_entry_point, module.functions)) + + +# --------------------------------------------------------------------------- +# Integration tests using fixtures +# --------------------------------------------------------------------------- class TestQiskitToQir: - """Tests for the qiskit_to_qir function.""" + """Tests for the qiskit_to_qir function with sequential verification.""" def test_bell_circuit(self, bell_circuit): - """Test conversion of a Bell state circuit.""" module = qiskit_to_qir(bell_circuit) - assert module is not None - ir = str(module) - assert "__quantum__qis__h__body" in ir - assert "__quantum__qis__cnot__body" in ir - assert "__quantum__qis__mz__body" in ir + func = _get_entry_point(module) + assert required_num_qubits(func) == 2 + assert required_num_results(func) == 2 + + body = _get_body(module) + gate_ops = [l for l in body if "qis__" in l] + assert "qis__h__body" in gate_ops[0] + assert "qis__cnot__body" in gate_ops[1] + assert "qis__mz__body" in gate_ops[2] + assert "qis__mz__body" in gate_ops[3] def test_ghz_circuit(self, ghz_circuit): - """Test conversion of a GHZ state circuit.""" module = qiskit_to_qir(ghz_circuit) - assert module is not None - # Verify the entry point has correct attributes - func = next(filter(is_entry_point, module.functions)) + func = _get_entry_point(module) assert required_num_qubits(func) == 3 assert required_num_results(func) == 3 + body = _get_body(module) + gate_ops = [l for l in body if "qis__" in l] + # H, CX, CX, MZ, MZ, MZ + assert len(gate_ops) == 6 + assert "qis__h__body" in gate_ops[0] + assert "qis__cnot__body" in gate_ops[1] + assert "qis__cnot__body" in gate_ops[2] + def test_single_qubit_gates(self, single_qubit_gates_circuit): - """Test conversion of single-qubit gates.""" module = qiskit_to_qir(single_qubit_gates_circuit) - ir = str(module) - assert "__quantum__qis__h__body" in ir - assert "__quantum__qis__x__body" in ir - assert "__quantum__qis__y__body" in ir - assert "__quantum__qis__z__body" in ir - assert "__quantum__qis__s__body" in ir - assert "__quantum__qis__s__adj" in ir - assert "__quantum__qis__t__body" in ir - assert "__quantum__qis__t__adj" in ir + body = _get_body(module) + gate_ops = [l for l in body if "qis__" in l] + + expected_gates = ["h", "x", "y", "z", "s", "s__adj", "t", "t__adj"] + assert len(gate_ops) == len(expected_gates) + for op, gate in zip(gate_ops, expected_gates): + assert f"qis__{gate}" in op def test_rotation_gates(self, rotation_gates_circuit): - """Test conversion of rotation gates.""" module = qiskit_to_qir(rotation_gates_circuit) - ir = str(module) - assert "__quantum__qis__rx__body" in ir - assert "__quantum__qis__ry__body" in ir - assert "__quantum__qis__rz__body" in ir + body = _get_body(module) + gate_ops = [l for l in body if "qis__" in l] + assert len(gate_ops) == 3 + assert "qis__rx__body" in gate_ops[0] + assert "qis__ry__body" in gate_ops[1] + assert "qis__rz__body" in gate_ops[2] def test_two_qubit_gates(self, two_qubit_gates_circuit): - """Test conversion of two-qubit gates.""" module = qiskit_to_qir(two_qubit_gates_circuit) - ir = str(module) - assert "__quantum__qis__cnot__body" in ir - assert "__quantum__qis__cz__body" in ir - assert "__quantum__qis__swap__body" in ir + body = _get_body(module) + gate_ops = [l for l in body if "qis__" in l] + assert len(gate_ops) == 3 + assert "qis__cnot__body" in gate_ops[0] + assert "qis__cz__body" in gate_ops[1] + assert "qis__swap__body" in gate_ops[2] def test_three_qubit_gates(self, three_qubit_gates_circuit): - """Test conversion of three-qubit gates (CCX/Toffoli).""" module = qiskit_to_qir(three_qubit_gates_circuit) - ir = str(module) - assert "__quantum__qis__ccx__body" in ir + body = _get_body(module) + gate_ops = [l for l in body if "qis__" in l] + assert len(gate_ops) == 1 + assert "qis__ccx__body" in gate_ops[0] def test_reset_gate(self, reset_circuit): - """Test conversion of reset gate.""" module = qiskit_to_qir(reset_circuit) - ir = str(module) - assert "__quantum__qis__reset__body" in ir + body = _get_body(module) + gate_ops = [l for l in body if "qis__" in l] + assert len(gate_ops) == 2 + assert "qis__reset__body" in gate_ops[0] + assert "qis__h__body" in gate_ops[1] def test_identity_gate(self, identity_circuit): - """Test conversion of identity gate (implemented as X.X).""" + """Identity gate is now a true no-op.""" module = qiskit_to_qir(identity_circuit) - ir = str(module) - # Identity is implemented as two X gates (count calls, not declarations) - assert ir.count("call void @__quantum__qis__x__body") == 2 + body = _get_body(module) + gate_ops = [l for l in body if "qis__" in l] + assert len(gate_ops) == 0 def test_barrier_not_emitted_by_default(self, barrier_circuit): - """Test that barrier is not emitted by default.""" module = qiskit_to_qir(barrier_circuit) - ir = str(module) - assert "__quantum__qis__barrier__body" not in ir + body = _get_body(module) + gate_ops = [l for l in body if "qis__" in l] + assert not any("barrier" in l for l in gate_ops) def test_barrier_emitted_when_enabled(self, barrier_circuit): - """Test that barrier is emitted when emit_barrier_calls=True.""" module = qiskit_to_qir(barrier_circuit, emit_barrier_calls=True) - ir = str(module) - assert "__quantum__qis__barrier__body" in ir + body = _get_body(module) + gate_ops = [l for l in body if "qis__" in l] + assert any("barrier" in l for l in gate_ops) def test_delay_ignored(self, delay_circuit): - """Test that delay instruction is ignored (no-op).""" module = qiskit_to_qir(delay_circuit) ir = str(module) - # Delay should not produce any output assert "delay" not in ir.lower() - # But the other gates should be present - assert "__quantum__qis__h__body" in ir - assert "__quantum__qis__x__body" in ir + body = _get_body(module) + gate_ops = [l for l in body if "qis__" in l] + assert len(gate_ops) == 2 + assert "qis__h__body" in gate_ops[0] + assert "qis__x__body" in gate_ops[1] def test_named_registers(self, named_registers_circuit): - """Test conversion with named quantum and classical registers.""" module = qiskit_to_qir(named_registers_circuit, name="test_named") - assert module is not None ir = str(module) - assert "test_named" in ir or "@test_named" in ir + assert "test_named" in ir def test_multiple_registers(self, multiple_registers_circuit): - """Test conversion with multiple quantum and classical registers.""" module = qiskit_to_qir(multiple_registers_circuit) - func = next(filter(is_entry_point, module.functions)) - # Total: 2 + 1 = 3 qubits, 2 + 1 = 3 classical bits + func = _get_entry_point(module) assert required_num_qubits(func) == 3 assert required_num_results(func) == 3 + body = _get_body(module) + # Check output recording has two array_record_output calls (2 classical registers) + array_records = [l for l in body if "array_record_output" in l] + assert len(array_records) == 2 + def test_composite_gate(self, composite_gate_circuit): - """Test conversion of composite (custom) gates.""" + """Composite gate should be decomposed into primitive gates.""" module = qiskit_to_qir(composite_gate_circuit) - ir = str(module) - # The composite gate should be decomposed - assert "__quantum__qis__h__body" in ir - assert "__quantum__qis__cnot__body" in ir - assert "__quantum__qis__mz__body" in ir + body = _get_body(module) + gate_ops = [l for l in body if "qis__" in l] + + # bell_prep decomposes to H + CX, then measure + gate_names = [] + for op in gate_ops: + if "qis__h__body" in op: + gate_names.append("h") + elif "qis__cnot__body" in op: + gate_names.append("cnot") + elif "qis__mz__body" in op: + gate_names.append("mz") + + assert "h" in gate_names + assert "cnot" in gate_names + assert "mz" in gate_names def test_custom_name(self, bell_circuit): - """Test that custom module name is used.""" module = qiskit_to_qir(bell_circuit, name="custom_bell") ir = str(module) - assert "custom_bell" in ir or "@custom_bell" in ir + assert "custom_bell" in ir def test_no_runtime_init(self, bell_circuit): - """Test conversion without runtime initialization.""" module = qiskit_to_qir(bell_circuit, initialize_runtime=False) ir = str(module) assert "__quantum__rt__initialize" not in ir def test_no_output_recording(self, bell_circuit): - """Test conversion without output recording.""" module = qiskit_to_qir(bell_circuit, record_output=False) ir = str(module) assert "__quantum__rt__result_record_output" not in ir assert "__quantum__rt__array_record_output" not in ir +# --------------------------------------------------------------------------- +# Error handling +# --------------------------------------------------------------------------- + + class TestQiskitToQirErrors: """Tests for error handling in qiskit_to_qir.""" def test_invalid_input_type(self): - """Test that TypeError is raised for non-QuantumCircuit input.""" with pytest.raises(TypeError, match="must be of type qiskit.QuantumCircuit"): qiskit_to_qir("not a circuit") def test_empty_circuit(self): - """Test that ValueError is raised for empty circuit.""" circuit = QuantumCircuit(1) with pytest.raises(ValueError, match="at least one operation"): qiskit_to_qir(circuit) - @pytest.mark.skip(reason="Most gates in qiskit 2.x have decompositions to base gates") - def test_unsupported_gate(self): - """Test that QiskitConversionError is raised for unsupported gates.""" - # In qiskit 2.x, most gates have decompositions, so it's hard to find - # a gate that will truly fail. This test is skipped for now. + def test_unsupported_gate_no_definition(self): + """Gate with no decomposition should raise QiskitConversionError.""" + + class NoDefGate(Gate): + def __init__(self): + super().__init__("no_def_gate", 1, []) + + circuit = QuantumCircuit(1) + circuit.append(NoDefGate(), [0]) + with pytest.raises(QiskitConversionError, match="not supported"): + qiskit_to_qir(circuit) + + +# --------------------------------------------------------------------------- +# QiskitModule +# --------------------------------------------------------------------------- class TestQiskitModule: """Tests for QiskitModule class.""" def test_from_circuit(self, bell_circuit): - """Test QiskitModule.from_circuit factory method.""" module = QiskitModule.from_circuit(bell_circuit, None) assert module.num_qubits == 2 assert module.num_clbits == 2 assert module.name in (bell_circuit.name, "main") def test_circuit_property(self, bell_circuit): - """Test that circuit property returns the original circuit.""" module = QiskitModule.from_circuit(bell_circuit, None) assert module.circuit is bell_circuit + def test_reg_sizes(self): + """Test reg_sizes for multiple classical registers.""" + qr = QuantumRegister(3) + cr1 = ClassicalRegister(2) + cr2 = ClassicalRegister(1) + circuit = QuantumCircuit(qr, cr1, cr2) + circuit.h(0) + + module = QiskitModule.from_circuit(circuit, None) + assert module.reg_sizes == [2, 1] + + +# --------------------------------------------------------------------------- +# BasicQiskitVisitor +# --------------------------------------------------------------------------- + class TestBasicQiskitVisitor: """Tests for BasicQiskitVisitor class.""" def test_visitor_entry_point(self, bell_circuit): - """Test that visitor generates correct entry point.""" llvm_module = qir_module(Context(), "test") qiskit_module = QiskitModule.from_circuit(bell_circuit, llvm_module) @@ -211,7 +283,6 @@ def test_visitor_entry_point(self, bell_circuit): assert len(visitor.entry_point) > 0 def test_visitor_ir_output(self, bell_circuit): - """Test that visitor can generate IR string.""" llvm_module = qir_module(Context(), "test") qiskit_module = QiskitModule.from_circuit(bell_circuit, llvm_module) @@ -219,6 +290,74 @@ def test_visitor_ir_output(self, bell_circuit): qiskit_module.accept(visitor) ir = visitor.ir() - assert ir is not None assert len(ir) > 0 assert "__quantum__qis__h__body" in ir + + def test_visitor_bitcode_output(self, bell_circuit): + llvm_module = qir_module(Context(), "test") + qiskit_module = QiskitModule.from_circuit(bell_circuit, llvm_module) + + visitor = BasicQiskitVisitor() + qiskit_module.accept(visitor) + + bitcode = visitor.bitcode() + assert isinstance(bitcode, bytes) + assert len(bitcode) > 0 + + +# --------------------------------------------------------------------------- +# SDK API contract +# --------------------------------------------------------------------------- + + +class TestMaps: + """Tests for the maps module utility function.""" + + def test_map_one_qubit_op(self): + func, num_qubits = map_qiskit_op_to_pyqir_callable("h") + assert num_qubits == 1 + assert func is not None + + def test_map_rotation_op(self): + func, num_qubits = map_qiskit_op_to_pyqir_callable("rx") + assert num_qubits == 1 + assert func is not None + + def test_map_two_qubit_op(self): + func, num_qubits = map_qiskit_op_to_pyqir_callable("cx") + assert num_qubits == 2 + assert func is not None + + def test_map_three_qubit_op(self): + func, num_qubits = map_qiskit_op_to_pyqir_callable("ccx") + assert num_qubits == 3 + assert func is not None + + def test_map_unsupported_op(self): + with pytest.raises(QiskitConversionError, match="Unsupported"): + map_qiskit_op_to_pyqir_callable("nonexistent_gate") + + def test_map_case_insensitive(self): + fn1, n1 = map_qiskit_op_to_pyqir_callable("H") + fn2, n2 = map_qiskit_op_to_pyqir_callable("h") + assert fn1 == fn2 + assert n1 == n2 + + +class TestSdkApiContract: + """Verify the API matches what qBraid SDK PR #1132 expects.""" + + def test_qiskit_to_qir_returns_module(self): + circuit = QuantumCircuit(2, 2) + circuit.h(0) + circuit.cx(0, 1) + circuit.measure([0, 1], [0, 1]) + module = qiskit_to_qir(circuit) + assert isinstance(module, pyqir.Module) + + def test_function_accepts_circuit_only(self): + """SDK calls qiskit_to_qir(circuit) with no extra args.""" + circuit = QuantumCircuit(1) + circuit.h(0) + module = qiskit_to_qir(circuit) + assert module is not None From ff5d4e296aa87e06d5a6bb50ea529d174f5913f4 Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Tue, 19 May 2026 13:03:15 +0530 Subject: [PATCH 6/9] fix: tests to be compatible with pyqir 0.11.x --- tests/qiskit_qir/test_basic_gates.py | 40 +++++++++++++++++----------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/tests/qiskit_qir/test_basic_gates.py b/tests/qiskit_qir/test_basic_gates.py index df45d878..3f0424f0 100644 --- a/tests/qiskit_qir/test_basic_gates.py +++ b/tests/qiskit_qir/test_basic_gates.py @@ -23,8 +23,11 @@ from pyqir import is_entry_point, required_num_qubits, required_num_results from qiskit import QuantumCircuit +from qbraid_qir._pyqir_compat import pyqir_uses_opaque_pointers from qbraid_qir.qiskit import qiskit_to_qir +_OPAQUE = pyqir_uses_opaque_pointers() + def _get_body(module): """Extract the entry point body as a list of stripped instruction lines.""" @@ -38,44 +41,49 @@ def _get_entry_point(module): return next(filter(is_entry_point, module.functions)) +def _qubit_ref(n): + """Return the QIR qubit reference string for qubit index *n*.""" + if _OPAQUE: + return "ptr null" if n == 0 else f"ptr inttoptr (i64 {n} to ptr)" + return "%Qubit* null" if n == 0 else f"%Qubit* inttoptr (i64 {n} to %Qubit*)" + + +def _result_ref(n): + """Return the QIR result reference string for result index *n*.""" + if _OPAQUE: + return "ptr null" if n == 0 else f"ptr inttoptr (i64 {n} to ptr)" + return "%Result* null" if n == 0 else f"%Result* inttoptr (i64 {n} to %Result*)" + + def _op_call(name, qubit=0): """Generate expected single-qubit gate call string.""" - qb = "ptr null" if qubit == 0 else f"ptr inttoptr (i64 {qubit} to ptr)" - return f"call void @__quantum__qis__{name}__body({qb})" + return f"call void @__quantum__qis__{name}__body({_qubit_ref(qubit)})" def _adj_call(name, qubit=0): """Generate expected adjoint gate call string.""" - qb = "ptr null" if qubit == 0 else f"ptr inttoptr (i64 {qubit} to ptr)" - return f"call void @__quantum__qis__{name}__adj({qb})" + return f"call void @__quantum__qis__{name}__adj({_qubit_ref(qubit)})" def _rot_call(name, angle, qubit=0): """Generate expected rotation gate call string.""" - qb = "ptr null" if qubit == 0 else f"ptr inttoptr (i64 {qubit} to ptr)" - return f"call void @__quantum__qis__{name}__body(double {angle:#e}, {qb})" + return f"call void @__quantum__qis__{name}__body(double {angle:#e}, {_qubit_ref(qubit)})" def _two_qubit_call(name, qb1=0, qb2=1): """Generate expected two-qubit gate call string.""" - q1 = "ptr null" if qb1 == 0 else f"ptr inttoptr (i64 {qb1} to ptr)" - q2 = "ptr null" if qb2 == 0 else f"ptr inttoptr (i64 {qb2} to ptr)" - return f"call void @__quantum__qis__{name}__body({q1}, {q2})" + return f"call void @__quantum__qis__{name}__body({_qubit_ref(qb1)}, {_qubit_ref(qb2)})" def _three_qubit_call(name, qb1=0, qb2=1, qb3=2): """Generate expected three-qubit gate call string.""" - qubits = [] - for q in [qb1, qb2, qb3]: - qubits.append("ptr null" if q == 0 else f"ptr inttoptr (i64 {q} to ptr)") - return f"call void @__quantum__qis__{name}__body({', '.join(qubits)})" + qubits = ", ".join(_qubit_ref(q) for q in [qb1, qb2, qb3]) + return f"call void @__quantum__qis__{name}__body({qubits})" def _mz_call(qubit=0, result=0): """Generate expected measurement call string.""" - qb = "ptr null" if qubit == 0 else f"ptr inttoptr (i64 {qubit} to ptr)" - res = "ptr null" if result == 0 else f"ptr inttoptr (i64 {result} to ptr)" - return f"call void @__quantum__qis__mz__body({qb}, {res})" + return f"call void @__quantum__qis__mz__body({_qubit_ref(qubit)}, {_result_ref(result)})" def _assert_body_ops(module, expected_ops): From d3072214d33e01154621ac827fc2e367dd1f630a Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Tue, 19 May 2026 13:29:56 +0530 Subject: [PATCH 7/9] fix: address CodeRabbit review comments on PR #271 - Fix CHANGELOG signature to show name=None default - Fix _lazy dict values to be tuples so __dir__() doesn't iterate chars - Override from_element_list in _Instruction to raise NotImplementedError - Replace assert with ValueError in visitor.visit_qiskit_module - Add strict=True to zip(qubits, results) in measurement dispatch - Rename ambiguous loop variable l to line in test files (E741) Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 2 +- qbraid_qir/__init__.py | 2 +- qbraid_qir/qiskit/elements.py | 6 +++++ qbraid_qir/qiskit/visitor.py | 5 ++-- tests/qiskit_qir/test_basic_gates.py | 12 ++++----- tests/qiskit_qir/test_complex_circuits.py | 26 ++++++++++---------- tests/qiskit_qir/test_qiskit_to_qir.py | 30 +++++++++++------------ 7 files changed, 45 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3bc3603..94129eac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ Types of changes: ### ➕ New Features -- Added `qbraid_qir.qiskit` module for Qiskit to QIR conversion, ported from the archived [microsoft/qiskit-qir](https://github.com/microsoft/qiskit-qir) repository (MIT License). The module has been updated for compatibility with Qiskit 2.x and follows qbraid-qir conventions. Main entry point is `qiskit_to_qir(circuit, name, **kwargs)` which converts a Qiskit `QuantumCircuit` to a PyQIR `Module`. ([#272](https://github.com/qBraid/qbraid-qir/issues/272), [#271](https://github.com/qBraid/qbraid-qir/pull/271)) +- Added `qbraid_qir.qiskit` module for Qiskit to QIR conversion, ported from the archived [microsoft/qiskit-qir](https://github.com/microsoft/qiskit-qir) repository (MIT License). The module has been updated for compatibility with Qiskit 2.x and follows qbraid-qir conventions. Main entry point is `qiskit_to_qir(circuit, name=None, **kwargs)` which converts a Qiskit `QuantumCircuit` to a PyQIR `Module`. ([#272](https://github.com/qBraid/qbraid-qir/issues/272), [#271](https://github.com/qBraid/qbraid-qir/pull/271)) ### 🌟 Improvements diff --git a/qbraid_qir/__init__.py b/qbraid_qir/__init__.py index 48cb7d21..c0e6ad69 100644 --- a/qbraid_qir/__init__.py +++ b/qbraid_qir/__init__.py @@ -54,7 +54,7 @@ "qiskit_to_qir", ] -_lazy = {"cirq": "cirq_to_qir", "qasm3": "qasm3_to_qir", "qiskit": "qiskit_to_qir"} +_lazy = {"cirq": ("cirq_to_qir",), "qasm3": ("qasm3_to_qir",), "qiskit": ("qiskit_to_qir",)} if TYPE_CHECKING: from .cirq import cirq_to_qir diff --git a/qbraid_qir/qiskit/elements.py b/qbraid_qir/qiskit/elements.py index 518dce96..4eab0be2 100644 --- a/qbraid_qir/qiskit/elements.py +++ b/qbraid_qir/qiskit/elements.py @@ -62,6 +62,12 @@ def accept(self, visitor): class _Instruction(_QuantumCircuitElement): """Wrapper for a Qiskit instruction element.""" + @classmethod + def from_element_list(cls, elements): + raise NotImplementedError( + "_Instruction requires (instruction, qargs, cargs); use direct construction." + ) + def __init__( self, instruction: "Instruction", diff --git a/qbraid_qir/qiskit/visitor.py b/qbraid_qir/qiskit/visitor.py index f4804215..89e9c38d 100644 --- a/qbraid_qir/qiskit/visitor.py +++ b/qbraid_qir/qiskit/visitor.py @@ -113,7 +113,8 @@ def visit_qiskit_module(self, module: QiskitModule) -> None: module.num_qubits, module.num_clbits, ) - assert module.module is not None, "QiskitModule must have a PyQIR module set" + if module.module is None: + raise ValueError("QiskitModule must have a PyQIR module set before visiting.") self._module = module.module self._qiskit_module = module context = self._module.context @@ -253,7 +254,7 @@ def visit_instruction( return if op_name in PYQIR_MEASUREMENT_OP_MAP: - for qubit, result in zip(qubits, results): + for qubit, result in zip(qubits, results, strict=True): qubit_id = pointer_id(qubit) if qubit_id is not None: self._measured_qubits[qubit_id] = True diff --git a/tests/qiskit_qir/test_basic_gates.py b/tests/qiskit_qir/test_basic_gates.py index 3f0424f0..cf47c7cc 100644 --- a/tests/qiskit_qir/test_basic_gates.py +++ b/tests/qiskit_qir/test_basic_gates.py @@ -96,7 +96,7 @@ def _assert_body_ops(module, expected_ops): body = _get_body(module) # Filter to only "call" lines and "ret" for comparison - gate_lines = [l for l in body if l.startswith("call void @__quantum__qis__")] + gate_lines = [line for line in body if line.startswith("call void @__quantum__qis__")] expected_str = "\n ".join(expected_ops) actual_str = "\n ".join(gate_lines) @@ -211,7 +211,7 @@ def test_rotation_edge_angles(self, angle): module = qiskit_to_qir(circuit) body = _get_body(module) - gate_ops = [l for l in body if "qis__" in l] + gate_ops = [line for line in body if "qis__" in line] assert len(gate_ops) == 1 assert "qis__rx__body" in gate_ops[0] @@ -374,10 +374,10 @@ def test_output_recording_single_register(self): module = qiskit_to_qir(circuit) body = _get_body(module) - rt_calls = [l for l in body if "__quantum__rt__" in l] + rt_calls = [line for line in body if "__quantum__rt__" in line] # init + array_record_output + 2 result_record_output - assert any("array_record_output" in l for l in rt_calls) - assert sum("result_record_output" in l for l in rt_calls) == 2 + assert any("array_record_output" in line for line in rt_calls) + assert sum("result_record_output" in line for line in rt_calls) == 2 def test_output_recording_disabled(self): circuit = QuantumCircuit(2, 2) @@ -422,7 +422,7 @@ def test_barrier_emitted_when_enabled(self): module = qiskit_to_qir(circuit, emit_barrier_calls=True) body = _get_body(module) - gate_and_barrier = [l for l in body if l.startswith("call void @__quantum__qis__")] + gate_and_barrier = [line for line in body if line.startswith("call void @__quantum__qis__")] assert len(gate_and_barrier) == 3 assert "barrier" in gate_and_barrier[1] diff --git a/tests/qiskit_qir/test_complex_circuits.py b/tests/qiskit_qir/test_complex_circuits.py index faa54c7c..2ad6f209 100644 --- a/tests/qiskit_qir/test_complex_circuits.py +++ b/tests/qiskit_qir/test_complex_circuits.py @@ -37,7 +37,7 @@ def _get_body(module): def _gate_ops(module): """Return only qis__ gate call lines from the body.""" - return [l for l in _get_body(module) if "qis__" in l] + return [line for line in _get_body(module) if "qis__" in line] # --------------------------------------------------------------------------- @@ -203,8 +203,8 @@ def test_10_qubit_circuit(self): assert required_num_qubits(func) == n ops = _gate_ops(module) - h_count = sum(1 for l in ops if "qis__h__body" in l) - cx_count = sum(1 for l in ops if "qis__cnot__body" in l) + h_count = sum(1 for line in ops if "qis__h__body" in line) + cx_count = sum(1 for line in ops if "qis__cnot__body" in line) assert h_count == n assert cx_count == n - 1 @@ -244,7 +244,7 @@ def test_selective_measurement(self): assert required_num_results(func) == 3 body = _get_body(module) - array_records = [l for l in body if "array_record_output" in l] + array_records = [line for line in body if "array_record_output" in line] assert len(array_records) == 2 # Two classical registers def test_three_classical_registers(self): @@ -262,7 +262,7 @@ def test_three_classical_registers(self): module = qiskit_to_qir(circuit) body = _get_body(module) - array_records = [l for l in body if "array_record_output" in l] + array_records = [line for line in body if "array_record_output" in line] assert len(array_records) == 3 @@ -283,7 +283,7 @@ def test_ecr_gate_with_transpile(self): ops = _gate_ops(module) assert len(ops) > 0 # Should not contain ECR in the output - assert not any("ecr" in l.lower() for l in ops) + assert not any("ecr" in line.lower() for line in ops) def test_u_gate_with_transpile(self): """U gate transpiled to basis gates.""" @@ -317,8 +317,8 @@ def test_transpile_preserves_simple_circuit(self): ops_yes = _gate_ops(module_transpile) # Both should have the same gate types - no_names = [l.split("qis__")[1].split("__")[0] for l in ops_no] - yes_names = [l.split("qis__")[1].split("__")[0] for l in ops_yes] + no_names = [line.split("qis__")[1].split("__")[0] for line in ops_no] + yes_names = [line.split("qis__")[1].split("__")[0] for line in ops_yes] assert set(no_names) == set(yes_names) def test_ecr_without_transpile_uses_decomposition(self): @@ -353,7 +353,7 @@ def test_circuit_with_barrier_and_transpile(self): module = qiskit_to_qir(circuit, transpile=True, emit_barrier_calls=True) body = _get_body(module) - assert any("barrier" in l for l in body) + assert any("barrier" in line for line in body) # --------------------------------------------------------------------------- @@ -417,10 +417,10 @@ def test_all_runtime_options_disabled(self): ) body = _get_body(module) - assert not any("rt__initialize" in l for l in body) - assert not any("array_record_output" in l for l in body) - assert not any("result_record_output" in l for l in body) - assert not any("barrier" in l for l in body) + assert not any("rt__initialize" in line for line in body) + assert not any("array_record_output" in line for line in body) + assert not any("result_record_output" in line for line in body) + assert not any("barrier" in line for line in body) def test_circuit_with_no_classical_bits(self): """Circuit without classical bits should have no measurement output.""" diff --git a/tests/qiskit_qir/test_qiskit_to_qir.py b/tests/qiskit_qir/test_qiskit_to_qir.py index 670c0880..e6eb98c7 100644 --- a/tests/qiskit_qir/test_qiskit_to_qir.py +++ b/tests/qiskit_qir/test_qiskit_to_qir.py @@ -59,7 +59,7 @@ def test_bell_circuit(self, bell_circuit): assert required_num_results(func) == 2 body = _get_body(module) - gate_ops = [l for l in body if "qis__" in l] + gate_ops = [line for line in body if "qis__" in line] assert "qis__h__body" in gate_ops[0] assert "qis__cnot__body" in gate_ops[1] assert "qis__mz__body" in gate_ops[2] @@ -72,7 +72,7 @@ def test_ghz_circuit(self, ghz_circuit): assert required_num_results(func) == 3 body = _get_body(module) - gate_ops = [l for l in body if "qis__" in l] + gate_ops = [line for line in body if "qis__" in line] # H, CX, CX, MZ, MZ, MZ assert len(gate_ops) == 6 assert "qis__h__body" in gate_ops[0] @@ -82,7 +82,7 @@ def test_ghz_circuit(self, ghz_circuit): def test_single_qubit_gates(self, single_qubit_gates_circuit): module = qiskit_to_qir(single_qubit_gates_circuit) body = _get_body(module) - gate_ops = [l for l in body if "qis__" in l] + gate_ops = [line for line in body if "qis__" in line] expected_gates = ["h", "x", "y", "z", "s", "s__adj", "t", "t__adj"] assert len(gate_ops) == len(expected_gates) @@ -92,7 +92,7 @@ def test_single_qubit_gates(self, single_qubit_gates_circuit): def test_rotation_gates(self, rotation_gates_circuit): module = qiskit_to_qir(rotation_gates_circuit) body = _get_body(module) - gate_ops = [l for l in body if "qis__" in l] + gate_ops = [line for line in body if "qis__" in line] assert len(gate_ops) == 3 assert "qis__rx__body" in gate_ops[0] assert "qis__ry__body" in gate_ops[1] @@ -101,7 +101,7 @@ def test_rotation_gates(self, rotation_gates_circuit): def test_two_qubit_gates(self, two_qubit_gates_circuit): module = qiskit_to_qir(two_qubit_gates_circuit) body = _get_body(module) - gate_ops = [l for l in body if "qis__" in l] + gate_ops = [line for line in body if "qis__" in line] assert len(gate_ops) == 3 assert "qis__cnot__body" in gate_ops[0] assert "qis__cz__body" in gate_ops[1] @@ -110,14 +110,14 @@ def test_two_qubit_gates(self, two_qubit_gates_circuit): def test_three_qubit_gates(self, three_qubit_gates_circuit): module = qiskit_to_qir(three_qubit_gates_circuit) body = _get_body(module) - gate_ops = [l for l in body if "qis__" in l] + gate_ops = [line for line in body if "qis__" in line] assert len(gate_ops) == 1 assert "qis__ccx__body" in gate_ops[0] def test_reset_gate(self, reset_circuit): module = qiskit_to_qir(reset_circuit) body = _get_body(module) - gate_ops = [l for l in body if "qis__" in l] + gate_ops = [line for line in body if "qis__" in line] assert len(gate_ops) == 2 assert "qis__reset__body" in gate_ops[0] assert "qis__h__body" in gate_ops[1] @@ -126,27 +126,27 @@ def test_identity_gate(self, identity_circuit): """Identity gate is now a true no-op.""" module = qiskit_to_qir(identity_circuit) body = _get_body(module) - gate_ops = [l for l in body if "qis__" in l] + gate_ops = [line for line in body if "qis__" in line] assert len(gate_ops) == 0 def test_barrier_not_emitted_by_default(self, barrier_circuit): module = qiskit_to_qir(barrier_circuit) body = _get_body(module) - gate_ops = [l for l in body if "qis__" in l] - assert not any("barrier" in l for l in gate_ops) + gate_ops = [line for line in body if "qis__" in line] + assert not any("barrier" in line for line in gate_ops) def test_barrier_emitted_when_enabled(self, barrier_circuit): module = qiskit_to_qir(barrier_circuit, emit_barrier_calls=True) body = _get_body(module) - gate_ops = [l for l in body if "qis__" in l] - assert any("barrier" in l for l in gate_ops) + gate_ops = [line for line in body if "qis__" in line] + assert any("barrier" in line for line in gate_ops) def test_delay_ignored(self, delay_circuit): module = qiskit_to_qir(delay_circuit) ir = str(module) assert "delay" not in ir.lower() body = _get_body(module) - gate_ops = [l for l in body if "qis__" in l] + gate_ops = [line for line in body if "qis__" in line] assert len(gate_ops) == 2 assert "qis__h__body" in gate_ops[0] assert "qis__x__body" in gate_ops[1] @@ -164,14 +164,14 @@ def test_multiple_registers(self, multiple_registers_circuit): body = _get_body(module) # Check output recording has two array_record_output calls (2 classical registers) - array_records = [l for l in body if "array_record_output" in l] + array_records = [line for line in body if "array_record_output" in line] assert len(array_records) == 2 def test_composite_gate(self, composite_gate_circuit): """Composite gate should be decomposed into primitive gates.""" module = qiskit_to_qir(composite_gate_circuit) body = _get_body(module) - gate_ops = [l for l in body if "qis__" in l] + gate_ops = [line for line in body if "qis__" in line] # bell_prep decomposes to H + CX, then measure gate_names = [] From ed3fd3791f7b1966e4df2e29efb11e4d8f50d9f6 Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Tue, 19 May 2026 18:08:54 +0530 Subject: [PATCH 8/9] fix: address remaining CodeRabbit review comments (round 2) - Guard visitor public methods against uninitialized state with RuntimeError - Reset visitor label/measurement state at start of each module visit - Tighten bound-angle assertion to check concrete pi/4 value - Use sorted() instead of set() to preserve gate count multiplicity in test Co-Authored-By: Claude Opus 4.6 --- qbraid_qir/qiskit/visitor.py | 19 +++++++++++++++++-- tests/qiskit_qir/test_complex_circuits.py | 5 +++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/qbraid_qir/qiskit/visitor.py b/qbraid_qir/qiskit/visitor.py index 89e9c38d..8a1afd4e 100644 --- a/qbraid_qir/qiskit/visitor.py +++ b/qbraid_qir/qiskit/visitor.py @@ -90,9 +90,9 @@ def __init__( record_output: bool = True, emit_barrier_calls: bool = False, ): - self._module: pyqir.Module + self._module: pyqir.Module = None # type: ignore[assignment] self._qiskit_module: QiskitModule | None = None - self._builder: pyqir.Builder + self._builder: pyqir.Builder = None # type: ignore[assignment] self._entry_point: str = "" self._qubit_labels: dict[Qubit, int] = {} self._clbit_labels: dict[Clbit, int] = {} @@ -113,6 +113,10 @@ def visit_qiskit_module(self, module: QiskitModule) -> None: module.num_qubits, module.num_clbits, ) + self._qubit_labels.clear() + self._clbit_labels.clear() + self._measured_qubits.clear() + if module.module is None: raise ValueError("QiskitModule must have a PyQIR module set before visiting.") self._module = module.module @@ -134,8 +138,16 @@ def entry_point(self) -> str: """Return the entry point name.""" return self._entry_point + def _check_initialized(self) -> None: + """Raise if the visitor has not been initialized via visit_qiskit_module.""" + if self._module is None or self._builder is None: + raise RuntimeError( + "Visitor has not been initialized. Call visit_qiskit_module() first." + ) + def finalize(self) -> None: """Finalize the QIR module by adding a return instruction.""" + self._check_initialized() self._builder.ret(None) def record_output(self, module: QiskitModule) -> None: @@ -146,6 +158,7 @@ def record_output(self, module: QiskitModule) -> None: """ if not self._record_output: return + self._check_initialized() i8p = PointerType(IntType(self._module.context, 8)) @@ -282,8 +295,10 @@ def visit_instruction( def ir(self) -> str: """Return the QIR as a string.""" + self._check_initialized() return str(self._module) def bitcode(self) -> bytes: """Return the QIR as bitcode.""" + self._check_initialized() return self._module.bitcode diff --git a/tests/qiskit_qir/test_complex_circuits.py b/tests/qiskit_qir/test_complex_circuits.py index 2ad6f209..4619db32 100644 --- a/tests/qiskit_qir/test_complex_circuits.py +++ b/tests/qiskit_qir/test_complex_circuits.py @@ -128,7 +128,8 @@ def test_bound_parameter(self): ops = _gate_ops(module) assert len(ops) == 1 assert "qis__rx__body" in ops[0] - assert "double" in ops[0] + # pi/4 ≈ 0.7854; pyqir may emit as hex (0x3FE921FB…) or decimal + assert ("0x3FE921FB" in ops[0]) or ("7.853982e-01" in ops[0]) or ("0.785398" in ops[0]) def test_multiple_bound_parameters(self): a = Parameter("a") @@ -319,7 +320,7 @@ def test_transpile_preserves_simple_circuit(self): # Both should have the same gate types no_names = [line.split("qis__")[1].split("__")[0] for line in ops_no] yes_names = [line.split("qis__")[1].split("__")[0] for line in ops_yes] - assert set(no_names) == set(yes_names) + assert sorted(no_names) == sorted(yes_names) def test_ecr_without_transpile_uses_decomposition(self): """ECR without transpile should still work via composite decomposition.""" From 6353236594247aede0b3170ebc18bf294f80818a Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Wed, 20 May 2026 17:13:40 +0530 Subject: [PATCH 9/9] fix: add type hints to abstract visitor, bind record_output to visited module, move NOTICE.md - Add parameter and return type annotations to QuantumCircuitElementVisitor abstract methods - Remove module param from record_output(); use self._qiskit_module instead - Move NOTICE.md to qbraid_qir/qiskit/ where the adapted code lives --- NOTICE.md => qbraid_qir/qiskit/NOTICE.md | 0 qbraid_qir/qiskit/elements.py | 2 +- qbraid_qir/qiskit/visitor.py | 24 +++++++++++++++--------- 3 files changed, 16 insertions(+), 10 deletions(-) rename NOTICE.md => qbraid_qir/qiskit/NOTICE.md (100%) diff --git a/NOTICE.md b/qbraid_qir/qiskit/NOTICE.md similarity index 100% rename from NOTICE.md rename to qbraid_qir/qiskit/NOTICE.md diff --git a/qbraid_qir/qiskit/elements.py b/qbraid_qir/qiskit/elements.py index 4eab0be2..b495fe2e 100644 --- a/qbraid_qir/qiskit/elements.py +++ b/qbraid_qir/qiskit/elements.py @@ -191,5 +191,5 @@ def accept(self, visitor): visitor.visit_qiskit_module(self) for element in self._elements: element.accept(visitor) - visitor.record_output(self) + visitor.record_output() visitor.finalize() diff --git a/qbraid_qir/qiskit/visitor.py b/qbraid_qir/qiskit/visitor.py index 8a1afd4e..509759c8 100644 --- a/qbraid_qir/qiskit/visitor.py +++ b/qbraid_qir/qiskit/visitor.py @@ -63,11 +63,16 @@ class QuantumCircuitElementVisitor(metaclass=ABCMeta): """Abstract base class for quantum circuit element visitors.""" @abstractmethod - def visit_register(self, register): + def visit_register(self, register: Union[QuantumRegister, ClassicalRegister]) -> None: """Visit a register element.""" @abstractmethod - def visit_instruction(self, instruction, qargs, cargs): + def visit_instruction( + self, + instruction: Instruction, + qargs: tuple[Qubit, ...], + cargs: tuple[Clbit, ...], + ) -> None: """Visit an instruction element.""" @@ -150,22 +155,23 @@ def finalize(self) -> None: self._check_initialized() self._builder.ret(None) - def record_output(self, module: QiskitModule) -> None: - """Record output for classical registers. - - Args: - module: The QiskitModule containing register information. - """ + def record_output(self) -> None: + """Record output for classical registers using the visited module's register layout.""" if not self._record_output: return self._check_initialized() + if self._qiskit_module is None: + raise RuntimeError( + "No QiskitModule has been visited. Call visit_qiskit_module() first." + ) + i8p = PointerType(IntType(self._module.context, 8)) # Qiskit inverts the ordering of results within each register # but keeps the overall register ordering logical_id_base = 0 - for size in module.reg_sizes: + for size in self._qiskit_module.reg_sizes: rt.array_record_output( self._builder, pyqir.const(IntType(self._module.context, 64), size),