Source code for qbraid.transpiler.conversions.qiskit.qiskit_to_aqt_connector

# 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 defining the ``qiskit -> aqt_connector`` conversion.

Reduces a qiskit circuit to the AQT native basis ``{RZ, R, RXX}`` — with angles wrapped into the
ranges the arnica API accepts (``R.theta in [0, pi]``, ``R.phi in [0, 2*pi]``,
``RXX.theta in [0, pi/2]``) — and builds a native ``aqt_connector.models.circuits.QuantumCircuit``.
``repetitions`` (shots) is a placeholder here and is set per circuit by
:class:`~qbraid.runtime.aqt.AQTDevice` at submit time.

The angle-wrapping passes (:class:`WrapRxxAngles`, :class:`RewriteRxAsR`) and the gate serializer
(:func:`_qiskit_to_aqt_circuit`) are adapted from ``qiskit-aqt-provider``
(``transpiler_plugin.py``, (C) Copyright Alpine Quantum Technologies GmbH 2023; and
``circuit_to_aqt.py``, (C) Copyright IBM 2019, Alpine Quantum Technologies GmbH 2022), both under
the Apache License 2.0. They have been altered to live in qBraid — applied via an explicit
:class:`~qiskit.transpiler.PassManager` rather than qiskit entry-point plugins — so qBraid does not
depend on the ``qiskit-aqt-provider`` package.

