Source code for qbraid_qir.qiskit.visitor

# 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.

# 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.

"""

import logging
from abc import ABCMeta, abstractmethod
from typing import Union

import pyqir
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

from qbraid_qir._pyqir_compat import pointer_id

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__)


class QuantumCircuitElementVisitor(metaclass=ABCMeta):
    """Abstract base class for quantum circuit element visitors."""

    @abstractmethod
    def visit_register(self, register: Union[QuantumRegister, ClassicalRegister]) -> None:
        """Visit a register element."""

    @abstractmethod
    def visit_instruction(
        self,
        instruction: Instruction,
        qargs: tuple[Qubit, ...],
        cargs: tuple[Clbit, ...],
    ) -> None:
        """Visit an instruction element."""


[docs] 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. 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. """
[docs] def __init__( self, initialize_runtime: bool = True, record_output: bool = True, emit_barrier_calls: bool = False, ): self._module: pyqir.Module = None # type: ignore[assignment] self._qiskit_module: QiskitModule | None = None self._builder: pyqir.Builder = None # type: ignore[assignment] 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._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 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 _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) -> 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 self._qiskit_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", list(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[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] 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() if op_name in NOOP_INSTRUCTIONS: return if op_name in PYQIR_MEASUREMENT_OP_MAP: 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 qis.mz(self._builder, qubit, result) elif op_name == "barrier": if self._emit_barrier_calls: qis.barrier(self._builder) 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( 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.""" self._check_initialized() return str(self._module) def bitcode(self) -> bytes: """Return the QIR as bitcode.""" self._check_initialized() return self._module.bitcode