From 4208cbe2c983257f6e7a283fab7ccdb1877ca92f Mon Sep 17 00:00:00 2001 From: Omar Ibrahim Date: Thu, 8 Oct 2026 10:37:17 +0200 Subject: [PATCH 1/8] Fix incorrect gates implementation, and adjust global phases. Fix incorrect gates implementation (i.e: use the correct matrices). This covers RGate, SX, SX_dag gates. Also the U1 maps to an RZ Gate in qrisp but maps to a phase gate in qiskit, so necessary global phase is now added when doing the mapping. --- src/qrisp/circuit/standard_operations.py | 4 +++- src/qrisp/interface/converter/qiskit_converter.py | 2 +- .../operation_tests/test_standard_operations.py | 7 +++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/qrisp/circuit/standard_operations.py b/src/qrisp/circuit/standard_operations.py index aabb03af16..63b6420d8b 100644 --- a/src/qrisp/circuit/standard_operations.py +++ b/src/qrisp/circuit/standard_operations.py @@ -271,7 +271,7 @@ def RGate(theta: FloatLike = 0, phi: FloatLike = 0): The R gate operation. """ - res = U3Gate(-theta, phi, -phi, name="r", global_phase=0) + res = U3Gate(theta, phi - (np.pi / 2), (np.pi / 2) - phi, name="r", global_phase=0) res.params = [theta, phi] return res @@ -587,6 +587,7 @@ def SXGate(): """ res = RXGate(np.pi / 2) + res.global_phase = np.pi / 4 res.name = "sx" res.params = [] @@ -606,6 +607,7 @@ def SXDGGate(): """ res = RXGate(-np.pi / 2) + res.global_phase = -np.pi / 4 res.name = "sx_dg" res.params = [] diff --git a/src/qrisp/interface/converter/qiskit_converter.py b/src/qrisp/interface/converter/qiskit_converter.py index 3f0b4294b4..7addb7e664 100644 --- a/src/qrisp/interface/converter/qiskit_converter.py +++ b/src/qrisp/interface/converter/qiskit_converter.py @@ -224,7 +224,7 @@ def create_qiskit_instruction(op, params=[]): elif op.name == "p": qiskit_ins = qsk_gates.PhaseGate(params[0]) elif op.name == "u1": - qiskit_ins = qsk_gates.PhaseGate(params[0]) + qiskit_ins = qsk_gates.RZGate(params[0]) elif op.name == "s": qiskit_ins = qsk_gates.SGate() elif op.name == "s_dg": diff --git a/tests/circuit_tests/operation_tests/test_standard_operations.py b/tests/circuit_tests/operation_tests/test_standard_operations.py index 96aca85cb3..5ddbc571c7 100644 --- a/tests/circuit_tests/operation_tests/test_standard_operations.py +++ b/tests/circuit_tests/operation_tests/test_standard_operations.py @@ -66,15 +66,14 @@ def test_sx_permeability_and_qfree(self): def test_sx_unitary(self): """SXGate unitary matches RX(π/2) = (1/√2)*[[1, -i], [-i, 1]].""" gate = SXGate() - s = 1 / np.sqrt(2) - expected = np.array([[s, -1j * s], [-1j * s, s]], dtype=complex) + expected = (1 / 2) * np.array([[1 + 1j, 1 - 1j], [1 - 1j, 1 + 1j]], dtype=complex) assert np.allclose(gate.get_unitary(), expected, atol=1e-6) def test_sx_squared_is_x_up_to_phase(self): """SX @ SX = -i·X (RX(π/2) composed twice = RX(π) = -i·X).""" sx = SXGate().get_unitary() x = np.array([[0, 1], [1, 0]], dtype=complex) - assert np.allclose(sx @ sx, -1j * x, atol=1e-6) + assert np.allclose(sx @ sx, x, atol=1e-6) def test_sxdg_name_and_params(self): """SXDGGate has name 'sx_dg' and no parameters.""" @@ -164,7 +163,7 @@ def test_r_unitary(self, theta, phi): c = np.cos(theta / 2) s = np.sin(theta / 2) expected = np.array( - [[c, np.exp(-1j * phi) * s], [-np.exp(1j * phi) * s, c]], + [[c, -1j * np.exp(-1j * phi) * s], [-1j * np.exp(1j * phi) * s, c]], dtype=complex, ) assert np.allclose(gate.get_unitary(), expected, atol=1e-6) From 385b0d17ef6ebb58df5fa31ee7729a77fee043e2 Mon Sep 17 00:00:00 2001 From: Omar Ibrahim Date: Fri, 9 Oct 2026 17:21:36 +0200 Subject: [PATCH 2/8] address comments, update other affected tests, extend one test --- .../general/changelog/changelog-dev.rst | 12 ++++++++ src/qrisp/circuit/controlled_operations.py | 5 +++- .../converter/pennylane_converter.py | 6 ++-- .../test_standard_operations.py | 4 +-- .../operation_tests/test_u3_gate.py | 3 ++ .../test_converter_qrisp_to_qml.py | 28 ++++++++++--------- 6 files changed, 39 insertions(+), 19 deletions(-) diff --git a/documentation/source/general/changelog/changelog-dev.rst b/documentation/source/general/changelog/changelog-dev.rst index bfdbdc221f..05662d000c 100644 --- a/documentation/source/general/changelog/changelog-dev.rst +++ b/documentation/source/general/changelog/changelog-dev.rst @@ -607,6 +607,18 @@ Bug Fixes * Fixed ``reset`` on a single qubit, such as ``reset(qv[0])``, failing to compile with :ref:`qjit `. Resetting a whole QuantumVariable was not affected (`PR #941 `_). +* Fixed the matrices of several standard gates and their conversions: + + - ``RGate(theta, phi)`` now implements ``RZ(phi) RX(theta) RZ(-phi)``, + matching the standard R gate. Its PennyLane conversion is now + ``qml.Rot(pi/2 - phi, theta, phi - pi/2)``. + - ``SXGate`` and ``SXDGGate`` now carry the global phase ``±pi/4``, so they + are the exact square roots of ``X`` instead of ``RX(±pi/2)``. This phase is + also kept when they are controlled. They are now converted to ``qml.SX`` + in PennyLane. + - ``U1Gate`` now converts to Qiskit's ``RZGate`` instead of ``PhaseGate``, + since Qrisp's ``U1`` is an RZ rotation. + (`PR #949 `_). Compatibility ------------- diff --git a/src/qrisp/circuit/controlled_operations.py b/src/qrisp/circuit/controlled_operations.py index d5f35be8f7..36da2f133a 100644 --- a/src/qrisp/circuit/controlled_operations.py +++ b/src/qrisp/circuit/controlled_operations.py @@ -93,11 +93,14 @@ def multi_controlled_u3_circ(u3_gate, control_amount, ctrl_state, method=None): elif u3_gate.name in ["rx", "sx", "sx_dg"]: # Same thing as with rz but now we use RX = H RZ H + # Once controlled, the base gate's global phase (non-zero for sx and sx_dg) + # becomes a relative phase, and thus is added to both target phases. qc.h(target_qubit) gray_phase_synth_qb_list( qc, qc.qubits, - (2 ** (control_amount + 1) - 2) * [0] + [-u3_gate.theta / 2, u3_gate.theta / 2], + (2 ** (control_amount + 1) - 2) * [0] + + [u3_gate.global_phase - u3_gate.theta / 2, u3_gate.global_phase + u3_gate.theta / 2], phase_tolerant=method in ["gray_pt", "gray_pt_inv"], ) qc.h(target_qubit) diff --git a/src/qrisp/interface/converter/pennylane_converter.py b/src/qrisp/interface/converter/pennylane_converter.py index fbf243f519..bd1de10b94 100644 --- a/src/qrisp/interface/converter/pennylane_converter.py +++ b/src/qrisp/interface/converter/pennylane_converter.py @@ -70,13 +70,13 @@ class QMLGateDescriptor: "s": QMLGateDescriptor(qml.S), "t": QMLGateDescriptor(qml.T), "id": QMLGateDescriptor(qml.Identity, param_fn=lambda _: []), - "sx": QMLGateDescriptor(qml.RX, param_fn=lambda _: [np.pi / 2]), # SXGate() -> qml.RX(pi/2) + "sx": QMLGateDescriptor(qml.SX), "gphase": QMLGateDescriptor( qml.GlobalPhase, param_fn=lambda op: [-op.params[0]] ), # GPhaseGate(phi) -> qml.GlobalPhase(-phi) "r": QMLGateDescriptor( - qml.Rot, param_fn=lambda op: [-op.params[1], -op.params[0], op.params[1]] - ), # RGate(theta, phi) -> qml.Rot(-phi, -theta, phi) + qml.Rot, param_fn=lambda op: [np.pi / 2 - op.params[1], op.params[0], op.params[1] - np.pi / 2] + ), # RGate(theta, phi) = RZ(phi) RX(theta) RZ(-phi) -> qml.Rot(pi/2 - phi, theta, phi - pi/2) "measure": QMLGateDescriptor(qml.measurements.MidMeasureMP), } diff --git a/tests/circuit_tests/operation_tests/test_standard_operations.py b/tests/circuit_tests/operation_tests/test_standard_operations.py index 5ddbc571c7..4044eb4314 100644 --- a/tests/circuit_tests/operation_tests/test_standard_operations.py +++ b/tests/circuit_tests/operation_tests/test_standard_operations.py @@ -64,13 +64,13 @@ def test_sx_permeability_and_qfree(self): assert gate.is_qfree is False def test_sx_unitary(self): - """SXGate unitary matches RX(π/2) = (1/√2)*[[1, -i], [-i, 1]].""" + """SXGate unitary matches √X = (1/2)*[[1 + i, 1 -i], [1 - i, 1 + i]].""" gate = SXGate() expected = (1 / 2) * np.array([[1 + 1j, 1 - 1j], [1 - 1j, 1 + 1j]], dtype=complex) assert np.allclose(gate.get_unitary(), expected, atol=1e-6) def test_sx_squared_is_x_up_to_phase(self): - """SX @ SX = -i·X (RX(π/2) composed twice = RX(π) = -i·X).""" + """SX @ SX = X""" sx = SXGate().get_unitary() x = np.array([[0, 1], [1, 0]], dtype=complex) assert np.allclose(sx @ sx, x, atol=1e-6) diff --git a/tests/circuit_tests/operation_tests/test_u3_gate.py b/tests/circuit_tests/operation_tests/test_u3_gate.py index 4a40454177..4799f9aad8 100644 --- a/tests/circuit_tests/operation_tests/test_u3_gate.py +++ b/tests/circuit_tests/operation_tests/test_u3_gate.py @@ -37,6 +37,7 @@ RYGate, RZGate, SGate, + SXDGGate, SXGate, TGate, ) @@ -524,6 +525,8 @@ class TestU3GateNamedVariantControl: ("rz", RZGate, (2.0,)), ("rx", RXGate, (1.2,)), ("ry", RYGate, (0.8,)), + ("sx", SXGate, ()), + ("sx_dg", SXDGGate, ()), ], ) def test_named_variant_controlled_unitary_vs_transpiled(self, name, gate_factory, args): diff --git a/tests/interface_tests/test_converter_qrisp_to_qml.py b/tests/interface_tests/test_converter_qrisp_to_qml.py index c30f4063d3..35954c502f 100644 --- a/tests/interface_tests/test_converter_qrisp_to_qml.py +++ b/tests/interface_tests/test_converter_qrisp_to_qml.py @@ -67,6 +67,7 @@ (u3Gate, qml.U3, [np.pi / 2, np.pi / 4, np.pi / 8]), (IDGate, qml.Identity, []), (U1Gate, qml.RZ, [np.pi / 6]), + (SXGate, qml.SX, []), ] # These are gates that map to the same PennyLane gate but with different parameters @@ -75,9 +76,8 @@ RGate, qml.Rot, [np.pi / 2, np.pi / 3], - [-np.pi / 3, -np.pi / 2, np.pi / 3], - ), # RGate(theta, phi) -> qml.Rot(-phi, -theta, phi) - (SXGate, qml.RX, [], [np.pi / 2]), # SXGate() -> qml.RX(pi/2) + [np.pi / 2 - np.pi / 3, np.pi / 2, np.pi / 3 - np.pi / 2], + ), # RGate(theta, phi) -> qml.Rot(pi/2 - phi, theta, phi - pi/2) ( GPhaseGate, qml.GlobalPhase, @@ -180,7 +180,7 @@ def check_probs_measurement_equivalence(qrisp_qv, qml_res, atol=1e-5): [ ( RGate, - lambda t, p: qml.Rot(-p, -t, p, wires=0), + lambda t, p: qml.Rot(np.pi / 2 - p, t, p - np.pi / 2, wires=0), [(np.pi / 3, np.pi / 4), (1.2, 2.3), (0.0, 0.0)], ), (PGate, lambda p: qml.PhaseShift(p, wires=0), [(np.pi / 6,), (1.5,), (0.0,)]), @@ -212,8 +212,8 @@ def check_probs_measurement_equivalence(qrisp_qv, qml_res, atol=1e-5): ), ( SXGate, - lambda: qml.RX(np.pi / 2, wires=0), - [], + lambda: qml.SX(wires=0), + [()], ), ], ) @@ -360,6 +360,7 @@ def test_abstract_parameters(self): qrisp_qs.append(RZGate(abstract_parameters[0]), qrisp_qv[0]) qrisp_qs.append(PGate(1), qrisp_qv[0]) qrisp_qs.append(RYGate(abstract_parameters[1] + abstract_parameters[2]), qrisp_qv[0]) + qrisp_qs.append(RGate(abstract_parameters[2], abstract_parameters[0]), qrisp_qv[0]) subs_dic = { abstract_parameters[0]: np.pi / 4, @@ -375,6 +376,7 @@ def test_abstract_parameters(self): qml.RZ(np.pi / 4, wires=qrisp_qv[0].identifier), qml.PhaseShift(1, wires=qrisp_qv[0].identifier), qml.RY(np.pi / 2 + np.pi / 3, wires=qrisp_qv[0].identifier), + qml.Rot(np.pi / 2 - np.pi / 4, np.pi / 3, np.pi / 4 - np.pi / 2, wires=qrisp_qv[0].identifier), ] check_qml_operations(qml_converted_circuit, expected_ops) @@ -508,10 +510,10 @@ def test_h_sx_gates(self): expected_ops = [ qml.Hadamard(wires=qrisp_qv[0].identifier), - qml.RX(np.pi / 2, wires=qrisp_qv[0].identifier), - qml.RX(np.pi / 2, wires=qrisp_qv[1].identifier), + qml.SX(wires=qrisp_qv[0].identifier), + qml.SX(wires=qrisp_qv[1].identifier), qml.Hadamard(wires=qrisp_qv[1].identifier), - qml.RX(np.pi / 2, wires=qrisp_qv[1].identifier), + qml.SX(wires=qrisp_qv[1].identifier), qml.Hadamard(wires=qrisp_qv[0].identifier), qml.Hadamard(wires=qrisp_qv[1].identifier), ] @@ -534,7 +536,7 @@ def test_adjoint_gates(self): expected_ops = [ qml.adjoint(qml.S)(wires=qrisp_qv[0].identifier), - qml.adjoint(qml.RX(np.pi / 2, wires=qrisp_qv[0].identifier)), + qml.adjoint(qml.SX(wires=qrisp_qv[0].identifier)), qml.adjoint(qml.T)(wires=qrisp_qv[0].identifier), qml.RX(-0.5, wires=qrisp_qv[0].identifier), ] @@ -740,7 +742,7 @@ def test_controlled_adjoint_gate(self): expected_ops = [ qml.ctrl( - op=qml.adjoint(qml.RX(np.pi / 2, wires=qrisp_qv[2].identifier)), + op=qml.adjoint(qml.SX(wires=qrisp_qv[2].identifier)), control=[ qrisp_qv[3].identifier, qrisp_qv[0].identifier, @@ -749,7 +751,7 @@ def test_controlled_adjoint_gate(self): control_values=[0, 1, 0], ), qml.ctrl( - op=qml.adjoint(qml.RX(np.pi / 2, wires=qrisp_qv[2].identifier)), + op=qml.adjoint(qml.SX(wires=qrisp_qv[2].identifier)), control=[ qrisp_qv[3].identifier, qrisp_qv[0].identifier, @@ -828,7 +830,7 @@ def test_mixed_circuit(): qml.Y(qrisp_qv[1].identifier), qml.Z(qrisp_qv[2].identifier), qml.H(qrisp_qv[3].identifier), - qml.RX(np.pi / 2, wires=qrisp_qv[4].identifier), + qml.SX(wires=qrisp_qv[4].identifier), qml.S(qrisp_qv[5].identifier), qml.RX(0.5, wires=qrisp_qv[1].identifier), qml.RY(0.5, wires=qrisp_qv[2].identifier), From 1c198fa44888a5fc4fd1ed7e549e533937fa13c6 Mon Sep 17 00:00:00 2001 From: Omar Ibrahim Date: Tue, 27 Oct 2026 10:45:31 +0100 Subject: [PATCH 3/8] address comments --- src/qrisp/circuit/controlled_operations.py | 36 +++++++----------- .../pass_management/passes/convert_to_prx.py | 6 +++ src/qrisp/circuit/quantum_circuit.py | 4 +- src/qrisp/circuit/standard_operations.py | 33 +++++++++++------ src/qrisp/core/gate_application_functions.py | 2 +- .../interface/converter/qiskit_converter.py | 2 + .../interpreters/catalyst_interpreter.py | 2 + src/qrisp/operators/qubit/qubit_operator.py | 6 +-- .../test_standard_operations.py | 8 ++++ tests/circuit_tests/passes/test_PRXGate.py | 14 +++++++ tests/core_tests/test_controlled_gates.py | 17 +++++++++ .../test_gate_application_functions.py | 9 +++++ .../test_converter_qrisp_to_qml.py | 13 ++++++- .../interface_tests/test_qiskit_converter.py | 17 +++++++++ tests/jax_tests/test_catalyst_interface.py | 37 +++++++++++++++++++ tests/operators_tests/test_qubit_operator.py | 23 +++++++++++- 16 files changed, 186 insertions(+), 43 deletions(-) diff --git a/src/qrisp/circuit/controlled_operations.py b/src/qrisp/circuit/controlled_operations.py index 36da2f133a..89ba6a83cd 100644 --- a/src/qrisp/circuit/controlled_operations.py +++ b/src/qrisp/circuit/controlled_operations.py @@ -64,27 +64,20 @@ def multi_controlled_u3_circ(u3_gate, control_amount, ctrl_state, method=None): if ctrl_state[i] == "0": qc.x(qc.qubits[i]) + # Once controlled, the global phase becomes a phase on the states where all controls + # are on. The branches for diagonal unitaries add it to both target phases, the others synthesize + # it on the control qubits only. + gp = u3_gate.global_phase + # If the U3 gate is an rx , ry, rz or p gate, we can use gray phase synthesis on the # target qubit and wrap this in the corresponding gates # (for instance H for rx because RX = H RZ H) - if u3_gate.name in ["p", "t", "s", "t_dg", "s_dg"]: - # Synthesize phases using gray synthesis - gray_phase_synth_qb_list( - qc, - qc.qubits, - (2 ** (control_amount + 1) - 1) * [0] + [u3_gate.lam + u3_gate.phi], - phase_tolerant=method in ["gray_pt", "gray_pt_inv"], - ) - - if method == "gray_pt_inv": - qc = qc.inverse() - - elif u3_gate.name == "rz": - # Make sure to account for the possible global phase of rz + if u3_gate.name in ["p", "t", "s", "t_dg", "s_dg", "rz"]: + # Since theta = 0, U3 is diag(e^(i*gp), e^(i*(gp + phi + lam))) gray_phase_synth_qb_list( qc, qc.qubits, - (2 ** (control_amount + 1) - 2) * [0] + [-u3_gate.params[0] / 2, u3_gate.params[0] / 2], + (2 ** (control_amount + 1) - 2) * [0] + [gp, gp + u3_gate.phi + u3_gate.lam], phase_tolerant=method in ["gray_pt", "gray_pt_inv"], ) @@ -93,14 +86,11 @@ def multi_controlled_u3_circ(u3_gate, control_amount, ctrl_state, method=None): elif u3_gate.name in ["rx", "sx", "sx_dg"]: # Same thing as with rz but now we use RX = H RZ H - # Once controlled, the base gate's global phase (non-zero for sx and sx_dg) - # becomes a relative phase, and thus is added to both target phases. qc.h(target_qubit) gray_phase_synth_qb_list( qc, qc.qubits, - (2 ** (control_amount + 1) - 2) * [0] - + [u3_gate.global_phase - u3_gate.theta / 2, u3_gate.global_phase + u3_gate.theta / 2], + (2 ** (control_amount + 1) - 2) * [0] + [gp - u3_gate.theta / 2, gp + u3_gate.theta / 2], phase_tolerant=method in ["gray_pt", "gray_pt_inv"], ) qc.h(target_qubit) @@ -115,7 +105,7 @@ def multi_controlled_u3_circ(u3_gate, control_amount, ctrl_state, method=None): gray_phase_synth_qb_list( qc, qc.qubits, - (2 ** (control_amount + 1) - 2) * [0] + [u3_gate.theta / 2, -u3_gate.theta / 2], + (2 ** (control_amount + 1) - 2) * [0] + [gp + u3_gate.theta / 2, gp - u3_gate.theta / 2], phase_tolerant=method in ["gray_pt", "gray_pt_inv"], ) qc.h(target_qubit) @@ -124,7 +114,7 @@ def multi_controlled_u3_circ(u3_gate, control_amount, ctrl_state, method=None): if method == "gray_pt_inv": qc = qc.inverse() - elif u3_gate.phi == 0 and u3_gate.lam == 0 and u3_gate.theta == 0 and u3_gate.global_phase == 0: + elif u3_gate.phi == 0 and u3_gate.lam == 0 and u3_gate.theta == 0 and gp == 0: pass elif u3_gate.phi == 0 and u3_gate.lam == 0 and u3_gate.theta == 0: @@ -133,7 +123,7 @@ def multi_controlled_u3_circ(u3_gate, control_amount, ctrl_state, method=None): gray_phase_synth_qb_list( qc, qc.qubits[:-1], - (2 ** (control_amount) - 1) * [0] + [u3_gate.global_phase], + (2 ** (control_amount) - 1) * [0] + [gp], ) # Treat pauli gates @@ -185,7 +175,7 @@ def multi_controlled_u3_circ(u3_gate, control_amount, ctrl_state, method=None): gray_phase_synth_qb_list( qc, qc.qubits[:-1], - (2 ** (control_amount) - 1) * [0] + [u3_gate.global_phase + (alpha + beta) / 2], + (2 ** (control_amount) - 1) * [0] + [gp + (alpha + beta) / 2], ) qc.append(A.to_gate("A"), [qc.qubits[-1]]) diff --git a/src/qrisp/circuit/pass_management/passes/convert_to_prx.py b/src/qrisp/circuit/pass_management/passes/convert_to_prx.py index a2c347f33a..4bce3db26b 100644 --- a/src/qrisp/circuit/pass_management/passes/convert_to_prx.py +++ b/src/qrisp/circuit/pass_management/passes/convert_to_prx.py @@ -57,6 +57,12 @@ def inverse(self): """ return PRXGate(-self.alpha, self.beta) + def bind_parameters(self, subs_dic: dict) -> PRXGate: + """Return the PRX gate with the abstract parameters in ``subs_dic`` bound to their values.""" + # U3Gate.bind_parameters would return a plain U3Gate named "prx" + bound = U3Gate.bind_parameters(self, subs_dic) + return PRXGate(bound.theta, bound.phi + np.pi / 2) + def _get_phase_diff(U_a: np.ndarray, U_b: np.ndarray) -> float: r"""Return the global phase :math:`\gamma` such that :math:`U_a = e^{i\gamma} \, U_b`. diff --git a/src/qrisp/circuit/quantum_circuit.py b/src/qrisp/circuit/quantum_circuit.py index 46bf428ed3..2abc20a299 100644 --- a/src/qrisp/circuit/quantum_circuit.py +++ b/src/qrisp/circuit/quantum_circuit.py @@ -2830,8 +2830,8 @@ def r(self, theta: FloatLike, phi: FloatLike, qubits: QubitLike): R(\theta, \phi) = \begin{pmatrix} - \cos\!\tfrac{\theta}{2} & e^{-i\phi}\sin\!\tfrac{\theta}{2} \\ - -e^{i\phi}\sin\!\tfrac{\theta}{2} & \cos\!\tfrac{\theta}{2} + \cos\!\tfrac{\theta}{2} & -i e^{-i\phi}\sin\!\tfrac{\theta}{2} \\ + -i e^{i\phi}\sin\!\tfrac{\theta}{2} & \cos\!\tfrac{\theta}{2} \end{pmatrix} Parameters diff --git a/src/qrisp/circuit/standard_operations.py b/src/qrisp/circuit/standard_operations.py index 63b6420d8b..e5b0e79f56 100644 --- a/src/qrisp/circuit/standard_operations.py +++ b/src/qrisp/circuit/standard_operations.py @@ -255,8 +255,8 @@ def RZGate(phi: FloatLike = 0): return res -def RGate(theta: FloatLike = 0, phi: FloatLike = 0): - """Return an R gate. +class RGate(U3Gate): + """R gate. Parameters ---------- @@ -265,15 +265,23 @@ def RGate(theta: FloatLike = 0, phi: FloatLike = 0): phi : FloatLike, optional The axis angle in radians. The default is 0. - Returns - ------- - :class:`.Operation` - The R gate operation. - """ - res = U3Gate(theta, phi - (np.pi / 2), (np.pi / 2) - phi, name="r", global_phase=0) - res.params = [theta, phi] - return res + + def __init__(self, theta: FloatLike = 0, phi: FloatLike = 0) -> None: + """Initialize the R gate as ``U3(theta, phi - pi/2, pi/2 - phi)``.""" + super().__init__(theta, phi - (np.pi / 2), (np.pi / 2) - phi, name="r", global_phase=0) + self.params = [theta, phi] + + def inverse(self) -> RGate: + """Return the inverse of the R gate, ``RGate(-theta, phi)``.""" + theta, phi = self.params + return RGate(-theta, phi) + + def bind_parameters(self, subs_dic: dict) -> RGate: + """Return the R gate with the abstract parameters in ``subs_dic`` bound to their values.""" + # U3Gate.bind_parameters would return a plain U3Gate with the raw U3 angles as params + theta, phi = Operation.bind_parameters(self, subs_dic).params + return RGate(theta, phi) def GPhaseGate(phi: FloatLike = 0): @@ -540,7 +548,10 @@ def u3Gate(theta: FloatLike = 0, phi: FloatLike = 0, lam: FloatLike = 0): def U1Gate(phi: FloatLike = 0): - """Return a U1 gate, equivalent to :func:`RZGate` up to a global phase. + """Return a U1 gate, identical to :func:`RZGate`. + + Unlike Qiskit's ``u1``, which is the phase gate :func:`PGate`, this gate + includes the global phase of RZ. Parameters ---------- diff --git a/src/qrisp/core/gate_application_functions.py b/src/qrisp/core/gate_application_functions.py index 1f00095ff9..b2adcab458 100644 --- a/src/qrisp/core/gate_application_functions.py +++ b/src/qrisp/core/gate_application_functions.py @@ -986,7 +986,7 @@ def sx_dg(qubits): The Qubit to perform the daggered SX gate on. """ - append_operation(std_ops.SXDGGate().inverse(), [qubits]) + append_operation(std_ops.SXDGGate(), [qubits]) def gphase(phi, qubits): diff --git a/src/qrisp/interface/converter/qiskit_converter.py b/src/qrisp/interface/converter/qiskit_converter.py index 7addb7e664..9ebb97fa72 100644 --- a/src/qrisp/interface/converter/qiskit_converter.py +++ b/src/qrisp/interface/converter/qiskit_converter.py @@ -267,6 +267,8 @@ def create_qiskit_instruction(op, params=[]): op_dic = {op().name: op for op in op_list} op_dic["u"] = op_dic["u3"] +# Qiskit's u1 is the phase gate, while Qrisp's U1Gate is RZ +op_dic["u1"] = op_dic["p"] def convert_from_qiskit(qiskit_qc): diff --git a/src/qrisp/jasp/interpreter_tools/interpreters/catalyst_interpreter.py b/src/qrisp/jasp/interpreter_tools/interpreters/catalyst_interpreter.py index 228bb00b0e..04f197bdd0 100644 --- a/src/qrisp/jasp/interpreter_tools/interpreters/catalyst_interpreter.py +++ b/src/qrisp/jasp/interpreter_tools/interpreters/catalyst_interpreter.py @@ -327,6 +327,8 @@ def exec_qrisp_op(op, catalyst_qbs, param_dict): param_list = [lambdify(greek_letters[: len(op.abstract_params)], expr)(*jax_values) for expr in op.params] + # Catalyst has no native SX gate. SX = RX(pi/2) up to a global phase, + # which is dropped here anyway (see gphase above). if op_name == "sx": op_name = "rx" param_list = [jnp.pi / 2] diff --git a/src/qrisp/operators/qubit/qubit_operator.py b/src/qrisp/operators/qubit/qubit_operator.py index 8fdbb88098..2825307106 100644 --- a/src/qrisp/operators/qubit/qubit_operator.py +++ b/src/qrisp/operators/qubit/qubit_operator.py @@ -38,7 +38,7 @@ invert, merge, s, - sx_dg, + sx, ) from qrisp.jasp import check_for_tracing_mode, jrange, q_switch from qrisp.operators.hamiltonian import Hamiltonian @@ -1123,7 +1123,7 @@ def change_of_basis(self, qarg=None, method="commuting_qw"): """ # Assuming all terms of self commute qubit-wise, # the basis change for Pauli factor is trivial: - # Z stays the same, for X we apply an h gate and for Y and s_dg. + # Z stays the same, for X we apply an h gate and for Y an SX gate. # For ladder operators, the situation is more intricate. @@ -1233,7 +1233,7 @@ def change_of_basis(self, qarg=None, method="commuting_qw"): h(qarg[j]) if factor_dict[j] == "Y": - sx_dg(qarg[j]) + sx(qarg[j]) new_factor_dict[j] = "Z" diff --git a/tests/circuit_tests/operation_tests/test_standard_operations.py b/tests/circuit_tests/operation_tests/test_standard_operations.py index 4044eb4314..f7ca7a2e46 100644 --- a/tests/circuit_tests/operation_tests/test_standard_operations.py +++ b/tests/circuit_tests/operation_tests/test_standard_operations.py @@ -174,6 +174,14 @@ def test_r_zero_theta_is_identity(self): gate = RGate(0.0, phi) assert np.allclose(gate.get_unitary(), np.eye(2), atol=1e-6) + def test_r_bind_parameters_keeps_r_gate(self): + """Binding symbolic parameters returns an RGate with params [theta, phi], not a plain U3Gate.""" + theta, phi = sympy.symbols("theta phi") + gate = RGate(theta, phi).bind_parameters({theta: 1.2, phi: 0.7}) + assert isinstance(gate, RGate) + assert gate.params == [1.2, 0.7] + assert np.allclose(gate.get_unitary(), RGate(1.2, 0.7).get_unitary()) + class TestCPGate: """Tests for CPGate and its special-case short-circuits.""" diff --git a/tests/circuit_tests/passes/test_PRXGate.py b/tests/circuit_tests/passes/test_PRXGate.py index 0948c44acd..aeedf4b145 100644 --- a/tests/circuit_tests/passes/test_PRXGate.py +++ b/tests/circuit_tests/passes/test_PRXGate.py @@ -17,6 +17,7 @@ """Tests for PRXGate construction, inverse, and unitary in qrisp.circuit.pass_management.passes.convert_to_prx.""" import numpy as np +import sympy from qrisp import U3Gate from qrisp.circuit import PRXGate @@ -101,6 +102,19 @@ def test_double_inverse_is_identity(self): assert abs(inv_inv.beta - prx.beta) < 1e-10 +class TestPRXGateBindParameters: + """Tests for binding symbolic PRXGate parameters.""" + + def test_bind_parameters_keeps_prx_gate(self): + """Binding symbolic parameters returns a PRXGate with bound alpha and beta, not a plain U3Gate.""" + alpha, beta = sympy.symbols("alpha beta") + gate = PRXGate(alpha, beta).bind_parameters({alpha: 0.7, beta: 1.2}) + assert isinstance(gate, PRXGate) + assert np.isclose(gate.alpha, 0.7) + assert np.isclose(gate.beta, 1.2) + assert np.allclose(gate.get_unitary(), PRXGate(0.7, 1.2).get_unitary()) + + class TestPRXGateUnitary: """Verify the PRXGate unitary against the IQM SDK definition.""" diff --git a/tests/core_tests/test_controlled_gates.py b/tests/core_tests/test_controlled_gates.py index dd164ed345..11bec0f9a4 100644 --- a/tests/core_tests/test_controlled_gates.py +++ b/tests/core_tests/test_controlled_gates.py @@ -129,3 +129,20 @@ def test_controlled_gates(): u3 = U3Gate(1.7465, -0.58331, 0) cu3 = u3.control(1) assert np.allclose(cu3.get_unitary(), cu3.definition.get_unitary(), atol=1e-5) + + +@pytest.mark.parametrize( + "u3_gate", + [ + U3Gate(0, 0, 0.3, name="p", global_phase=0.4), + U3Gate(0, 0.3, 0, name="rz", global_phase=0.25), + U3Gate(0.7, -np.pi / 2, np.pi / 2, name="rx", global_phase=0.4), + U3Gate(0.7, 0, 0, name="ry", global_phase=0.4), + ], +) +@pytest.mark.parametrize("control_amount", [1, 2]) +def test_controlled_named_gate_keeps_global_phase(u3_gate, control_amount): + """Once controlled, a named gate's global phase becomes a relative phase, so the + synthesized circuit must include it, not only the angles the gate name implies.""" + controlled_gate = u3_gate.control(control_amount) + assert np.allclose(controlled_gate.get_unitary(), controlled_gate.definition.get_unitary(), atol=1e-6) diff --git a/tests/core_tests/test_gate_application_functions.py b/tests/core_tests/test_gate_application_functions.py index ae5ce99866..dddf40e3e6 100644 --- a/tests/core_tests/test_gate_application_functions.py +++ b/tests/core_tests/test_gate_application_functions.py @@ -16,6 +16,7 @@ """Tests for the gate application functions in Qrisp.""" +import numpy as np import pytest from qrisp import QuantumVariable @@ -119,3 +120,11 @@ def test_mcz_applies_the_gate_exactly_once(method): result = mcz(list(qv.reg), method=method) assert result is None assert str(qv.qs.statevector()) == "-|111>" + + +def test_sx_dg_is_inverse_of_sx(): + """sx_dg used to apply SX itself, so sx followed by sx_dg gave X instead of the identity.""" + qv = QuantumVariable(1) + sx(qv[0]) + sx_dg(qv[0]) + assert np.allclose(qv.qs.compile().get_unitary(), np.eye(2)) diff --git a/tests/interface_tests/test_converter_qrisp_to_qml.py b/tests/interface_tests/test_converter_qrisp_to_qml.py index 35954c502f..bb686c06ff 100644 --- a/tests/interface_tests/test_converter_qrisp_to_qml.py +++ b/tests/interface_tests/test_converter_qrisp_to_qml.py @@ -22,7 +22,7 @@ from qrisp.grover import diffuser from sympy import symbols -from qrisp import QuantumFloat, QuantumVariable, auto_uncompute, h, z +from qrisp import QuantumCircuit, QuantumFloat, QuantumVariable, auto_uncompute, h, z from qrisp.circuit.standard_operations import ( # Barrier, CPGate, CXGate, @@ -265,6 +265,15 @@ def test_single_gate_conversion_diff_params(self, qrisp_gate_class, qml_gate_cla check_statevector_equivalence(qv, qml_res) + @pytest.mark.parametrize("theta,phi", [(0.3, 0.5), (np.pi / 3, -1.2), (np.pi, 2.0)]) + def test_inverse_r_gate_unitary(self, theta, phi): + """The inverse R gate converts to the conjugate transpose of R(theta, phi).""" + qc = QuantumCircuit(1) + qc.r(theta, phi, 0) + tape = qml.tape.make_qscript(qml_converter(qc.inverse()))() + expected = RGate(theta, phi).get_unitary().conj().T + assert np.allclose(qml.matrix(tape, wire_order=tape.wires), expected) + def test_inner_wires(self): """Test conversion of a circuit with inner wires provided to the converter.""" qrisp_qv = QuantumVariable(2) @@ -530,6 +539,7 @@ def test_adjoint_gates(self): qrisp_qs.append(SXGate().inverse(), qrisp_qv[0]) qrisp_qs.append(TGate().inverse(), qrisp_qv[0]) qrisp_qs.append(RXGate(0.5).inverse(), qrisp_qv[0]) + qrisp_qs.append(RGate(0.3, 0.5).inverse(), qrisp_qv[0]) qml_converted_circuit = _create_qml_circuit(qrisp_qv) qml_res = qml_converted_circuit() @@ -539,6 +549,7 @@ def test_adjoint_gates(self): qml.adjoint(qml.SX(wires=qrisp_qv[0].identifier)), qml.adjoint(qml.T)(wires=qrisp_qv[0].identifier), qml.RX(-0.5, wires=qrisp_qv[0].identifier), + qml.Rot(np.pi / 2 - 0.5, -0.3, 0.5 - np.pi / 2, wires=qrisp_qv[0].identifier), ] check_qml_operations(qml_converted_circuit, expected_ops) diff --git a/tests/interface_tests/test_qiskit_converter.py b/tests/interface_tests/test_qiskit_converter.py index 9a05154397..2dab87941e 100644 --- a/tests/interface_tests/test_qiskit_converter.py +++ b/tests/interface_tests/test_qiskit_converter.py @@ -19,6 +19,7 @@ import numpy as np import pytest from qiskit import QuantumCircuit as QiskitQC +from qiskit.circuit.library import SXGate, U1Gate from qiskit.quantum_info import Operator from qrisp import QuantumCircuit @@ -247,6 +248,22 @@ def test_rgate_to_prx(self, theta, phi): assert abs(prx_ops[0].alpha - theta) < 1e-10 assert abs(prx_ops[0].beta - phi) < 1e-10 + @pytest.mark.parametrize("gate", [U1Gate(0.7), SXGate()], ids=["u1", "sx"]) + @pytest.mark.parametrize("num_ctrl_qubits", [0, 1]) + def test_global_phase_survives_transpile(self, gate, num_ctrl_qubits): + """Qiskit's u1 and sx keep their exact unitary, global phase included, through Qrisp. + + Controlling a gate turns its global phase into a relative phase. The circuit is + transpiled in Qrisp before converting back, so the conversion goes through the gates' + definitions rather than relying only on their names. + """ + if num_ctrl_qubits: + gate = gate.control(num_ctrl_qubits) + qc = QiskitQC(2) + qc.append(gate, list(range(gate.num_qubits))) + qc_rt = convert_to_qiskit(convert_from_qiskit(qc).transpile()) + assert_unitary_equal(qc, qc_rt) + def test_cx(self): qc = QiskitQC(2) qc.cx(0, 1) diff --git a/tests/jax_tests/test_catalyst_interface.py b/tests/jax_tests/test_catalyst_interface.py index ba29254830..f8f1431dad 100644 --- a/tests/jax_tests/test_catalyst_interface.py +++ b/tests/jax_tests/test_catalyst_interface.py @@ -183,6 +183,43 @@ def array_reset(): return measure(qf) assert array_reset() == 0 +def test_sx_catalyst(): + """SX is lowered to RX(pi/2) since Catalyst has no native SX gate.""" + try: + import catalyst + except ModuleNotFoundError: + return + + # SX * SX = X + @qjit + def sx_squared(): + qv = QuantumVariable(1) + sx(qv[0]) + sx(qv[0]) + return measure(qv) + + assert sx_squared() == 1 + + # SX_DG * SX = I + @qjit + def sx_then_sx_dg(): + qv = QuantumVariable(1) + sx(qv[0]) + sx_dg(qv[0]) + return measure(qv) + + assert sx_then_sx_dg() == 0 + + # SX|0> ~ |0> - i|1>, which S maps to |+> and H to |0>. SX_DG would give |1> instead. + @qjit + def sx_s_h(): + qv = QuantumVariable(1) + sx(qv[0]) + s(qv[0]) + h(qv[0]) + return measure(qv) + + assert sx_s_h() == 0 def test_parity_catalyst(): diff --git a/tests/operators_tests/test_qubit_operator.py b/tests/operators_tests/test_qubit_operator.py index 9ed4d69319..9a328b6a77 100644 --- a/tests/operators_tests/test_qubit_operator.py +++ b/tests/operators_tests/test_qubit_operator.py @@ -14,9 +14,13 @@ # * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 # ******************************************************************************** -"""Tests that multiplying a QubitOperator by zero produces an operator with an empty terms dict.""" +"""Tests for QubitOperator.""" -from qrisp.operators.qubit import X +import numpy as np +import pytest + +from qrisp import QuantumVariable +from qrisp.operators.qubit import X, Y, Z def test_qubit_operator_mul_by_zero(): @@ -35,3 +39,18 @@ def test_qubit_operator_mul_by_zero(): op *= 0 assert op.terms_dict == {} assert not op.find_minimal_qubit_amount() + + +@pytest.mark.parametrize( + "operator", + [Y(0), X(0) * Y(1), Y(0) * Z(1) + 0.5 * Y(0), Y(0) * Y(1) * X(2)], +) +def test_change_of_basis_reconstructs_operator(operator): + """The gates U applied by change_of_basis and the returned diagonal operator D + satisfy U^dg D U = operator, so measuring D after U measures the operator. + """ + n = operator.find_minimal_qubit_amount() + qv = QuantumVariable(n) + diagonal_operator = operator.change_of_basis(qv) + U = qv.qs.compile().get_unitary() + assert np.allclose(U.conj().T @ diagonal_operator.to_array(n) @ U, operator.to_array(n)) From 0db221222a63184df26b9e30626e74958728da78 Mon Sep 17 00:00:00 2001 From: Omar Ibrahim Date: Tue, 27 Oct 2026 15:34:30 +0100 Subject: [PATCH 4/8] Fix ruff formatting in test_catalyst_interface.py --- tests/jax_tests/test_catalyst_interface.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/jax_tests/test_catalyst_interface.py b/tests/jax_tests/test_catalyst_interface.py index f8f1431dad..8121f4b639 100644 --- a/tests/jax_tests/test_catalyst_interface.py +++ b/tests/jax_tests/test_catalyst_interface.py @@ -183,6 +183,8 @@ def array_reset(): return measure(qf) assert array_reset() == 0 + + def test_sx_catalyst(): """SX is lowered to RX(pi/2) since Catalyst has no native SX gate.""" try: From 98de45235f48b635908e013ab69a7e06011ff185 Mon Sep 17 00:00:00 2001 From: Omar Ibrahim Date: Tue, 10 Nov 2026 12:07:22 +0100 Subject: [PATCH 5/8] Deprecate RGate and QuantumCircuit.r in favor of PRXGate After the R gate fix, RGate(theta, phi) and PRXGate(theta, phi) are the same gate, U3(theta, phi - pi/2, pi/2 - phi). Keeping both meant two names, two parameter conventions and two converter mappings for one gate, so PRXGate becomes the single implementation. Deprecations (both emit QrispDeprecationWarning and keep working): - RGate is now a thin PRXGate subclass. RGate(theta, phi) builds PRXGate(theta, phi), so the operation is named "prx" instead of "r" and stores its angles as alpha and beta. - QuantumCircuit.r forwards to the new QuantumCircuit.prx. Supporting changes: - Move PRXGate from passes/convert_to_prx.py to standard_operations.py, which convert_to_prx now imports from. Existing imports keep working. - Give PRXGate default angles of 0 and add it to op_list, so the Qiskit converter can register it. RGate is left out of op_list so that importing qrisp does not emit the warning. Qiskit's "r" is still imported as PRXGate. - Rename the PennyLane mapping key from "r" to "prx" (same qml.Rot formula), since no operation is named "r" anymore. Tests and docs: - Move the PRXGate tests from passes/test_PRXGate.py into test_standard_operations.py, since they test the gate, not the pass. The RGate unitary tests now test PRXGate directly, and TestRGate only covers the deprecation. - Add QuantumCircuit.prx to the docs and a changelog entry. --- .../general/changelog/changelog-dev.rst | 7 + .../Circuit Manipulation/QuantumCircuit.rst | 1 + .../generated/qrisp.QuantumCircuit.prx.rst | 6 + .../pass_management/passes/convert_to_prx.py | 56 +------ src/qrisp/circuit/quantum_circuit.py | 41 ++++- src/qrisp/circuit/standard_operations.py | 89 ++++++++-- .../converter/pennylane_converter.py | 6 +- .../test_standard_operations.py | 158 +++++++++++++++--- tests/circuit_tests/passes/test_PRXGate.py | 136 --------------- tests/circuit_tests/test_quantum_circuit.py | 19 ++- 10 files changed, 274 insertions(+), 245 deletions(-) create mode 100644 documentation/source/reference/Circuit Manipulation/generated/qrisp.QuantumCircuit.prx.rst delete mode 100644 tests/circuit_tests/passes/test_PRXGate.py diff --git a/documentation/source/general/changelog/changelog-dev.rst b/documentation/source/general/changelog/changelog-dev.rst index 05662d000c..683b6335c0 100644 --- a/documentation/source/general/changelog/changelog-dev.rst +++ b/documentation/source/general/changelog/changelog-dev.rst @@ -640,6 +640,13 @@ Compatibility above 63, for which the number of calls of the unitary overflowed a 64-bit integer (`PR #932 `_). +* ``RGate`` and ``QuantumCircuit.r`` are deprecated in favor of the identical + ``PRXGate`` and the new ``QuantumCircuit.prx``. Both still work but emit a + ``QrispDeprecationWarning``. ``RGate(theta, phi)`` now builds a + ``PRXGate(theta, phi)``, so the resulting operation is named ``"prx"`` instead + of ``"r"`` and stores its angles as ``alpha`` and ``beta``. ``PRXGate`` can now + also be imported from ``qrisp.circuit.standard_operations`` + (`PR #949 `_). .. Add compatibility notes above this line diff --git a/documentation/source/reference/Circuit Manipulation/QuantumCircuit.rst b/documentation/source/reference/Circuit Manipulation/QuantumCircuit.rst index 7fbbf9366d..90f477b46a 100644 --- a/documentation/source/reference/Circuit Manipulation/QuantumCircuit.rst +++ b/documentation/source/reference/Circuit Manipulation/QuantumCircuit.rst @@ -134,6 +134,7 @@ Operation application methods QuantumCircuit.id QuantumCircuit.crx QuantumCircuit.barrier + QuantumCircuit.prx QuantumCircuit.r \ No newline at end of file diff --git a/documentation/source/reference/Circuit Manipulation/generated/qrisp.QuantumCircuit.prx.rst b/documentation/source/reference/Circuit Manipulation/generated/qrisp.QuantumCircuit.prx.rst new file mode 100644 index 0000000000..81d3e19ed4 --- /dev/null +++ b/documentation/source/reference/Circuit Manipulation/generated/qrisp.QuantumCircuit.prx.rst @@ -0,0 +1,6 @@ +qrisp.QuantumCircuit.prx +======================== + +.. currentmodule:: qrisp + +.. automethod:: QuantumCircuit.prx \ No newline at end of file diff --git a/src/qrisp/circuit/pass_management/passes/convert_to_prx.py b/src/qrisp/circuit/pass_management/passes/convert_to_prx.py index 4bce3db26b..c1197daef5 100644 --- a/src/qrisp/circuit/pass_management/passes/convert_to_prx.py +++ b/src/qrisp/circuit/pass_management/passes/convert_to_prx.py @@ -7,61 +7,7 @@ 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 - - -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): - r"""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 bind_parameters(self, subs_dic: dict) -> PRXGate: - """Return the PRX gate with the abstract parameters in ``subs_dic`` bound to their values.""" - # U3Gate.bind_parameters would return a plain U3Gate named "prx" - bound = U3Gate.bind_parameters(self, subs_dic) - return PRXGate(bound.theta, bound.phi + np.pi / 2) +from qrisp.circuit.standard_operations import GPhaseGate, PRXGate def _get_phase_diff(U_a: np.ndarray, U_b: np.ndarray) -> float: diff --git a/src/qrisp/circuit/quantum_circuit.py b/src/qrisp/circuit/quantum_circuit.py index 2abc20a299..fcf379b3f4 100644 --- a/src/qrisp/circuit/quantum_circuit.py +++ b/src/qrisp/circuit/quantum_circuit.py @@ -18,6 +18,7 @@ from __future__ import annotations +import warnings from typing import TYPE_CHECKING, Any, cast import numpy as np @@ -33,6 +34,7 @@ import qrisp.circuit.standard_operations as ops from qrisp.circuit import Clbit, Instruction, Operation, Qubit, U3Gate from qrisp.misc import ( + QrispDeprecationWarning, cnot_count, cnot_depth_indicator, get_depth_dic, @@ -2821,19 +2823,39 @@ def u3(self, theta: FloatLike, phi: FloatLike, lam: FloatLike, qubits: QubitLike """ self.append(ops.u3Gate(theta, phi, lam), [qubits]) - def r(self, theta: FloatLike, phi: FloatLike, qubits: QubitLike): - r"""Instruct an R-gate. + def prx(self, alpha: FloatLike, beta: FloatLike, qubits: QubitLike): + r"""Instruct a PRX (Phased-RX) gate. The unitary is .. math:: - R(\theta, \phi) = + \text{PRX}(\alpha, \beta) = R_Z(\beta) R_X(\alpha) R_Z(-\beta) = \begin{pmatrix} - \cos\!\tfrac{\theta}{2} & -i e^{-i\phi}\sin\!\tfrac{\theta}{2} \\ - -i e^{i\phi}\sin\!\tfrac{\theta}{2} & \cos\!\tfrac{\theta}{2} + \cos\!\tfrac{\alpha}{2} & -i e^{-i\beta}\sin\!\tfrac{\alpha}{2} \\ + -i e^{i\beta}\sin\!\tfrac{\alpha}{2} & \cos\!\tfrac{\alpha}{2} \end{pmatrix} + Parameters + ---------- + alpha : FloatLike + Rotation angle in radians. Controls the amount of rotation. + beta : FloatLike + Phase angle in radians. Selects the rotation axis in the XY plane. + qubits : QubitLike + The qubit to apply the gate on. + + """ + self.append(ops.PRXGate(alpha, beta), [qubits]) + + def r(self, theta: FloatLike, phi: FloatLike, qubits: QubitLike): + """Instruct an R-gate. + + .. deprecated:: 0.10 + + ``r`` is deprecated, use :meth:`prx` instead. + ``qc.r(theta, phi, qubits)`` is identical to ``qc.prx(theta, phi, qubits)``. + Parameters ---------- theta : FloatLike @@ -2844,7 +2866,14 @@ def r(self, theta: FloatLike, phi: FloatLike, qubits: QubitLike): The qubit to apply the gate on. """ - self.append(ops.RGate(theta, phi), [qubits]) + warnings.warn( + "DeprecationWarning: QuantumCircuit.r is deprecated and will be removed in a " + "later release of Qrisp. Use QuantumCircuit.prx instead, " + "qc.r(theta, phi, qubits) is identical to qc.prx(theta, phi, qubits).", + QrispDeprecationWarning, + stacklevel=2, + ) + self.prx(theta, phi, qubits) def unitary(self, unitary_array, qubits: QubitLike): """Instruct a gate from a given unitary matrix. diff --git a/src/qrisp/circuit/standard_operations.py b/src/qrisp/circuit/standard_operations.py index e5b0e79f56..3431789aaf 100644 --- a/src/qrisp/circuit/standard_operations.py +++ b/src/qrisp/circuit/standard_operations.py @@ -19,12 +19,14 @@ from __future__ import annotations import types +import warnings from typing import TYPE_CHECKING import numpy as np from qrisp.circuit import PauliGate, U3Gate from qrisp.circuit.operation import Operation +from qrisp.misc.exceptions import QrispDeprecationWarning if TYPE_CHECKING: from qrisp.typing import FloatLike @@ -255,9 +257,69 @@ def RZGate(phi: FloatLike = 0): return res -class RGate(U3Gate): +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 : FloatLike, optional + The rotation angle. The default is 0. + beta : FloatLike, optional + The phase parameter. The default is 0. + + """ + + def __init__(self, alpha: FloatLike = 0, beta: FloatLike = 0) -> None: + """Initialize the PRX gate as ``U3(alpha, beta - pi/2, pi/2 - beta)``.""" + 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") # type: ignore[operator] + + def inverse(self) -> PRXGate: + r"""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) # type: ignore[arg-type] + + def bind_parameters(self, subs_dic: dict) -> PRXGate: + """Return the PRX gate with the abstract parameters in ``subs_dic`` bound to their values.""" + # U3Gate.bind_parameters would return a plain U3Gate named "prx" + bound = U3Gate.bind_parameters(self, subs_dic) + return PRXGate(bound.theta, bound.phi + np.pi / 2) # type: ignore[operator] + + +class RGate(PRXGate): """R gate. + .. deprecated:: 0.10 + + ``RGate`` is deprecated, use :class:`PRXGate` instead. + ``RGate(theta, phi)`` is identical to ``PRXGate(theta, phi)``. + Parameters ---------- theta : FloatLike, optional @@ -268,20 +330,15 @@ class RGate(U3Gate): """ def __init__(self, theta: FloatLike = 0, phi: FloatLike = 0) -> None: - """Initialize the R gate as ``U3(theta, phi - pi/2, pi/2 - phi)``.""" - super().__init__(theta, phi - (np.pi / 2), (np.pi / 2) - phi, name="r", global_phase=0) - self.params = [theta, phi] - - def inverse(self) -> RGate: - """Return the inverse of the R gate, ``RGate(-theta, phi)``.""" - theta, phi = self.params - return RGate(-theta, phi) - - def bind_parameters(self, subs_dic: dict) -> RGate: - """Return the R gate with the abstract parameters in ``subs_dic`` bound to their values.""" - # U3Gate.bind_parameters would return a plain U3Gate with the raw U3 angles as params - theta, phi = Operation.bind_parameters(self, subs_dic).params - return RGate(theta, phi) + """Initialize the deprecated R gate as ``PRXGate(theta, phi)``.""" + warnings.warn( + "DeprecationWarning: RGate is deprecated and will be removed in a later " + "release of Qrisp. Use PRXGate instead, RGate(theta, phi) is identical " + "to PRXGate(theta, phi).", + QrispDeprecationWarning, + stacklevel=2, + ) + super().__init__(theta, phi) def GPhaseGate(phi: FloatLike = 0): @@ -718,5 +775,5 @@ def QubitDealloc(): SwapGate, U1Gate, IDGate, - RGate, + PRXGate, ] diff --git a/src/qrisp/interface/converter/pennylane_converter.py b/src/qrisp/interface/converter/pennylane_converter.py index bd1de10b94..bc27432372 100644 --- a/src/qrisp/interface/converter/pennylane_converter.py +++ b/src/qrisp/interface/converter/pennylane_converter.py @@ -74,9 +74,9 @@ class QMLGateDescriptor: "gphase": QMLGateDescriptor( qml.GlobalPhase, param_fn=lambda op: [-op.params[0]] ), # GPhaseGate(phi) -> qml.GlobalPhase(-phi) - "r": QMLGateDescriptor( - qml.Rot, param_fn=lambda op: [np.pi / 2 - op.params[1], op.params[0], op.params[1] - np.pi / 2] - ), # RGate(theta, phi) = RZ(phi) RX(theta) RZ(-phi) -> qml.Rot(pi/2 - phi, theta, phi - pi/2) + "prx": QMLGateDescriptor( + qml.Rot, param_fn=lambda op: [np.pi / 2 - op.beta, op.alpha, op.beta - np.pi / 2] + ), # PRXGate(alpha, beta) = RZ(beta) RX(alpha) RZ(-beta) -> qml.Rot(pi/2 - beta, alpha, beta - pi/2) "measure": QMLGateDescriptor(qml.measurements.MidMeasureMP), } diff --git a/tests/circuit_tests/operation_tests/test_standard_operations.py b/tests/circuit_tests/operation_tests/test_standard_operations.py index f7ca7a2e46..ba24d9eab3 100644 --- a/tests/circuit_tests/operation_tests/test_standard_operations.py +++ b/tests/circuit_tests/operation_tests/test_standard_operations.py @@ -31,6 +31,7 @@ MCRXGate, MCXGate, Measurement, + PRXGate, QubitAlloc, QubitDealloc, Reset, @@ -46,6 +47,7 @@ XXYYGate, u3Gate, ) +from qrisp.misc.exceptions import QrispDeprecationWarning class TestSXGate: @@ -136,18 +138,110 @@ def test_u1_unitary(self, phi): assert np.allclose(gate.get_unitary(), expected, atol=1e-6) -class TestRGate: - """Tests for the R gate (rotation around cos(phi)*X + sin(phi)*Y axis).""" - - def test_r_name_and_params(self): - """RGate stores [theta, phi] in params and has name 'r'.""" - theta, phi = 1.2, 0.8 - gate = RGate(theta, phi) - assert gate.name == "r" - assert gate.params == [theta, phi] +class TestPRXGateConstruction: + """Tests for PRXGate parameter mapping and U3 inheritance.""" + + def test_u3_parameter_mapping(self): + """PRX(alpha, beta) maps to U3(alpha, beta - pi/2, pi/2 - beta).""" + alpha, beta = np.pi / 2, np.pi / 4 + prx = PRXGate(alpha, beta) + + assert prx.alpha == alpha + assert prx.beta == beta + assert abs(prx.theta - alpha) < 1e-10 + assert abs(prx.phi - (beta - np.pi / 2)) < 1e-10 + assert abs(prx.lam - (np.pi / 2 - beta)) < 1e-10 + + def test_is_subclass_of_U3Gate(self): + """PRXGate must inherit from U3Gate.""" + prx = PRXGate(0.5, 0.3) + assert isinstance(prx, U3Gate) + assert issubclass(PRXGate, U3Gate) + + def test_name_is_prx(self): + """PRXGate is named 'prx'.""" + assert PRXGate(0.1, 0.2).name == "prx" + + def test_u3_equivalence(self): + """PRXGate parameters should reproduce an equivalent U3Gate.""" + alpha, beta = np.pi / 2, np.pi / 4 + prx = PRXGate(alpha, beta) + u3 = U3Gate(alpha, beta - np.pi / 2, np.pi / 2 - beta) + assert abs(prx.theta - u3.theta) < 1e-10 + assert abs(prx.phi - u3.phi) < 1e-10 + assert abs(prx.lam - u3.lam) < 1e-10 + + +class TestPRXGateEdgeCases: + """Parameter edge cases: zero, negative, and large values.""" + + def test_zero_parameters(self): + """PRX(0, 0) maps to U3(0, -pi/2, pi/2).""" + prx = PRXGate(0, 0) + assert prx.alpha == 0 + assert prx.beta == 0 + assert abs(prx.phi - (-np.pi / 2)) < 1e-10 + assert abs(prx.lam - np.pi / 2) < 1e-10 + + def test_negative_parameters(self): + """Negative alpha and beta are stored unchanged.""" + prx = PRXGate(-np.pi / 3, -np.pi / 6) + assert prx.alpha == -np.pi / 3 + assert prx.beta == -np.pi / 6 + + def test_large_parameters(self): + """Angles beyond 2*pi are stored unchanged.""" + alpha = 2 * np.pi + np.pi / 2 + beta = 3 * np.pi + prx = PRXGate(alpha, beta) + assert prx.alpha == alpha + assert prx.beta == beta + + +class TestPRXGateInverse: + """Tests for the inverse() method.""" + + def test_inverse_returns_prxgate(self): + """inverse() returns a PRXGate named 'prx'.""" + prx = PRXGate(0.7, 1.2) + inv = prx.inverse() + assert isinstance(inv, PRXGate) + assert inv.name == "prx" + + def test_inverse_negates_alpha_preserves_beta(self): + """inverse() maps PRX(alpha, beta) to PRX(-alpha, beta).""" + alpha, beta = 0.7, 1.2 + prx = PRXGate(alpha, beta) + inv = prx.inverse() + assert abs(inv.alpha - (-alpha)) < 1e-10 + assert abs(inv.beta - beta) < 1e-10 + + def test_double_inverse_is_identity(self): + """Inverting twice restores the original alpha and beta.""" + prx = PRXGate(0.7, 1.2) + inv_inv = prx.inverse().inverse() + assert abs(inv_inv.alpha - prx.alpha) < 1e-10 + assert abs(inv_inv.beta - prx.beta) < 1e-10 + + +class TestPRXGateBindParameters: + """Tests for binding symbolic PRXGate parameters.""" + + def test_bind_parameters_keeps_prx_gate(self): + """Binding symbolic parameters returns a PRXGate with bound alpha and beta, not a plain U3Gate.""" + alpha, beta = sympy.symbols("alpha beta") + gate = PRXGate(alpha, beta).bind_parameters({alpha: 0.7, beta: 1.2}) + assert isinstance(gate, PRXGate) + assert np.isclose(gate.alpha, 0.7) + assert np.isclose(gate.beta, 1.2) + assert np.allclose(gate.get_unitary(), PRXGate(0.7, 1.2).get_unitary()) + + +class TestPRXGateUnitary: + """Tests for the PRXGate unitary.""" @pytest.mark.parametrize( - "theta, phi", + "alpha, beta", [ (0.0, 0.0), (np.pi, 0.0), @@ -157,30 +251,44 @@ def test_r_name_and_params(self): (2.5, -1.3), ], ) - def test_r_unitary(self, theta, phi): - """R(theta, phi) unitary matches the analytical formula.""" - gate = RGate(theta, phi) - c = np.cos(theta / 2) - s = np.sin(theta / 2) + def test_matches_analytical_formula(self, alpha, beta): + """PRX(alpha, beta) = RZ(beta) RX(alpha) RZ(-beta), matching the IQM SDK definition.""" + gate = PRXGate(alpha, beta) + c = np.cos(alpha / 2) + s = np.sin(alpha / 2) expected = np.array( - [[c, -1j * np.exp(-1j * phi) * s], [-1j * np.exp(1j * phi) * s, c]], + [[c, -1j * np.exp(-1j * beta) * s], [-1j * np.exp(1j * beta) * s, c]], dtype=complex, ) assert np.allclose(gate.get_unitary(), expected, atol=1e-6) - def test_r_zero_theta_is_identity(self): - """R(0, phi) is identity for any phi.""" - for phi in [0.0, 1.0, np.pi]: - gate = RGate(0.0, phi) + def test_zero_alpha_is_identity(self): + """PRX(0, beta) is identity for any beta.""" + for beta in [0.0, 1.0, np.pi]: + gate = PRXGate(0.0, beta) assert np.allclose(gate.get_unitary(), np.eye(2), atol=1e-6) - def test_r_bind_parameters_keeps_r_gate(self): - """Binding symbolic parameters returns an RGate with params [theta, phi], not a plain U3Gate.""" + +class TestRGate: + """Tests for the deprecated RGate alias of PRXGate.""" + + def test_r_is_deprecated_alias_of_prx(self): + """RGate(theta, phi) warns and builds PRXGate(theta, phi).""" + theta, phi = 1.2, 0.8 + with pytest.warns(QrispDeprecationWarning, match="RGate is deprecated"): + gate = RGate(theta, phi) + assert isinstance(gate, PRXGate) + assert gate.name == "prx" + assert (gate.alpha, gate.beta) == (theta, phi) + assert np.allclose(gate.get_unitary(), PRXGate(theta, phi).get_unitary()) + + def test_r_bind_parameters_returns_prx_gate(self): + """Binding symbolic parameters returns a PRXGate, not a plain U3Gate.""" theta, phi = sympy.symbols("theta phi") gate = RGate(theta, phi).bind_parameters({theta: 1.2, phi: 0.7}) - assert isinstance(gate, RGate) - assert gate.params == [1.2, 0.7] - assert np.allclose(gate.get_unitary(), RGate(1.2, 0.7).get_unitary()) + assert isinstance(gate, PRXGate) + assert np.allclose([gate.alpha, gate.beta], [1.2, 0.7]) + assert np.allclose(gate.get_unitary(), PRXGate(1.2, 0.7).get_unitary()) class TestCPGate: diff --git a/tests/circuit_tests/passes/test_PRXGate.py b/tests/circuit_tests/passes/test_PRXGate.py deleted file mode 100644 index aeedf4b145..0000000000 --- a/tests/circuit_tests/passes/test_PRXGate.py +++ /dev/null @@ -1,136 +0,0 @@ -# ******************************************************************************** -# * Copyright (c) 2026 the Qrisp authors -# * -# * This program and the accompanying materials are made available under the -# * terms of the Eclipse Public License 2.0 which is available at -# * http://www.eclipse.org/legal/epl-2.0. -# * -# * This Source Code may also be made available under the following Secondary -# * Licenses when the conditions for such availability set forth in the Eclipse -# * Public License, v. 2.0 are satisfied: GNU General Public License, version 2 -# * with the GNU Classpath Exception which is -# * available at https://www.gnu.org/software/classpath/license.html. -# * -# * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 -# ******************************************************************************** - -"""Tests for PRXGate construction, inverse, and unitary in qrisp.circuit.pass_management.passes.convert_to_prx.""" - -import numpy as np -import sympy - -from qrisp import U3Gate -from qrisp.circuit import PRXGate - - -class TestPRXGateConstruction: - """Tests for PRXGate parameter mapping and U3 inheritance.""" - - def test_u3_parameter_mapping(self): - """PRX(alpha, beta) maps to U3(alpha, beta - pi/2, pi/2 - beta).""" - alpha, beta = np.pi / 2, np.pi / 4 - prx = PRXGate(alpha, beta) - - assert prx.alpha == alpha - assert prx.beta == beta - assert abs(prx.theta - alpha) < 1e-10 - assert abs(prx.phi - (beta - np.pi / 2)) < 1e-10 - assert abs(prx.lam - (np.pi / 2 - beta)) < 1e-10 - - def test_is_subclass_of_U3Gate(self): - """PRXGate must inherit from U3Gate.""" - prx = PRXGate(0.5, 0.3) - assert isinstance(prx, U3Gate) - assert issubclass(PRXGate, U3Gate) - - def test_name_is_prx(self): - assert PRXGate(0.1, 0.2).name == "prx" - - def test_u3_equivalence(self): - """PRXGate parameters should reproduce an equivalent U3Gate.""" - alpha, beta = np.pi / 2, np.pi / 4 - prx = PRXGate(alpha, beta) - u3 = U3Gate(alpha, beta - np.pi / 2, np.pi / 2 - beta) - assert abs(prx.theta - u3.theta) < 1e-10 - assert abs(prx.phi - u3.phi) < 1e-10 - assert abs(prx.lam - u3.lam) < 1e-10 - - -class TestPRXGateEdgeCases: - """Parameter edge cases: zero, negative, and large values.""" - - def test_zero_parameters(self): - prx = PRXGate(0, 0) - assert prx.alpha == 0 - assert prx.beta == 0 - assert abs(prx.phi - (-np.pi / 2)) < 1e-10 - assert abs(prx.lam - np.pi / 2) < 1e-10 - - def test_negative_parameters(self): - prx = PRXGate(-np.pi / 3, -np.pi / 6) - assert prx.alpha == -np.pi / 3 - assert prx.beta == -np.pi / 6 - - def test_large_parameters(self): - alpha = 2 * np.pi + np.pi / 2 - beta = 3 * np.pi - prx = PRXGate(alpha, beta) - assert prx.alpha == alpha - assert prx.beta == beta - - -class TestPRXGateInverse: - """Tests for the inverse() method.""" - - def test_inverse_returns_prxgate(self): - prx = PRXGate(0.7, 1.2) - inv = prx.inverse() - assert isinstance(inv, PRXGate) - assert inv.name == "prx" - - def test_inverse_negates_alpha_preserves_beta(self): - alpha, beta = 0.7, 1.2 - prx = PRXGate(alpha, beta) - inv = prx.inverse() - assert abs(inv.alpha - (-alpha)) < 1e-10 - assert abs(inv.beta - beta) < 1e-10 - - def test_double_inverse_is_identity(self): - prx = PRXGate(0.7, 1.2) - inv_inv = prx.inverse().inverse() - assert abs(inv_inv.alpha - prx.alpha) < 1e-10 - assert abs(inv_inv.beta - prx.beta) < 1e-10 - - -class TestPRXGateBindParameters: - """Tests for binding symbolic PRXGate parameters.""" - - def test_bind_parameters_keeps_prx_gate(self): - """Binding symbolic parameters returns a PRXGate with bound alpha and beta, not a plain U3Gate.""" - alpha, beta = sympy.symbols("alpha beta") - gate = PRXGate(alpha, beta).bind_parameters({alpha: 0.7, beta: 1.2}) - assert isinstance(gate, PRXGate) - assert np.isclose(gate.alpha, 0.7) - assert np.isclose(gate.beta, 1.2) - assert np.allclose(gate.get_unitary(), PRXGate(0.7, 1.2).get_unitary()) - - -class TestPRXGateUnitary: - """Verify the PRXGate unitary against the IQM SDK definition.""" - - def test_matches_iqm_definition(self): - """PRXGate unitary must match IQM's PRX = RZ(b) RX(a) RZ(-b).""" - from qrisp import QuantumCircuit - - a, b = 0.7, 1.2 - qc = QuantumCircuit(1) - qc.append(PRXGate(a, b), [0]) - U_prx = qc.get_unitary() - - U_expected = np.array( - [ - [np.cos(a / 2), -1j * np.exp(-1j * b) * np.sin(a / 2)], - [-1j * np.exp(1j * b) * np.sin(a / 2), np.cos(a / 2)], - ] - ) - assert np.allclose(U_prx, U_expected, atol=1e-6) diff --git a/tests/circuit_tests/test_quantum_circuit.py b/tests/circuit_tests/test_quantum_circuit.py index 8dd036ec25..55ee74edf5 100644 --- a/tests/circuit_tests/test_quantum_circuit.py +++ b/tests/circuit_tests/test_quantum_circuit.py @@ -2480,12 +2480,23 @@ def test_u3_appends_one_instruction(self): # r # # ------------------------------------------------------------------ # - def test_r_appends_instruction_named_r(self): - """r() appends exactly one instruction named 'r'.""" + def test_prx_appends_instruction_named_prx(self): + """prx() appends exactly one instruction named 'prx'.""" qc = QuantumCircuit(1) - qc.r(np.pi / 4, np.pi / 4, 0) + qc.prx(np.pi / 4, np.pi / 4, 0) assert len(qc.data) == 1 - assert qc.data[0].op.name == "r" + assert qc.data[0].op.name == "prx" + + def test_r_method_is_deprecated(self): + """r() warns and appends the same PRX gate as prx().""" + from qrisp.misc.exceptions import QrispDeprecationWarning + + qc = QuantumCircuit(1) + with pytest.warns(QrispDeprecationWarning, match="QuantumCircuit.r is deprecated"): + qc.r(0.3, 1.1, 0) + assert len(qc.data) == 1 + assert qc.data[0].op.name == "prx" + assert (qc.data[0].op.alpha, qc.data[0].op.beta) == (0.3, 1.1) # ------------------------------------------------------------------ # # unitary # From bfadb509b55496751d8291bee5075d77ee39b8ce Mon Sep 17 00:00:00 2001 From: Omar Ibrahim Date: Tue, 10 Nov 2026 12:19:39 +0100 Subject: [PATCH 6/8] remove deaed code branch --- src/qrisp/interface/converter/qiskit_converter.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/qrisp/interface/converter/qiskit_converter.py b/src/qrisp/interface/converter/qiskit_converter.py index 9ebb97fa72..66ae4a3a23 100644 --- a/src/qrisp/interface/converter/qiskit_converter.py +++ b/src/qrisp/interface/converter/qiskit_converter.py @@ -243,8 +243,6 @@ def create_qiskit_instruction(op, params=[]): qiskit_ins = qsk_gates.TGate().inverse() elif op.name == "u3": qiskit_ins = qsk_gates.U3Gate(*params) - elif op.name == "r": - qiskit_ins = qsk_gates.RGate(*params) elif isinstance(op, PRXGate): qiskit_ins = qsk_gates.RGate(op.alpha, op.beta) elif op.name == "id": @@ -363,9 +361,7 @@ def convert_from_qiskit(qiskit_qc): # Qiskit RGate → PRXGate if qiskit_op.name == "r": - from qrisp.circuit import PRXGate as _PRXGate - - op = _PRXGate(params[0], params[1]) + op = PRXGate(params[0], params[1]) else: try: op = op_dic[qiskit_op.name](*params) From 3bfc4ec8e3acd3733ca738af0452285036f4a573 Mon Sep 17 00:00:00 2001 From: Omar Ibrahim Date: Tue, 10 Nov 2026 13:12:17 +0100 Subject: [PATCH 7/8] Fix PennyLane conversion of controlled PRXGate --- .../converter/pennylane_converter.py | 3 ++- .../test_converter_qrisp_to_qml.py | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/qrisp/interface/converter/pennylane_converter.py b/src/qrisp/interface/converter/pennylane_converter.py index bc27432372..7175eeb134 100644 --- a/src/qrisp/interface/converter/pennylane_converter.py +++ b/src/qrisp/interface/converter/pennylane_converter.py @@ -156,7 +156,8 @@ def _process_qrisp_circuit(qc: QuantumCircuit | QuantumSession, wire_map: dict, n_ctrls = len(op.ctrl_state) if is_controlled else 0 controls, targets = qml_wires[:n_ctrls], qml_wires[n_ctrls:] - qml_params = qml_gate_desc.param_fn(op) + # param_fn is keyed by the base gate's name, so it reads the base gate's attributes (e.g. PRXGate.alpha) + qml_params = qml_gate_desc.param_fn(op.base_operation if is_controlled else op) if subs_dic is not None: qml_params = _evaluate_abstract_params(qml_params, subs_dic) diff --git a/tests/interface_tests/test_converter_qrisp_to_qml.py b/tests/interface_tests/test_converter_qrisp_to_qml.py index bb686c06ff..110b9553fe 100644 --- a/tests/interface_tests/test_converter_qrisp_to_qml.py +++ b/tests/interface_tests/test_converter_qrisp_to_qml.py @@ -34,6 +34,7 @@ MCRXGate, MCXGate, PGate, + PRXGate, RGate, RXGate, RXXGate, @@ -735,6 +736,29 @@ def test_custom_controlled_gate(self, gate_class, qml_gate_class): check_statevector_equivalence(qrisp_qv, qml_res) + def test_controlled_prx_gate(self): + """A controlled PRXGate reads alpha and beta from its base gate and converts to a controlled qml.Rot.""" + alpha, beta = 0.7, 1.2 + qrisp_qv = QuantumVariable(3) + qrisp_qv.qs.append( + PRXGate(alpha, beta).control(num_ctrl_qubits=2, ctrl_state="01"), + [qrisp_qv[0], qrisp_qv[1], qrisp_qv[2]], + ) + + qml_converted_circuit = _create_qml_circuit(qrisp_qv) + qml_res = qml_converted_circuit() + + expected_ops = [ + qml.ctrl( + op=qml.Rot(np.pi / 2 - beta, alpha, beta - np.pi / 2, wires=qrisp_qv[2].identifier), + control=[qrisp_qv[0].identifier, qrisp_qv[1].identifier], + control_values=[0, 1], + ) + ] + check_qml_operations(qml_converted_circuit, expected_ops) + + check_statevector_equivalence(qrisp_qv, qml_res) + def test_controlled_adjoint_gate(self): """Test conversion of controlled adjoint gates from Qrisp to PennyLane.""" qrisp_qv = QuantumVariable(4) From 7d6356ef0894ce910a84e81651ccb525b1b400b7 Mon Sep 17 00:00:00 2001 From: Omar Ibrahim Date: Tue, 10 Nov 2026 16:46:01 +0100 Subject: [PATCH 8/8] fix changelog bug --- documentation/source/general/changelog/changelog-dev.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/documentation/source/general/changelog/changelog-dev.rst b/documentation/source/general/changelog/changelog-dev.rst index 683b6335c0..202842be62 100644 --- a/documentation/source/general/changelog/changelog-dev.rst +++ b/documentation/source/general/changelog/changelog-dev.rst @@ -618,6 +618,7 @@ Bug Fixes in PennyLane. - ``U1Gate`` now converts to Qiskit's ``RZGate`` instead of ``PhaseGate``, since Qrisp's ``U1`` is an RZ rotation. + (`PR #949 `_). Compatibility @@ -640,6 +641,7 @@ Compatibility above 63, for which the number of calls of the unitary overflowed a 64-bit integer (`PR #932 `_). + * ``RGate`` and ``QuantumCircuit.r`` are deprecated in favor of the identical ``PRXGate`` and the new ``QuantumCircuit.prx``. Both still work but emit a ``QrispDeprecationWarning``. ``RGate(theta, phi)`` now builds a