Source code for qrisp.circuit.pass_management.passes.convert_to_prx

"""Convert single-qubit gates to PRX (Phased-RX) decomposition."""

from __future__ import annotations

import numpy as np

from qrisp.circuit.operation import ClControlledOperation, U3Gate
from qrisp.circuit.pass_management.circuit_pass import CircuitPass
from qrisp.circuit.quantum_circuit import QuantumCircuit
from qrisp.circuit.standard_operations import GPhaseGate


[docs] class PRXGate(U3Gate): r"""PRX (Phased-RX) gate. The PRX gate is a single-qubit gate of the form: .. math:: \text{PRX}(\alpha, \beta) = R_Z(\beta) \cdot R_X(\alpha) \cdot R_Z(-\beta) Parameters ---------- alpha : float The rotation angle. beta : float The phase parameter. """ def __init__(self, alpha: float, beta: float) -> None: self.alpha = alpha self.beta = beta # PRX(alpha, beta) = RZ(beta) * RX(alpha) * RZ(-beta) # # Using RX(alpha) = RZ(-pi/2) * RY(alpha) * RZ(pi/2) and the U3 # decomposition U3(theta, phi, lam) = RZ(phi) * RY(theta) * RZ(lam): # # PRX(alpha, beta) = RZ(beta) * RZ(-pi/2) * RY(alpha) * RZ(pi/2) * RZ(-beta) # = RZ(beta - pi/2) * RY(alpha) * RZ(pi/2 - beta) # = U3(alpha, beta - pi/2, pi/2 - beta) # super().__init__(alpha, beta - np.pi / 2, np.pi / 2 - beta, name="prx") def inverse(self): """Returns the inverse of the PRX gate. The inverse of :math:`R_Z(\\beta) R_X(\\alpha) R_Z(-\\beta)` is :math:`R_Z(\\beta) R_X(-\\alpha) R_Z(-\\beta)`. Returns ------- PRXGate The inverted PRX gate with parameters ``(-alpha, beta)``. """ return PRXGate(-self.alpha, self.beta)
def _get_phase_diff(U_a: np.ndarray, U_b: np.ndarray) -> float: """Return the global phase :math:`\\gamma` such that :math:`U_a = e^{i\\gamma} \\, U_b`. Computed as :math:`\\gamma = \\arg(\\operatorname{tr}(U_a U_b^\\dagger) / 2)`. """ return float(np.angle(np.trace(U_a @ U_b.conj().T) / 2))
[docs] @CircuitPass def convert_to_prx(qc: QuantumCircuit) -> QuantumCircuit: """Convert single-qubit gates to PRX (Phased-RX) gate decomposition. This pass converts arbitrary single-qubit gates to PRX gates. When a U3 gate is already in PRX form (:math:`\\lambda \\approx -\\phi`), it is replaced by a single :class:`PRXGate`. Otherwise it is decomposed into a sequence of two :class:`PRXGate` operations. Global phases introduced by the decompositions are accumulated and emitted as a :class:`~qrisp.circuit.GPhaseGate` on the zeroth qubit of the output circuit at the end of the pass. Parameters ---------- qc : QuantumCircuit The input quantum circuit. Returns ------- QuantumCircuit A new circuit with single-qubit gates decomposed into PRX gates and an optional trailing global-phase gate. Example ------- >>> from qrisp import PassManager, convert_to_prx >>> pm = PassManager() >>> pm.add_pass(convert_to_prx) >>> transpiled_qc = pm.run(qc) """ qc_new = qc.clearcopy() accumulated_phase = 0.0 for i in range(len(qc.data)): op = qc.data[i].op if isinstance(op, ClControlledOperation): conversion_op = op.base_op else: conversion_op = op if isinstance(conversion_op, U3Gate): U_orig = conversion_op.get_unitary() # Single PRX case: lambda ≈ -phi (U3 is already in PRX form) if abs(conversion_op.lam + conversion_op.phi) < 1e-5: prx_0 = PRXGate(conversion_op.theta, conversion_op.phi + np.pi / 2) # Track global phase accumulated_phase += _get_phase_diff(U_orig, prx_0.get_unitary()) # Append gate if not identity if abs(conversion_op.theta % (2 * np.pi)) >= 1e-5: if isinstance(op, ClControlledOperation): qc_new.append(prx_0.c_if(op.num_control, op.ctrl_state), qc.data[i].qubits) # type: ignore[arg-type] else: qc_new.append(prx_0, qc.data[i].qubits) # Two PRX case: decompose arbitrary U3 into two PRX gates else: prx_0 = PRXGate(conversion_op.theta + np.pi, -conversion_op.lam + np.pi / 2) prx_1 = PRXGate(np.pi, (conversion_op.phi - conversion_op.lam) / 2 + np.pi / 2) # Combined unitary: PRX_1 · PRX_0 (prx_0 applied first in circuit) U_prx = prx_1.get_unitary() @ prx_0.get_unitary() accumulated_phase += _get_phase_diff(U_orig, U_prx) if not (abs(prx_0.alpha % (2 * np.pi)) < 1e-5): if isinstance(op, ClControlledOperation): qc_new.append( prx_0.c_if(op.num_control, op.ctrl_state), # type: ignore[arg-type] qc.data[i].qubits, qc.data[i].clbits, ) else: qc_new.append(prx_0, qc.data[i].qubits) if not (abs(prx_1.alpha % (2 * np.pi)) < 1e-5): if isinstance(op, ClControlledOperation): qc_new.append( prx_1.c_if(op.num_control, op.ctrl_state), # type: ignore[arg-type] qc.data[i].qubits, qc.data[i].clbits, ) else: qc_new.append(prx_1, qc.data[i].qubits) else: qc_new.append(qc.data[i]) # Emit accumulated global phase on the zeroth qubit accumulated_phase = accumulated_phase % (2 * np.pi) if abs(accumulated_phase) > 1e-10 and len(qc_new.qubits) > 0: qc_new.append(GPhaseGate(accumulated_phase), [qc_new.qubits[0]]) return qc_new