"""

from __future__ import annotations

import math
from dataclasses import dataclass
from typing import TYPE_CHECKING, Final

import numpy as np
from qbraid_core._import import LazyLoader
from qiskit import QuantumCircuit, transpile
from qiskit.circuit import Gate, Instruction
from qiskit.circuit.library import RGate, RXGate, RXXGate, RZGate
from qiskit.circuit.tools import pi_check
from qiskit.dagcircuit import DAGCircuit
from qiskit.transpiler import PassManager
from qiskit.transpiler.basepasses import TransformationPass
from qiskit.transpiler.passes import Decompose, Optimize1qGatesDecomposition

from qbraid.transpiler.annotations import requires_extras

# aqt_connector is an optional (aqt-extra) dependency; this module is imported whenever the qiskit
# conversions load, so defer importing it via LazyLoader to keep the transpiler import-safe.
aqt_circuits = LazyLoader("aqt_circuits", globals(), "aqt_connector.models.circuits")
aqt_operations = LazyLoader("aqt_operations", globals(), "aqt_connector.models.operations")

if TYPE_CHECKING:
    import qiskit.circuit
    from aqt_connector.models.circuits import Circuit
    from aqt_connector.models.circuits import QuantumCircuit as AQTQuantumCircuit

# AQT hardware natively implements this basis: virtual-Z (RZ), single-qubit rotation (R),
# and the Molmer-Sorensen entangler (RXX).
AQT_BASIS_GATES: Final = ["rz", "r", "rxx"]


def rewrite_rx_as_r(theta: float) -> Instruction:
    """Instruction equivalent to ``Rx(theta)`` as ``R(theta, phi)`` with theta in [0, pi]."""
    theta = math.atan2(math.sin(theta), math.cos(theta))
    phi = math.pi if theta < 0.0 else 0.0
    return RGate(abs(theta), phi)


class RewriteRxAsR(TransformationPass):
    """Rewrite ``Rx(theta)`` as ``R(theta, phi)`` with theta in [0, pi] and phi in [0, 2*pi].

    Since the pass needs to determine if the relevant angles are in range, target circuits must
    have all these angles bound when applying the pass.
    """

    def run(self, dag: DAGCircuit) -> DAGCircuit:
        """Apply the transformation pass."""
        for node in dag.gate_nodes():
            if node.name == "rx":
                (theta,) = node.op.params
                dag.substitute_node(node, rewrite_rx_as_r(float(theta)))
        return dag


@dataclass(frozen=True)
class _CircuitInstruction:
    """Substitute for ``qiskit.circuit.CircuitInstruction`` that allows integer qubits."""

    gate: Gate
    qubits: tuple[int, ...]


def _rxx_positive_angle(theta: float) -> list[_CircuitInstruction]:
    """List of instructions equivalent to ``RXX(theta)`` with theta >= 0."""
    rxx = _CircuitInstruction(RXXGate(abs(theta)), qubits=(0, 1))

    if theta >= 0:
        return [rxx]

    return [
        _CircuitInstruction(RZGate(math.pi), (0,)),
        rxx,
        _CircuitInstruction(RZGate(math.pi), (0,)),
    ]


def _emit_rxx_instruction(theta: float, instructions: list[_CircuitInstruction]) -> Instruction:
    """Collect the passed instructions into a single one labeled ``Rxx(theta)``."""
    qc = QuantumCircuit(2, name=f"{WrapRxxAngles.SUBSTITUTE_GATE_NAME}({pi_check(theta)})")
    for instruction in instructions:
        qc.append(instruction.gate, instruction.qubits)

    return qc.to_instruction()


def wrap_rxx_angle(theta: float) -> Instruction:
    """Instruction equivalent to ``RXX(theta)`` with theta in [0, pi/2]."""
    # fast path if -pi/2 <= theta <= pi/2
    if abs(theta) <= math.pi / 2:
        operations = _rxx_positive_angle(theta)
        return _emit_rxx_instruction(theta, operations)

    # exploit 2-pi periodicity of Rxx
    theta %= 2 * math.pi

    if abs(theta) <= math.pi / 2:
        operations = _rxx_positive_angle(theta)
    elif abs(theta) <= 3 * math.pi / 2:
        corrected_angle = theta - np.sign(theta) * math.pi
        operations = [
            _CircuitInstruction(RXGate(math.pi), (0,)),
            _CircuitInstruction(RXGate(math.pi), (1,)),
        ]
        operations.extend(_rxx_positive_angle(corrected_angle))
    else:
        corrected_angle = theta - np.sign(theta) * 2 * math.pi
        operations = _rxx_positive_angle(corrected_angle)

    return _emit_rxx_instruction(theta, operations)


class WrapRxxAngles(TransformationPass):
    """Wrap ``Rxx`` angles to [0, pi/2]."""

    SUBSTITUTE_GATE_NAME: Final = "Rxx-wrapped"

    def run(self, dag: DAGCircuit) -> DAGCircuit:
        """Apply the transformation pass."""
        for node in dag.gate_nodes():
            if node.name == "rxx":
                (theta,) = node.op.params

                if 0 <= float(theta) <= math.pi / 2:
                    continue

                rxx = wrap_rxx_angle(float(theta))
                dag.substitute_node(node, rxx)

        return dag


def transpile_to_aqt(
    circuit: qiskit.circuit.QuantumCircuit,
) -> qiskit.circuit.QuantumCircuit:
    """Transpile ``circuit`` to the AQT native basis with API-valid angles.

    Mirrors ``qiskit-aqt-provider``'s ``translation_method="aqt"`` + ``scheduling_method="aqt"``
    stages without depending on that package: a standard translation to ``{rx, rz, rxx}`` followed
    by the ported angle-wrapping passes. The result uses only ``{rz, r, rxx}`` (plus measurement)
    with ``R``/``RXX`` angles in the ranges the arnica API accepts.
    """
    base = transpile(circuit, basis_gates=["rx", "rz", "rxx"], optimization_level=1)
    pass_manager = PassManager(
        [
            # Wrap out-of-range RXX angles into [0, pi/2] (substituting a composite gate) ...
            WrapRxxAngles(),
            # ... then decompose those composites back to primitive gates.
            Decompose([f"{WrapRxxAngles.SUBSTITUTE_GATE_NAME}*"]),
            # Collapse single-qubit runs as ZXZ (virtual Z + a single RX pulse) ...
            Optimize1qGatesDecomposition(basis=["rx", "rz"]),
            # ... then rewrite each RX as an angle-wrapped R.
            RewriteRxAsR(),
        ]
    )
    return pass_manager.run(base)


def _qiskit_to_aqt_circuit(circuit: qiskit.circuit.QuantumCircuit) -> Circuit:
    """Serialize a qiskit circuit already in the AQT native basis into an AQT ``Circuit``.

    Expects only ``{rz, r, rxx}`` gates (plus terminal measurement / barriers) with API-valid
    angles — i.e. the output of :func:`transpile_to_aqt`. Angles are converted from radians to the
    API's units of pi.
    """
    ops: list = []
    num_measurements = 0

    for instruction in circuit.data:
        name = instruction.operation.name
        if name != "measure" and num_measurements > 0:
            raise ValueError(
                "Measurement operations can only be located at the end of the circuit."
            )

        if name == "rz":
            (phi,) = instruction.operation.params
            (qubit,) = instruction.qubits
            ops.append(
                aqt_operations.OperationModel(
                    root=aqt_operations.GateRZ(
                        phi=float(phi) / np.pi, qubit=circuit.find_bit(qubit).index
                    )
                )
            )
        elif name == "r":
            theta, phi = instruction.operation.params
            (qubit,) = instruction.qubits
            ops.append(
                aqt_operations.OperationModel(
                    root=aqt_operations.GateR(
                        phi=float(phi) / np.pi,
                        theta=float(theta) / np.pi,
                        qubit=circuit.find_bit(qubit).index,
                    )
                )
            )
        elif name == "rxx":
            (theta,) = instruction.operation.params
            q0, q1 = instruction.qubits
            ops.append(
                aqt_operations.OperationModel(
                    root=aqt_operations.GateRXX(
                        theta=float(theta) / np.pi,
                        qubits=[circuit.find_bit(q0).index, circuit.find_bit(q1).index],
                    )
                )
            )
        elif name == "measure":
            num_measurements += 1
        elif name == "barrier":
            continue
        else:
            raise ValueError(f"Operation '{name}' not in basis gate set: {{rz, r, rxx}}")

    if not num_measurements:
        raise ValueError("Circuit must have at least one measurement operation.")

    ops.append(aqt_operations.OperationModel(root=aqt_operations.Measure()))
    return aqt_circuits.Circuit(root=ops)


[docs] @requires_extras("aqt_connector") def qiskit_to_aqt_connector(circuit: qiskit.circuit.QuantumCircuit) -> AQTQuantumCircuit: """Return a native AQT ``QuantumCircuit`` from a qiskit ``QuantumCircuit``. Args: circuit (qiskit.circuit.QuantumCircuit): Qiskit quantum circuit. Returns: aqt_connector.models.circuits.QuantumCircuit: the per-circuit submission unit with the circuit reduced to the AQT native basis and ``number_of_qubits`` taken from the transpiled register size. ``repetitions`` is a placeholder (``1``); the device sets it to the requested shots at submit time. """ transpiled = transpile_to_aqt(circuit) return aqt_circuits.QuantumCircuit( repetitions=1, quantum_circuit=_qiskit_to_aqt_circuit(transpiled), number_of_qubits=transpiled.num_qubits, )