diff --git a/python/pecos-rslib/pecos_rslib.pyi b/python/pecos-rslib/pecos_rslib.pyi index 5b17af6fa..0dd11ea31 100644 --- a/python/pecos-rslib/pecos_rslib.pyi +++ b/python/pecos-rslib/pecos_rslib.pyi @@ -1770,6 +1770,7 @@ class GateType: RXX: GateType RYY: GateType RZZ: GateType + RXYXY2Q: GateType RXY1Q: GateType U: GateType F: GateType @@ -1836,6 +1837,8 @@ class Gate: @staticmethod def cz(pairs: Sequence[tuple[int, int]]) -> Gate: ... @staticmethod + def rxyxy2q(theta: Any, phi: Any, pairs: Sequence[tuple[int, int]]) -> Gate: ... + @staticmethod def mx(qubits: Sequence[int]) -> Gate: ... @staticmethod def mz(qubits: Sequence[int]) -> Gate: ... @@ -1923,6 +1926,7 @@ class TickHandle: def rxx(self, theta: Any, pairs: Sequence[tuple[int, int]]) -> TickHandle: ... def ryy(self, theta: Any, pairs: Sequence[tuple[int, int]]) -> TickHandle: ... def rzz(self, theta: Any, pairs: Sequence[tuple[int, int]]) -> TickHandle: ... + def rxyxy2q(self, theta: Any, phi: Any, pairs: Sequence[tuple[int, int]]) -> TickHandle: ... def add_gate( self, name: str, diff --git a/python/pecos-rslib/src/dag_circuit_bindings.rs b/python/pecos-rslib/src/dag_circuit_bindings.rs index 2fc52e971..93e62567c 100644 --- a/python/pecos-rslib/src/dag_circuit_bindings.rs +++ b/python/pecos-rslib/src/dag_circuit_bindings.rs @@ -442,6 +442,14 @@ impl PyGateType { } } + #[classattr] + #[pyo3(name = "RXYXY2Q")] + fn rxyxy2q_attr() -> Self { + Self { + inner: GateType::RXYXY2Q, + } + } + #[classattr] #[pyo3(name = "RXY1Q")] fn rxy1q() -> Self { @@ -901,6 +909,14 @@ impl PyGate { } } + /// Create an RXYXY2Q gate. + #[staticmethod] + fn rxyxy2q(theta: AngleParam, phi: AngleParam, pairs: Vec<(usize, usize)>) -> Self { + Self { + inner: Gate::rxyxy2q(theta.0, phi.0, &pairs), + } + } + /// Create an RXY1Q gate. #[staticmethod] fn rxy1q(theta: AngleParam, phi: AngleParam, qubits: Vec) -> Self { @@ -3633,6 +3649,19 @@ impl PyTickHandle { Ok(slf) } + /// Apply an RXYXY2Q rotation. + fn rxyxy2q( + slf: Py, + py: Python<'_>, + theta: AngleParam, + phi: AngleParam, + pairs: Vec<(usize, usize)>, + ) -> PyResult> { + slf.borrow_mut(py) + .add_gate_internal(py, Gate::rxyxy2q(theta.0, phi.0, &pairs))?; + Ok(slf) + } + // --- Generic gate dispatch (name-based) --- /// Add a gate by name, resolving to a native `GateType` if possible. diff --git a/python/quantum-pecos/src/pecos/_qis_trace_replay.py b/python/quantum-pecos/src/pecos/_qis_trace_replay.py index a991079ff..f9b58bd48 100644 --- a/python/quantum-pecos/src/pecos/_qis_trace_replay.py +++ b/python/quantum-pecos/src/pecos/_qis_trace_replay.py @@ -196,6 +196,13 @@ def tuple_args(payload: object, op_name: str, arity: int) -> tuple[Any, ...]: float(theta), [(mapped_slot(int(qubit_a), op_name), mapped_slot(int(qubit_b), op_name))], ) + elif op_name == "RXYXY2Q": + theta, phi, qubit_a, qubit_b = tuple_args(payload, op_name, 4) + tick.rxyxy2q( + float(theta), + float(phi), + [(mapped_slot(int(qubit_a), op_name), mapped_slot(int(qubit_b), op_name))], + ) elif op_name in {"Measure", "MeasureLeaked"}: program_id, result_id = tuple_args(payload, op_name, 2) measurement_qubit = mapped_slot(int(program_id), op_name) @@ -440,6 +447,9 @@ def _replay_lowered_qis_trace_into_tick_circuit( elif gate_type == "RZZ": (theta,) = _require_gate_angles(angles, gate_type, 1) tick.rzz(theta, _gate_pairs(qubits, gate_type)) + elif gate_type == "RXYXY2Q": + theta, phi = _require_gate_angles(angles, gate_type, 2) + tick.rxyxy2q(theta, phi, _gate_pairs(qubits, gate_type)) elif gate_type == "CCX": tick.ccx(_gate_triples(qubits, gate_type)) else: diff --git a/python/quantum-pecos/src/pecos/noise/noise_impl/gate_groups.py b/python/quantum-pecos/src/pecos/noise/noise_impl/gate_groups.py index 8f473efc0..82b8755e8 100644 --- a/python/quantum-pecos/src/pecos/noise/noise_impl/gate_groups.py +++ b/python/quantum-pecos/src/pecos/noise/noise_impl/gate_groups.py @@ -32,6 +32,7 @@ "RXX", "RYY", "RZZ", + "RXYXY2Q", "CX", "SXX", "SXXdg", diff --git a/python/quantum-pecos/src/pecos/noise/noise_impl_old/gate_groups.py b/python/quantum-pecos/src/pecos/noise/noise_impl_old/gate_groups.py index e52ac9224..bde4e75ac 100644 --- a/python/quantum-pecos/src/pecos/noise/noise_impl_old/gate_groups.py +++ b/python/quantum-pecos/src/pecos/noise/noise_impl_old/gate_groups.py @@ -32,6 +32,7 @@ "RXX", "RYY", "RZZ", + "RXYXY2Q", "CX", "SXX", "SXXdg", diff --git a/python/quantum-pecos/src/pecos/quantum/gate_groups.py b/python/quantum-pecos/src/pecos/quantum/gate_groups.py index 85c574765..3b2b72956 100644 --- a/python/quantum-pecos/src/pecos/quantum/gate_groups.py +++ b/python/quantum-pecos/src/pecos/quantum/gate_groups.py @@ -29,6 +29,7 @@ "RXX", "RYY", "RZZ", + "RXYXY2Q", "RXXRYYRZZ", } diff --git a/python/quantum-pecos/tests/pecos/test_tracing.py b/python/quantum-pecos/tests/pecos/test_tracing.py index 34fd62cd7..20c56a701 100644 --- a/python/quantum-pecos/tests/pecos/test_tracing.py +++ b/python/quantum-pecos/tests/pecos/test_tracing.py @@ -8,6 +8,7 @@ import pecos import pecos_rslib import pytest +from pecos._qis_trace_replay import _replay_qis_trace_into_tick_circuit from pecos.quantum import TickCircuit from pecos.simulators import StateVec @@ -142,6 +143,81 @@ def test_qis_trace_crz_preserves_full_matrix() -> None: assert abs(columns[column][row] - reference[row][column]) < 1e-12 +@pytest.mark.parametrize("lowered", [False, True], ids=["raw", "runtime-lowered"]) +def test_qis_trace_rxyxy2q_preserves_angles_and_pair(lowered: bool) -> None: + operations = [ + {"AllocateQubit": {"id": 7}}, + {"AllocateQubit": {"id": 11}}, + {"AllocateQubit": {"id": 3}}, + {"Quantum": {"RXYXY2Q": [-0.73, 0.41, 3, 7]}}, + ] + if lowered: + trace = _completed_trace() + trace[0]["operations"] = operations + trace[0]["num_operations"] = len(operations) + trace[0]["lowered_quantum_ops"] = [ + { + "gate_type": "RXYXY2Q", + "qubits": [2, 0], + "angles": [-0.73, 0.41], + "params": [], + "metadata": {"source_label": "xyxy"}, + }, + ] + circuit = pecos.qis_operation_trace_to_tick_circuit(trace) + assert circuit.get_gate_meta(0, 0, "source_label") == "xyxy" + else: + circuit = _replay_qis_trace_into_tick_circuit(operations) + + gates = [gate for _, gate in circuit.gate_batches() if gate.gate_type.name != "PZ"] + assert len(gates) == 1 + assert gates[0].gate_type.name == "RXYXY2Q" + # Gate.angles exposes Angle64's unsigned radians; compare signed representatives. + assert [math.remainder(angle, math.tau) for angle in gates[0].angles] == pytest.approx([-0.73, 0.41]) + assert gates[0].qubits == [2, 0] + + +@pytest.mark.parametrize("typed_angles", [False, True], ids=["float", "angle64"]) +def test_tick_circuit_rxyxy2q_constructor(typed_angles: bool) -> None: + theta, phi = -0.73, 0.41 + if typed_angles: + theta, phi = (pecos_rslib.angle64.from_radians(angle) for angle in (theta, phi)) + circuit = TickCircuit() + tick = circuit.tick() + assert tick.rxyxy2q(theta, phi, [(5, 2)]) is tick + assert circuit.gate_count() == 1 + gates = [circuit.get_tick(0).gate_batches()[0], pecos_rslib.Gate.rxyxy2q(theta, phi, [(5, 2)])] + for gate in gates: + assert gate.gate_type.name == "RXYXY2Q" + assert gate.gate_type == pecos_rslib.GateType.RXYXY2Q + assert [math.remainder(angle, math.tau) for angle in gate.angles] == pytest.approx([-0.73, 0.41]) + assert gate.qubits == [5, 2] + + +@pytest.mark.parametrize("lowered", [False, True], ids=["raw", "runtime-lowered"]) +def test_qis_trace_unknown_gate_still_fails(lowered: bool) -> None: + if lowered: + trace = _completed_trace() + trace[0]["lowered_quantum_ops"][1]["gate_type"] = "UnknownGate" + with pytest.raises(ValueError, match=r"Unsupported.*UnknownGate"): + pecos.qis_operation_trace_to_tick_circuit(trace) + else: + with pytest.raises(ValueError, match=r"Unsupported.*UnknownGate"): + _replay_qis_trace_into_tick_circuit([{"Quantum": {"UnknownGate": 0}}]) + + +def test_qis_trace_rxyxy2q_rejects_incomplete_pair() -> None: + trace = _completed_trace() + trace[0]["lowered_quantum_ops"][1] = { + "gate_type": "RXYXY2Q", + "qubits": [2], + "angles": [-0.73, 0.41], + "params": [], + } + with pytest.raises(ValueError, match=r"RXYXY2Q.*expected an even number of qubits"): + pecos.qis_operation_trace_to_tick_circuit(trace) + + def test_tracing_apis_are_exported_at_top_level() -> None: assert pecos.capture_qis_operation_trace is pecos.tracing.capture_qis_operation_trace assert pecos.qis_operation_trace_to_tick_circuit is pecos.tracing.qis_operation_trace_to_tick_circuit @@ -286,6 +362,9 @@ def test_qis_operation_trace_conversion_rejects_boolean_framing_counts() -> None ("RXY1Q", [0.5]), ("CRZ", []), ("RZZ", []), + ("RXYXY2Q", []), + ("RXYXY2Q", [0.5]), + ("RXYXY2Q", [0.5, 0.25, 0.75]), ], ) def test_qis_operation_trace_conversion_rejects_invalid_angle_arity( @@ -295,7 +374,7 @@ def test_qis_operation_trace_conversion_rejects_invalid_angle_arity( trace = _completed_trace() trace[0]["lowered_quantum_ops"][1] = { "gate_type": gate_type, - "qubits": [0, 1] if gate_type in {"CRZ", "RZZ"} else [0], + "qubits": [0, 1] if gate_type in {"CRZ", "RZZ", "RXYXY2Q"} else [0], "angles": angles, "params": [], }