From f8e7478811583627b9eff7c8f6a95e1067eef475 Mon Sep 17 00:00:00 2001 From: Jake Arkinstall <65358059+jake-arkinstall@users.noreply.github.com> Date: Thu, 17 Sep 2026 14:16:40 +0100 Subject: [PATCH 1/5] Add RPP gate --- crates/pecos-core/src/gate_type.rs | 12 ++++++-- crates/pecos-core/src/gates.rs | 14 ++++++++++ .../pecos-engines/src/byte_message/builder.rs | 14 ++++++++++ .../src/noise/biased_depolarizing.rs | 1 + .../pecos-engines/src/noise/depolarizing.rs | 1 + crates/pecos-engines/src/quantum.rs | 21 ++++++++++++++ crates/pecos-qasm/src/engine.rs | 2 +- crates/pecos-quantum/src/circuit_display.rs | 2 ++ crates/pecos-quantum/src/unitary_matrix.rs | 1 + .../src/arbitrary_rotation_gateable.rs | 18 ++++++++++++ .../pecos-simulators/src/circuit_executor.rs | 3 ++ .../pecos-simulators/src/clifford_rotation.rs | 28 +++++++++++++++++++ 12 files changed, 113 insertions(+), 4 deletions(-) diff --git a/crates/pecos-core/src/gate_type.rs b/crates/pecos-core/src/gate_type.rs index 75302fa1f..80c1944af 100644 --- a/crates/pecos-core/src/gate_type.rs +++ b/crates/pecos-core/src/gate_type.rs @@ -82,6 +82,8 @@ pub enum GateType { RXXRYYRZZ = 83, /// General 2-qubit unitary via KAK decomposition U2q = 84, + /// exp(-i theta/2 P(phi) tensor P(phi)), where P(phi) = cos(phi) X + sin(phi) Y. + RPP = 85, /// Toffoli gate (CCX, 3 qubits) CCX = 90, @@ -462,6 +464,7 @@ impl TryFrom for GateType { 82 => GateType::RZZ, 83 => GateType::RXXRYYRZZ, 84 => GateType::U2q, + 85 => GateType::RPP, 90 => GateType::CCX, 100 => GateType::MX, 104 => GateType::MZ, @@ -646,7 +649,7 @@ impl GateType { | GateType::Idle => 1, // Gates with two parameters - GateType::RXY1Q => 2, + GateType::RXY1Q | GateType::RPP => 2, // Gates with three parameters GateType::U | GateType::RXXRYYRZZ => 3, @@ -722,7 +725,8 @@ impl GateType { | GateType::RYY | GateType::RZZ | GateType::RXXRYYRZZ - | GateType::U2q => 2, + | GateType::U2q + | GateType::RPP => 2, // Three-qubit gates GateType::CCX => 3, @@ -766,7 +770,7 @@ impl GateType { | GateType::RXX | GateType::RYY | GateType::RZZ => 1, - GateType::RXY1Q => 2, + GateType::RXY1Q | GateType::RPP => 2, GateType::U | GateType::RXXRYYRZZ => 3, GateType::U2q => 15, // All other gates have no angle parameters @@ -825,6 +829,7 @@ impl fmt::Display for GateType { GateType::Tdg => write!(f, "Tdg"), GateType::U => write!(f, "U"), GateType::RXY1Q => write!(f, "RXY1Q"), + GateType::RPP => write!(f, "RPP"), GateType::CX => write!(f, "CX"), GateType::CY => write!(f, "CY"), GateType::CZ => write!(f, "CZ"), @@ -896,6 +901,7 @@ impl std::str::FromStr for GateType { "RY" => Ok(GateType::RY), "RZ" => Ok(GateType::RZ), "RXY1Q" | "R1XY" => Ok(GateType::RXY1Q), + "RPP" => Ok(GateType::RPP), "U" => Ok(GateType::U), "CX" | "CNOT" => Ok(GateType::CX), "CY" => Ok(GateType::CY), diff --git a/crates/pecos-core/src/gates.rs b/crates/pecos-core/src/gates.rs index 4e7f84802..0a09f2c4d 100644 --- a/crates/pecos-core/src/gates.rs +++ b/crates/pecos-core/src/gates.rs @@ -884,6 +884,20 @@ impl Gate { ) } + /// Create RPP(theta, phi) gates on qubit pairs. + #[must_use] + pub fn rpp( + theta: Angle64, + phi: Angle64, + pairs: &[(impl Into + Copy, impl Into + Copy)], + ) -> Self { + Self::with_angles( + GateType::RPP, + smallvec::smallvec![theta, phi], + Self::flatten_qubit_pairs(pairs), + ) + } + /// Create U gate on multiple qubits #[must_use] pub fn u( diff --git a/crates/pecos-engines/src/byte_message/builder.rs b/crates/pecos-engines/src/byte_message/builder.rs index 79b74bf4c..6a7d8ec7b 100644 --- a/crates/pecos-engines/src/byte_message/builder.rs +++ b/crates/pecos-engines/src/byte_message/builder.rs @@ -539,6 +539,20 @@ impl ByteMessageBuilder { self.add_gate_parts(GateType::RXY1Q, qubits, &[theta, phi], &[]) } + /// Add RPP(theta, phi) gates, with one shared XY-plane axis on both qubits. + pub fn rpp(&mut self, theta: Angle64, phi: Angle64, pairs: &[(usize, usize)]) -> &mut Self { + if let [(q0, q1)] = pairs { + return self.add_two_qubit_gate_parts(GateType::RPP, *q0, *q1, &[theta, phi], &[]); + } + self.add_gate_parts_from_usizes( + GateType::RPP, + pairs.len() * 2, + pairs.iter().copied().flat_map(|(q0, q1)| [q0, q1]), + &[theta, phi], + &[], + ) + } + /// Add a U gate pub fn u( &mut self, diff --git a/crates/pecos-engines/src/noise/biased_depolarizing.rs b/crates/pecos-engines/src/noise/biased_depolarizing.rs index 50dfe9359..8bb440707 100644 --- a/crates/pecos-engines/src/noise/biased_depolarizing.rs +++ b/crates/pecos-engines/src/noise/biased_depolarizing.rs @@ -197,6 +197,7 @@ impl BiasedDepolarizingNoiseModel { | GateType::SZZ | GateType::SZZdg | GateType::SWAP + | GateType::RPP | GateType::RXX | GateType::RYY | GateType::RZZ diff --git a/crates/pecos-engines/src/noise/depolarizing.rs b/crates/pecos-engines/src/noise/depolarizing.rs index a494d3455..8838b0946 100644 --- a/crates/pecos-engines/src/noise/depolarizing.rs +++ b/crates/pecos-engines/src/noise/depolarizing.rs @@ -213,6 +213,7 @@ impl DepolarizingNoiseModel { | GateType::SZZ | GateType::SZZdg | GateType::SWAP + | GateType::RPP | GateType::RXX | GateType::RYY | GateType::RZZ diff --git a/crates/pecos-engines/src/quantum.rs b/crates/pecos-engines/src/quantum.rs index b606538db..f80bd39d7 100644 --- a/crates/pecos-engines/src/quantum.rs +++ b/crates/pecos-engines/src/quantum.rs @@ -294,6 +294,12 @@ fn process_clifford_message { + with_flat_pairs(&cmd.qubits, &mut pair_scratch, |pairs| { + sim.try_rpp(cmd.angles[0], cmd.angles[1], pairs).map(|_| ()) + }) + .map_err(PecosError::Processing)?; + } GateType::RXY1Q => { sim.try_rxy1q(cmd.angles[0], cmd.angles[1], &cmd.qubits) .map_err(PecosError::Processing)?; @@ -530,6 +536,11 @@ fn process_general_message< sim.ryy(cmd.angles[0], pairs); }); } + GateType::RPP => { + with_flat_pairs(&cmd.qubits, &mut pair_scratch, |pairs| { + sim.rpp(cmd.angles[0], cmd.angles[1], pairs); + }); + } GateType::RXY1Q => { sim.rxy1q(cmd.angles[0], cmd.angles[1], &cmd.qubits); } @@ -1112,6 +1123,16 @@ where ); self.simulator.rz(angle, &cmd.qubits); } + GateType::RPP => { + if cmd.qubits.len() % 2 != 0 { + return Err(quantum_error(format!( + "RPP gate requires even number of qubits, got {}", + cmd.qubits.len() + ))); + } + let pairs = flat_to_pairs(&cmd.qubits); + self.simulator.rpp(cmd.angles[0], cmd.angles[1], &pairs); + } GateType::RXY1Q => { let theta = cmd.angles[0]; let phi = cmd.angles[1]; diff --git a/crates/pecos-qasm/src/engine.rs b/crates/pecos-qasm/src/engine.rs index 7f97f6ab5..98b3f35cd 100644 --- a/crates/pecos-qasm/src/engine.rs +++ b/crates/pecos-qasm/src/engine.rs @@ -689,7 +689,7 @@ impl QASMEngine { | GateType::CH | GateType::SWAP => self.process_two_qubit_gate(gate.gate_type, &qubits), // Gates not yet supported in QASM engine - GateType::CCX | GateType::PX | GateType::Channel => { + GateType::CCX | GateType::PX | GateType::Channel | GateType::RPP => { Err(PecosError::Processing(format!( "Gate type {:?} is not yet supported in the QASM engine", gate.gate_type diff --git a/crates/pecos-quantum/src/circuit_display.rs b/crates/pecos-quantum/src/circuit_display.rs index 4ef8fe90f..197a43bc8 100644 --- a/crates/pecos-quantum/src/circuit_display.rs +++ b/crates/pecos-quantum/src/circuit_display.rs @@ -48,6 +48,7 @@ fn gate_symbol(gate_type: GateType) -> &'static str { GateType::RZ => "RZ", GateType::U => "U", GateType::RXY1Q => "RXY1Q", + GateType::RPP => "RPP", GateType::CX => "CX", GateType::CY => "CY", GateType::CZ => "CZ", @@ -223,6 +224,7 @@ fn gate_color(gate_type: GateType) -> CellColor { | GateType::SWAP | GateType::U | GateType::RXY1Q + | GateType::RPP | GateType::RXXRYYRZZ | GateType::U2q | GateType::TrackedPauliMeta => CellColor::None, diff --git a/crates/pecos-quantum/src/unitary_matrix.rs b/crates/pecos-quantum/src/unitary_matrix.rs index c52a39d13..505b36a91 100644 --- a/crates/pecos-quantum/src/unitary_matrix.rs +++ b/crates/pecos-quantum/src/unitary_matrix.rs @@ -1910,6 +1910,7 @@ fn gate_to_matrix(gate_type: GateType, qubits: &[usize], num_qubits: usize) -> D | GateType::RZZ | GateType::U | GateType::RXY1Q + | GateType::RPP | GateType::RXXRYYRZZ | GateType::U2q => { panic!( diff --git a/crates/pecos-simulators/src/arbitrary_rotation_gateable.rs b/crates/pecos-simulators/src/arbitrary_rotation_gateable.rs index decd4cd68..dbf9c466d 100644 --- a/crates/pecos-simulators/src/arbitrary_rotation_gateable.rs +++ b/crates/pecos-simulators/src/arbitrary_rotation_gateable.rs @@ -127,6 +127,24 @@ pub trait ArbitraryRotationGateable: CliffordGateable { .apply_global_phase(phase, qubits) } + /// Rotate by theta about P(phi) tensor P(phi), where P(phi) = cos(phi) X + sin(phi) Y. + /// + /// RPP = (Rz(phi) tensor Rz(phi)) Rxx(theta) (Rz(-phi) tensor Rz(-phi)). + /// At phi = pi, the principal-value convention gives both inverse Z + /// rotations the same sign. Each ion contributes a minus sign, so the + /// two-qubit global phase still cancels. + #[inline] + fn rpp(&mut self, theta: Angle64, phi: Angle64, pairs: &[(QubitId, QubitId)]) -> &mut Self { + // These basis changes implement one RPP gate inside the simulator. + // Noise models still see a single two-qubit gate. + for &(q0, q1) in pairs { + self.rz(-phi, &[q0, q1]); + self.rxx(theta, &[(q0, q1)]); + self.rz(phi, &[q0, q1]); + } + self + } + /// Applies an X-Y plane rotation gate with a specified angle and axis. /// /// `RXY1Q(theta, phi) = exp(-i*theta*(cos(phi) X + sin(phi) Y)/2)`, with matrix diff --git a/crates/pecos-simulators/src/circuit_executor.rs b/crates/pecos-simulators/src/circuit_executor.rs index ab87b708f..1a2f9ddbb 100644 --- a/crates/pecos-simulators/src/circuit_executor.rs +++ b/crates/pecos-simulators/src/circuit_executor.rs @@ -233,6 +233,9 @@ fn execute_gate_command( GateType::RYY => { sim.try_ryy(gate.angles[0], &flat_to_pairs(qubits))?; } + GateType::RPP => { + sim.try_rpp(gate.angles[0], gate.angles[1], &flat_to_pairs(qubits))?; + } GateType::RXY1Q => { sim.try_rxy1q(gate.angles[0], gate.angles[1], qubits)?; } diff --git a/crates/pecos-simulators/src/clifford_rotation.rs b/crates/pecos-simulators/src/clifford_rotation.rs index fc08a5add..f42a45287 100644 --- a/crates/pecos-simulators/src/clifford_rotation.rs +++ b/crates/pecos-simulators/src/clifford_rotation.rs @@ -93,6 +93,34 @@ pub trait CliffordRotation: CliffordGateable { pairs: &[(QubitId, QubitId)], ) -> Result<&mut Self, String>; + /// Apply RPP when its Rz/Rxx decomposition consists of Clifford rotations. + /// + /// # Errors + /// Returns an error before changing the state if an angle is unsupported. + fn try_rpp( + &mut self, + theta: Angle64, + phi: Angle64, + pairs: &[(QubitId, QubitId)], + ) -> Result<&mut Self, String> { + if theta == Angle64::ZERO { + return Ok(self); + } + if simplify_two_qubit_clifford(GateType::RXX, theta).is_none() + || pecos_core::try_simplify_rotation_snapped(GateType::RZ, phi).is_none() + { + return Err(format!( + "RPP(theta={theta}, phi={phi}) is not a supported Clifford rotation" + )); + } + for &(q0, q1) in pairs { + self.try_rz(-phi, &[q0, q1])?; + self.try_rxx(theta, &[(q0, q1)])?; + self.try_rz(phi, &[q0, q1])?; + } + Ok(self) + } + /// Try to apply RXY1Q(theta, phi). Succeeds when the combination maps to a /// named Clifford (identity, X, Y, SX, `SXdg`, SY, `SYdg`). /// From 281729810824e4a074104a6d36c9e7de25321b93 Mon Sep 17 00:00:00 2001 From: Jake Arkinstall <65358059+jake-arkinstall@users.noreply.github.com> Date: Thu, 17 Sep 2026 16:07:29 +0100 Subject: [PATCH 2/5] Address (relevant) copilot review comments and test failures --- crates/pecos-qasm/src/parser/native_gates.rs | 2 + .../src/arbitrary_rotation_gateable.rs | 89 ++++++++++++++++++ .../pecos-simulators/src/clifford_rotation.rs | 92 +++++++++++++++++++ exp/pecos-experimental/src/hugr_executor.rs | 1 + 4 files changed, 184 insertions(+) diff --git a/crates/pecos-qasm/src/parser/native_gates.rs b/crates/pecos-qasm/src/parser/native_gates.rs index 3659fb466..3bfe955f7 100644 --- a/crates/pecos-qasm/src/parser/native_gates.rs +++ b/crates/pecos-qasm/src/parser/native_gates.rs @@ -33,6 +33,7 @@ pub fn parse_native_gate(name: &str) -> Option { "RX" => Some(CoreGateType::RX), "RY" => Some(CoreGateType::RY), "RZZ" => Some(CoreGateType::RZZ), + "RPP" => Some(CoreGateType::RPP), "RXY1Q" | "R1XY" => Some(CoreGateType::RXY1Q), "U" => Some(CoreGateType::U), _ => None, @@ -106,6 +107,7 @@ mod tests { | CoreGateType::RXX | CoreGateType::RYY | CoreGateType::RXXRYYRZZ + | CoreGateType::RPP | CoreGateType::U2q | CoreGateType::CCX | CoreGateType::MX diff --git a/crates/pecos-simulators/src/arbitrary_rotation_gateable.rs b/crates/pecos-simulators/src/arbitrary_rotation_gateable.rs index dbf9c466d..d57a80dc8 100644 --- a/crates/pecos-simulators/src/arbitrary_rotation_gateable.rs +++ b/crates/pecos-simulators/src/arbitrary_rotation_gateable.rs @@ -534,6 +534,95 @@ mod tests { } } + #[test] + fn default_rpp_matches_matrix_exactly() { + use crate::StateVecSoA32; + use num_complex::Complex64; + + let angles = [ + Angle64::ZERO, + Angle64::from_radians(0.73), + Angle64::from_radians(-0.41), + Angle64::QUARTER_TURN, + Angle64::HALF_TURN, + Angle64::THREE_QUARTERS_TURN, + ]; + for theta in angles { + for phi in angles { + let c = (theta.to_radians_signed() / 2.0).cos(); + let s = (theta.to_radians_signed() / 2.0).sin(); + let minus_i_s = Complex64::new(0.0, -s); + let phase = Complex64::from_polar(1.0, 2.0 * phi.to_radians_signed()); + let zero = Complex64::new(0.0, 0.0); + let diagonal = Complex64::new(c, 0.0); + // For RPP, phi changes the coupling between |00> and |11>, + // but leaves the |01> and |10> coupling alone. At phi = pi, + // the principal-value Z rotations each introduce a minus + // sign, so the signs from the two qubits should cancel. + // We compare amplitudes directly so we can check that phase + // cancellation as well as the relative phases of the entries. + let columns = [ + [diagonal, zero, zero, minus_i_s * phase], + [zero, diagonal, minus_i_s, zero], + [zero, minus_i_s, diagonal, zero], + [minus_i_s * phase.conj(), zero, zero, diagonal], + ]; + for (basis, expected) in columns.iter().enumerate() { + let mut sim = StateVecSoA32::new(2); + for qubit in 0..2 { + if basis & (1 << qubit) != 0 { + sim.x(&[QubitId(qubit)]); + } + } + sim.rpp(theta, phi, &[(QubitId(0), QubitId(1))]); + for (row, expected_entry) in expected.iter().enumerate() { + let actual = sim.get_amplitude(row); + let actual = Complex64::new(f64::from(actual.re), f64::from(actual.im)); + assert!( + (actual - expected_entry).norm() < 3e-6, + "theta={theta}, phi={phi}, column={basis}, row={row}: expected {expected_entry}, got {actual}" + ); + } + } + } + } + } + + #[test] + fn default_rpp_batches_match_sequential_pairs() { + use crate::StateVecSoA32; + + let theta = Angle64::from_radians(0.73); + let phi = Angle64::from_radians(-0.41); + // Include disjoint, overlapping and reversed pairs, as well as an + // empty batch. Qubit 4 is a spectator in every case. + let batches: &[&[(QubitId, QubitId)]] = &[ + &[], + &[(QubitId(2), QubitId(0)), (QubitId(1), QubitId(3))], + &[(QubitId(2), QubitId(0)), (QubitId(0), QubitId(1))], + ]; + for pairs in batches { + let mut batched = StateVecSoA32::new(5); + let mut sequential = StateVecSoA32::new(5); + for sim in [&mut batched, &mut sequential] { + sim.h(&[QubitId(0), QubitId(3)]) + .sz(&[QubitId(0)]) + .x(&[QubitId(2), QubitId(4)]) + .cx(&[(QubitId(0), QubitId(4))]); + } + batched.rpp(theta, phi, pairs); + for &pair in *pairs { + sequential.rpp(theta, phi, &[pair]); + } + for basis in 0..32 { + assert!( + (batched.get_amplitude(basis) - sequential.get_amplitude(basis)).norm() < 3e-6, + "pairs={pairs:?}, basis={basis}" + ); + } + } + } + #[test] fn default_rxy1q_matches_documented_matrix_exactly() { use crate::StateVecSoA32; diff --git a/crates/pecos-simulators/src/clifford_rotation.rs b/crates/pecos-simulators/src/clifford_rotation.rs index f42a45287..ff5fedb31 100644 --- a/crates/pecos-simulators/src/clifford_rotation.rs +++ b/crates/pecos-simulators/src/clifford_rotation.rs @@ -595,6 +595,98 @@ mod tests { ); } + #[test] + fn try_rpp_matches_xx_and_yy_rotations() { + let angles = [ + Angle64::ZERO, + Angle64::QUARTER_TURN, + Angle64::HALF_TURN, + Angle64::THREE_QUARTERS_TURN, + ]; + let batches: &[&[(QubitId, QubitId)]] = &[ + &[], + &[(QubitId(2), QubitId(0))], + &[(QubitId(2), QubitId(0)), (QubitId(1), QubitId(3))], + &[(QubitId(2), QubitId(0)), (QubitId(0), QubitId(1))], + ]; + for theta in angles { + for (axis, phi) in angles.into_iter().enumerate() { + for pairs in batches { + let mut actual = SparseStab::new(5); + let mut expected = SparseStab::new(5); + for sim in [&mut actual, &mut expected] { + sim.h(&[QubitId(0), QubitId(3)]) + .sz(&[QubitId(0)]) + .x(&[QubitId(2), QubitId(4)]) + .cx(&[(QubitId(0), QubitId(4))]); + } + actual.try_rpp(theta, phi, pairs).unwrap(); + // At phi = 0 or pi, both factors are X up to sign, so + // their product is XX. At +/-pi/2, we get YY instead. + // Applying each pair separately also checks overlapping + // batches without relying on RXX or RYY batch behaviour. + for &pair in *pairs { + if axis % 2 == 0 { + expected.try_rxx(theta, &[pair]).unwrap(); + } else { + expected.try_ryy(theta, &[pair]).unwrap(); + } + } + assert_eq!( + (actual.stab_tableau(), actual.destab_tableau()), + (expected.stab_tableau(), expected.destab_tableau()), + "theta={theta}, phi={phi}, pairs={pairs:?}" + ); + } + } + } + } + + #[test] + fn try_rpp_zero_theta_accepts_arbitrary_axis() { + let mut sim = SparseStab::new(3); + sim.h(&qid(0)).sz(&qid(0)).cx(&[(QubitId(0), QubitId(2))]); + let before = (sim.stab_tableau(), sim.destab_tableau()); + + // With no rotation, phi doesn't matter. In particular, we shouldn't + // reject an axis that would require non-Clifford basis changes. + sim.try_rpp( + Angle64::ZERO, + Angle64::from_radians(0.123), + &[(QubitId(0), QubitId(1)), (QubitId(1), QubitId(2))], + ) + .unwrap(); + assert_eq!((sim.stab_tableau(), sim.destab_tableau()), before); + } + + #[test] + fn try_rpp_unsupported_angles_leave_state_unchanged() { + let eighth = Angle64::QUARTER_TURN / 2u64; + for (theta, phi) in [ + (eighth, Angle64::QUARTER_TURN), + (Angle64::QUARTER_TURN, eighth), + (Angle64::from_radians(0.123), Angle64::HALF_TURN), + (Angle64::HALF_TURN, Angle64::from_radians(0.123)), + ] { + let mut sim = SparseStab::new(3); + sim.h(&qid(0)).sz(&qid(1)).cx(&[(QubitId(0), QubitId(2))]); + let before = (sim.stab_tableau(), sim.destab_tableau()); + + // Even if one angle is supported, we should reject the other + // before applying any basis changes or touching either pair. + assert!( + sim.try_rpp( + theta, + phi, + &[(QubitId(0), QubitId(1)), (QubitId(1), QubitId(2))], + ) + .is_err(), + "theta={theta}, phi={phi}" + ); + assert_eq!((sim.stab_tableau(), sim.destab_tableau()), before); + } + } + #[test] fn lower_crz_pi_matches_sz_then_szzdg_tableau() { let mut lowered = SparseStab::new(2); diff --git a/exp/pecos-experimental/src/hugr_executor.rs b/exp/pecos-experimental/src/hugr_executor.rs index 6a91c13a3..b40067a40 100644 --- a/exp/pecos-experimental/src/hugr_executor.rs +++ b/exp/pecos-experimental/src/hugr_executor.rs @@ -324,6 +324,7 @@ where | GateType::RXX | GateType::RYY | GateType::RZZ + | GateType::RPP | GateType::RXXRYYRZZ | GateType::U2q | GateType::T From 8c87d492abd28d1b7c3b501e1a44b3a640046656 Mon Sep 17 00:00:00 2001 From: Jake Arkinstall <65358059+jake-arkinstall@users.noreply.github.com> Date: Thu, 17 Sep 2026 17:01:39 +0100 Subject: [PATCH 3/5] Rename to RXYXY2Q, add selene integration with rpp --- crates/pecos-core/src/gate_type.rs | 14 +- crates/pecos-core/src/gates.rs | 6 +- .../pecos-engines/src/byte_message/builder.rs | 8 +- .../src/noise/biased_depolarizing.rs | 2 +- .../pecos-engines/src/noise/depolarizing.rs | 2 +- crates/pecos-engines/src/quantum.rs | 15 +- crates/pecos-qasm/src/engine.rs | 2 +- crates/pecos-qasm/src/parser/native_gates.rs | 4 +- crates/pecos-qis-ffi-types/src/operations.rs | 1 + crates/pecos-qis/src/ccengine.rs | 51 ++++++ crates/pecos-qis/src/selene_runtime.rs | 173 ++++++++++++++++-- crates/pecos-quantum/src/circuit_display.rs | 4 +- crates/pecos-quantum/src/unitary_matrix.rs | 2 +- crates/pecos-selene-core/src/lib.rs | 21 ++- .../tests/adapter_conformance_test.rs | 55 ++++++ .../src/arbitrary_rotation_gateable.rs | 18 +- .../pecos-simulators/src/circuit_executor.rs | 4 +- .../pecos-simulators/src/clifford_rotation.rs | 18 +- exp/pecos-experimental/src/hugr_executor.rs | 2 +- .../pecos-selene-general-noise/README.md | 10 +- .../pecos-selene-general-noise/src/lib.rs | 130 ++++++++++++- .../src/simulator.rs | 10 + .../pecos-selene-mast/src/lib.rs | 83 ++++++++- .../pecos-selene-stab-mps/src/lib.rs | 83 ++++++++- .../pecos-selene-stab-vec/src/lib.rs | 83 ++++++++- .../pecos-selene-stabilizer/src/lib.rs | 143 ++++++++++++++- .../pecos-selene-statevec/src/lib.rs | 110 ++++++++++- 27 files changed, 965 insertions(+), 89 deletions(-) diff --git a/crates/pecos-core/src/gate_type.rs b/crates/pecos-core/src/gate_type.rs index 80c1944af..5e5371ce5 100644 --- a/crates/pecos-core/src/gate_type.rs +++ b/crates/pecos-core/src/gate_type.rs @@ -83,7 +83,7 @@ pub enum GateType { /// General 2-qubit unitary via KAK decomposition U2q = 84, /// exp(-i theta/2 P(phi) tensor P(phi)), where P(phi) = cos(phi) X + sin(phi) Y. - RPP = 85, + RXYXY2Q = 85, /// Toffoli gate (CCX, 3 qubits) CCX = 90, @@ -464,7 +464,7 @@ impl TryFrom for GateType { 82 => GateType::RZZ, 83 => GateType::RXXRYYRZZ, 84 => GateType::U2q, - 85 => GateType::RPP, + 85 => GateType::RXYXY2Q, 90 => GateType::CCX, 100 => GateType::MX, 104 => GateType::MZ, @@ -649,7 +649,7 @@ impl GateType { | GateType::Idle => 1, // Gates with two parameters - GateType::RXY1Q | GateType::RPP => 2, + GateType::RXY1Q | GateType::RXYXY2Q => 2, // Gates with three parameters GateType::U | GateType::RXXRYYRZZ => 3, @@ -726,7 +726,7 @@ impl GateType { | GateType::RZZ | GateType::RXXRYYRZZ | GateType::U2q - | GateType::RPP => 2, + | GateType::RXYXY2Q => 2, // Three-qubit gates GateType::CCX => 3, @@ -770,7 +770,7 @@ impl GateType { | GateType::RXX | GateType::RYY | GateType::RZZ => 1, - GateType::RXY1Q | GateType::RPP => 2, + GateType::RXY1Q | GateType::RXYXY2Q => 2, GateType::U | GateType::RXXRYYRZZ => 3, GateType::U2q => 15, // All other gates have no angle parameters @@ -829,7 +829,7 @@ impl fmt::Display for GateType { GateType::Tdg => write!(f, "Tdg"), GateType::U => write!(f, "U"), GateType::RXY1Q => write!(f, "RXY1Q"), - GateType::RPP => write!(f, "RPP"), + GateType::RXYXY2Q => write!(f, "RXYXY2Q"), GateType::CX => write!(f, "CX"), GateType::CY => write!(f, "CY"), GateType::CZ => write!(f, "CZ"), @@ -901,7 +901,7 @@ impl std::str::FromStr for GateType { "RY" => Ok(GateType::RY), "RZ" => Ok(GateType::RZ), "RXY1Q" | "R1XY" => Ok(GateType::RXY1Q), - "RPP" => Ok(GateType::RPP), + "RXYXY2Q" => Ok(GateType::RXYXY2Q), "U" => Ok(GateType::U), "CX" | "CNOT" => Ok(GateType::CX), "CY" => Ok(GateType::CY), diff --git a/crates/pecos-core/src/gates.rs b/crates/pecos-core/src/gates.rs index 0a09f2c4d..16d4f91f1 100644 --- a/crates/pecos-core/src/gates.rs +++ b/crates/pecos-core/src/gates.rs @@ -884,15 +884,15 @@ impl Gate { ) } - /// Create RPP(theta, phi) gates on qubit pairs. + /// Create RXYXY2Q(theta, phi) gates on qubit pairs. #[must_use] - pub fn rpp( + pub fn rxyxy2q( theta: Angle64, phi: Angle64, pairs: &[(impl Into + Copy, impl Into + Copy)], ) -> Self { Self::with_angles( - GateType::RPP, + GateType::RXYXY2Q, smallvec::smallvec![theta, phi], Self::flatten_qubit_pairs(pairs), ) diff --git a/crates/pecos-engines/src/byte_message/builder.rs b/crates/pecos-engines/src/byte_message/builder.rs index 6a7d8ec7b..5751bfc2f 100644 --- a/crates/pecos-engines/src/byte_message/builder.rs +++ b/crates/pecos-engines/src/byte_message/builder.rs @@ -539,13 +539,13 @@ impl ByteMessageBuilder { self.add_gate_parts(GateType::RXY1Q, qubits, &[theta, phi], &[]) } - /// Add RPP(theta, phi) gates, with one shared XY-plane axis on both qubits. - pub fn rpp(&mut self, theta: Angle64, phi: Angle64, pairs: &[(usize, usize)]) -> &mut Self { + /// Add RXYXY2Q(theta, phi) gates, with one shared XY-plane axis on both qubits. + pub fn rxyxy2q(&mut self, theta: Angle64, phi: Angle64, pairs: &[(usize, usize)]) -> &mut Self { if let [(q0, q1)] = pairs { - return self.add_two_qubit_gate_parts(GateType::RPP, *q0, *q1, &[theta, phi], &[]); + return self.add_two_qubit_gate_parts(GateType::RXYXY2Q, *q0, *q1, &[theta, phi], &[]); } self.add_gate_parts_from_usizes( - GateType::RPP, + GateType::RXYXY2Q, pairs.len() * 2, pairs.iter().copied().flat_map(|(q0, q1)| [q0, q1]), &[theta, phi], diff --git a/crates/pecos-engines/src/noise/biased_depolarizing.rs b/crates/pecos-engines/src/noise/biased_depolarizing.rs index 8bb440707..e8fb50ceb 100644 --- a/crates/pecos-engines/src/noise/biased_depolarizing.rs +++ b/crates/pecos-engines/src/noise/biased_depolarizing.rs @@ -197,7 +197,7 @@ impl BiasedDepolarizingNoiseModel { | GateType::SZZ | GateType::SZZdg | GateType::SWAP - | GateType::RPP + | GateType::RXYXY2Q | GateType::RXX | GateType::RYY | GateType::RZZ diff --git a/crates/pecos-engines/src/noise/depolarizing.rs b/crates/pecos-engines/src/noise/depolarizing.rs index 8838b0946..4689ba2f5 100644 --- a/crates/pecos-engines/src/noise/depolarizing.rs +++ b/crates/pecos-engines/src/noise/depolarizing.rs @@ -213,7 +213,7 @@ impl DepolarizingNoiseModel { | GateType::SZZ | GateType::SZZdg | GateType::SWAP - | GateType::RPP + | GateType::RXYXY2Q | GateType::RXX | GateType::RYY | GateType::RZZ diff --git a/crates/pecos-engines/src/quantum.rs b/crates/pecos-engines/src/quantum.rs index f80bd39d7..063f019d8 100644 --- a/crates/pecos-engines/src/quantum.rs +++ b/crates/pecos-engines/src/quantum.rs @@ -294,9 +294,10 @@ fn process_clifford_message { + GateType::RXYXY2Q => { with_flat_pairs(&cmd.qubits, &mut pair_scratch, |pairs| { - sim.try_rpp(cmd.angles[0], cmd.angles[1], pairs).map(|_| ()) + sim.try_rxyxy2q(cmd.angles[0], cmd.angles[1], pairs) + .map(|_| ()) }) .map_err(PecosError::Processing)?; } @@ -536,9 +537,9 @@ fn process_general_message< sim.ryy(cmd.angles[0], pairs); }); } - GateType::RPP => { + GateType::RXYXY2Q => { with_flat_pairs(&cmd.qubits, &mut pair_scratch, |pairs| { - sim.rpp(cmd.angles[0], cmd.angles[1], pairs); + sim.rxyxy2q(cmd.angles[0], cmd.angles[1], pairs); }); } GateType::RXY1Q => { @@ -1123,15 +1124,15 @@ where ); self.simulator.rz(angle, &cmd.qubits); } - GateType::RPP => { + GateType::RXYXY2Q => { if cmd.qubits.len() % 2 != 0 { return Err(quantum_error(format!( - "RPP gate requires even number of qubits, got {}", + "RXYXY2Q gate requires even number of qubits, got {}", cmd.qubits.len() ))); } let pairs = flat_to_pairs(&cmd.qubits); - self.simulator.rpp(cmd.angles[0], cmd.angles[1], &pairs); + self.simulator.rxyxy2q(cmd.angles[0], cmd.angles[1], &pairs); } GateType::RXY1Q => { let theta = cmd.angles[0]; diff --git a/crates/pecos-qasm/src/engine.rs b/crates/pecos-qasm/src/engine.rs index 98b3f35cd..5a0553b26 100644 --- a/crates/pecos-qasm/src/engine.rs +++ b/crates/pecos-qasm/src/engine.rs @@ -689,7 +689,7 @@ impl QASMEngine { | GateType::CH | GateType::SWAP => self.process_two_qubit_gate(gate.gate_type, &qubits), // Gates not yet supported in QASM engine - GateType::CCX | GateType::PX | GateType::Channel | GateType::RPP => { + GateType::CCX | GateType::PX | GateType::Channel | GateType::RXYXY2Q => { Err(PecosError::Processing(format!( "Gate type {:?} is not yet supported in the QASM engine", gate.gate_type diff --git a/crates/pecos-qasm/src/parser/native_gates.rs b/crates/pecos-qasm/src/parser/native_gates.rs index 3bfe955f7..954459bc3 100644 --- a/crates/pecos-qasm/src/parser/native_gates.rs +++ b/crates/pecos-qasm/src/parser/native_gates.rs @@ -33,7 +33,7 @@ pub fn parse_native_gate(name: &str) -> Option { "RX" => Some(CoreGateType::RX), "RY" => Some(CoreGateType::RY), "RZZ" => Some(CoreGateType::RZZ), - "RPP" => Some(CoreGateType::RPP), + "RXYXY2Q" => Some(CoreGateType::RXYXY2Q), "RXY1Q" | "R1XY" => Some(CoreGateType::RXY1Q), "U" => Some(CoreGateType::U), _ => None, @@ -107,7 +107,7 @@ mod tests { | CoreGateType::RXX | CoreGateType::RYY | CoreGateType::RXXRYYRZZ - | CoreGateType::RPP + | CoreGateType::RXYXY2Q | CoreGateType::U2q | CoreGateType::CCX | CoreGateType::MX diff --git a/crates/pecos-qis-ffi-types/src/operations.rs b/crates/pecos-qis-ffi-types/src/operations.rs index 5cc443b61..2c7a3cd4d 100644 --- a/crates/pecos-qis-ffi-types/src/operations.rs +++ b/crates/pecos-qis-ffi-types/src/operations.rs @@ -99,6 +99,7 @@ pub enum QuantumOp { // ZZ interaction ZZ(usize, usize), RZZ(f64, usize, usize), + RXYXY2Q(f64, f64, usize, usize), // theta, phi, qubit_1, qubit_2 // Measurement Measure(usize, usize), // qubit, result_id diff --git a/crates/pecos-qis/src/ccengine.rs b/crates/pecos-qis/src/ccengine.rs index d49c4fb0e..4c0df5263 100644 --- a/crates/pecos-qis/src/ccengine.rs +++ b/crates/pecos-qis/src/ccengine.rs @@ -727,6 +727,17 @@ impl QisEngine { )]); Self::push_gate_metadata(&mut gate_metadata, &mut pending_metadata); } + QuantumOp::RXYXY2Q(theta, phi, qubit1, qubit2) => { + builder.rxyxy2q( + Angle64::from_radians(*theta), + Angle64::from_radians(*phi), + &[( + self.mapped_qubit(*qubit1, qop)?, + self.mapped_qubit(*qubit2, qop)?, + )], + ); + Self::push_gate_metadata(&mut gate_metadata, &mut pending_metadata); + } QuantumOp::RZZ(angle, qubit1, qubit2) => { builder.rzz( Angle64::from_radians(*angle), @@ -888,6 +899,14 @@ impl QisEngine { builder.szz(&[(qubit1, qubit2)]); gate_metadata.push(metadata); } + QuantumOp::RXYXY2Q(theta, phi, qubit1, qubit2) => { + builder.rxyxy2q( + Angle64::from_radians(theta), + Angle64::from_radians(phi), + &[(qubit1, qubit2)], + ); + gate_metadata.push(metadata); + } QuantumOp::RZZ(angle, qubit1, qubit2) => { builder.rzz(Angle64::from_radians(angle), &[(qubit1, qubit2)]); gate_metadata.push(metadata); @@ -2288,6 +2307,38 @@ mod tests { } } + #[test] + fn test_rxyxy2q_lowering_preserves_gate_and_metadata() { + let mut engine = QisEngine::with_runtime(Box::new(DummyRuntime::default())); + let metadata = TraceMetadata::from([("source_label".to_string(), "xyxy".to_string())]); + let source = QuantumOp::RXYXY2Q(-0.73, 0.41, 2, 0); + let direct = engine + .operations_to_lowered_commands(&[ + Operation::TraceMetadata { + metadata: metadata.clone(), + qubit: None, + }, + source.clone().into(), + ]) + .unwrap(); + let scheduled = engine + .quantum_ops_to_lowered_commands(vec![LoweredQuantumOp::new(source, metadata.clone())]) + .unwrap(); + // Direct operations use program handles, which are assigned slots + // in encounter order. Scheduled operations already use physical IDs. + for (batch, qubits) in [(direct, [0, 1]), (scheduled, [2, 0])] { + let gates = batch.commands.quantum_ops().unwrap(); + assert_eq!(gates.len(), 1); + assert_eq!(gates[0].gate_type, pecos_core::gate_type::GateType::RXYXY2Q); + assert_eq!(gates[0].qubits.as_slice(), &qubits.map(pecos_core::QubitId)); + assert_eq!( + gates[0].angles.as_slice(), + &[Angle64::from_radians(-0.73), Angle64::from_radians(0.41)] + ); + assert_eq!(batch.gate_metadata, vec![metadata.clone()]); + } + } + #[test] fn test_direct_lowering_preserves_leakage_measurement() { let mut engine = QisEngine::with_runtime(Box::new(DummyRuntime::default())); diff --git a/crates/pecos-qis/src/selene_runtime.rs b/crates/pecos-qis/src/selene_runtime.rs index 91c84056c..58d7b5d3d 100644 --- a/crates/pecos-qis/src/selene_runtime.rs +++ b/crates/pecos-qis/src/selene_runtime.rs @@ -857,6 +857,33 @@ impl SeleneRuntime { Ok(()) } + fn call_runtime_rpp( + &self, + runtime_qubit_1: u64, + runtime_qubit_2: u64, + theta: f64, + phi: f64, + ) -> Result<()> { + let lib = self + .library + .as_ref() + .ok_or_else(|| RuntimeError::FfiError("Selene runtime is not loaded".to_string()))?; + let instance = self.instance.ok_or_else(|| { + RuntimeError::FfiError("Selene runtime is not initialized".to_string()) + })?; + + unsafe { + let rpp_fn = Self::runtime_plugin_descriptor(lib)?.rpp_gate_fn; + let errno = rpp_fn(instance, runtime_qubit_1, runtime_qubit_2, theta, phi); + if errno != 0 { + return Err(RuntimeError::FfiError(format!( + "rpp failed with errno {errno}" + ))); + } + } + Ok(()) + } + fn call_runtime_reset(&self, runtime_qubit: u64) -> Result<()> { let lib = self .library @@ -1060,6 +1087,9 @@ impl SeleneRuntime { QuantumOp::RZZ(theta, qubit_1, qubit_2) => { QuantumOp::RZZ(*theta, map(*qubit_1)?, map(*qubit_2)?) } + QuantumOp::RXYXY2Q(theta, phi, qubit_1, qubit_2) => { + QuantumOp::RXYXY2Q(*theta, *phi, map(*qubit_1)?, map(*qubit_2)?) + } QuantumOp::Measure(qubit, result_id) => QuantumOp::Measure(map(*qubit)?, *result_id), QuantumOp::MeasureLeaked(qubit, result_id) => { QuantumOp::MeasureLeaked(map(*qubit)?, *result_id) @@ -1087,6 +1117,11 @@ impl SeleneRuntime { let runtime_qubit_2 = self.runtime_qubit_for_program(*qubit_2)?; self.call_runtime_rzz(runtime_qubit_1, runtime_qubit_2, *theta)?; } + QuantumOp::RXYXY2Q(theta, phi, qubit_1, qubit_2) => { + let runtime_qubit_1 = self.runtime_qubit_for_program(*qubit_1)?; + let runtime_qubit_2 = self.runtime_qubit_for_program(*qubit_2)?; + self.call_runtime_rpp(runtime_qubit_1, runtime_qubit_2, *theta, *phi)?; + } QuantumOp::Measure(qubit, result_id) => { let runtime_qubit = self.runtime_qubit_for_program(*qubit)?; self.call_runtime_measure(runtime_qubit, *result_id)?; @@ -1360,6 +1395,19 @@ impl SeleneRuntime { *lowered_qubit_1, *lowered_qubit_2, ), + ( + QuantumOp::RXYXY2Q(source_theta, source_phi, source_qubit_1, source_qubit_2), + QuantumOp::RXYXY2Q(lowered_theta, lowered_phi, lowered_qubit_1, lowered_qubit_2), + ) => { + Self::same_float(*source_theta, *lowered_theta) + && Self::same_float(*source_phi, *lowered_phi) + && Self::same_unordered_pair( + *source_qubit_1, + *source_qubit_2, + *lowered_qubit_1, + *lowered_qubit_2, + ) + } ( QuantumOp::RZZ(source_theta, source_qubit_1, source_qubit_2), QuantumOp::RZZ(lowered_theta, lowered_qubit_1, lowered_qubit_2), @@ -1425,7 +1473,8 @@ impl SeleneRuntime { | QuantumOp::CH(qubit_1, qubit_2) | QuantumOp::CRZ(_, qubit_1, qubit_2) | QuantumOp::ZZ(qubit_1, qubit_2) - | QuantumOp::RZZ(_, qubit_1, qubit_2) => { + | QuantumOp::RZZ(_, qubit_1, qubit_2) + | QuantumOp::RXYXY2Q(_, _, qubit_1, qubit_2) => { qubits.insert(*qubit_1); qubits.insert(*qubit_2); } @@ -1464,7 +1513,8 @@ impl SeleneRuntime { | QuantumOp::CH(qubit_1, qubit_2) | QuantumOp::CRZ(_, qubit_1, qubit_2) | QuantumOp::ZZ(qubit_1, qubit_2) - | QuantumOp::RZZ(_, qubit_1, qubit_2) => Some((*qubit_1, *qubit_2)), + | QuantumOp::RZZ(_, qubit_1, qubit_2) + | QuantumOp::RXYXY2Q(_, _, qubit_1, qubit_2) => Some((*qubit_1, *qubit_2)), _ => None, } } @@ -1591,9 +1641,13 @@ impl SeleneRuntime { theta, phi, } => { - return Err(RuntimeError::ExecutionError(format!( - "Selene runtime emitted unsupported RPP operation on qubits {qubit_id_1} and {qubit_id_2} (theta={theta}, phi={phi})" - ))); + let qubit_1 = self.runtime_qubit_to_usize(qubit_id_1)?; + let qubit_2 = self.runtime_qubit_to_usize(qubit_id_2)?; + self.push_idle_before(&mut lowered_ops, qubit_1, start_time)?; + self.push_idle_before(&mut lowered_ops, qubit_2, start_time)?; + lowered_ops.push(QuantumOp::RXYXY2Q(theta, phi, qubit_1, qubit_2)); + self.mark_gate_end(qubit_1, end_time); + self.mark_gate_end(qubit_2, end_time); } RuntimeScheduledOp::Custom => {} } @@ -1790,7 +1844,8 @@ fn include_quantum_op_capacity(qop: &QuantumOp, num_qubits: &mut usize, num_resu | QuantumOp::CH(qubit_1, qubit_2) | QuantumOp::CRZ(_, qubit_1, qubit_2) | QuantumOp::ZZ(qubit_1, qubit_2) - | QuantumOp::RZZ(_, qubit_1, qubit_2) => { + | QuantumOp::RZZ(_, qubit_1, qubit_2) + | QuantumOp::RXYXY2Q(_, _, qubit_1, qubit_2) => { include_qubit(num_qubits, *qubit_1); include_qubit(num_qubits, *qubit_2); } @@ -2277,24 +2332,110 @@ mod tests { ); } + #[cfg(feature = "selene-runtimes")] + #[test] + fn test_rxyxy2q_round_trip_through_simple_runtime() { + let mut runtime = crate::selene_runtimes::selene_simple_runtime().unwrap(); + let metadata = TraceMetadata::from([("source_label".to_string(), "xyxy".to_string())]); + let lowered = runtime + .lower_operations_with_metadata(&[ + Operation::AllocateQubit { id: 7 }, + Operation::AllocateQubit { id: 3 }, + Operation::TraceMetadata { + metadata: metadata.clone(), + qubit: Some(7), + }, + QuantumOp::RXYXY2Q(-0.73, 0.41, 3, 7).into(), + ]) + .unwrap(); + // The runtime assigns physical qubits in allocation order. The + // source handles are deliberately different so we check the mapping + // and the callback, as well as the two angles and source metadata. + let gates = lowered + .iter() + .filter(|gate| matches!(gate.op, QuantumOp::RXYXY2Q(..))) + .collect::>(); + assert_eq!(gates.len(), 1, "{lowered:?}"); + assert_eq!(gates[0].op, QuantumOp::RXYXY2Q(-0.73, 0.41, 1, 0)); + assert_eq!(gates[0].metadata, metadata); + } + #[test] - fn test_runtime_batch_rpp_fails_loudly() { + fn test_runtime_batch_rpp_preserves_angles_and_timing() { let mut runtime = SeleneRuntime::new("/path/to/selene.so"); - let error = runtime + runtime.last_gate_time_end_nanos = vec![5, 10, 0]; + let ops = runtime .convert_runtime_batch(RuntimeOperationBatch { - start_time_nanos: 0, - duration_nanos: 5, + start_time_nanos: 20, + duration_nanos: 7, invoked: true, operations: vec![RuntimeScheduledOp::Rpp { - qubit_id_1: 0, - qubit_id_2: 1, - theta: 1.0, - phi: 0.5, + qubit_id_1: 1, + qubit_id_2: 0, + theta: -0.73, + phi: 0.41, }], }) - .expect_err("RPP must not be silently discarded"); + .unwrap(); + + // Both qubits wait until 20ns, but their preceding gates ended at + // different times. The RPP should become one gate with both angles. + assert_eq!( + ops, + vec![ + QuantumOp::Idle(10e-9, 1), + QuantumOp::Idle(15e-9, 0), + QuantumOp::RXYXY2Q(-0.73, 0.41, 1, 0), + ] + ); + assert_eq!(runtime.last_gate_time_end_nanos, vec![27, 27, 0]); + let mut following = Vec::new(); + runtime.push_idle_before(&mut following, 0, 30).unwrap(); + runtime.push_idle_before(&mut following, 1, 30).unwrap(); + assert_eq!( + following, + vec![QuantumOp::Idle(3e-9, 0), QuantumOp::Idle(3e-9, 1)] + ); + } - assert!(error.to_string().contains("unsupported RPP")); + #[test] + fn test_rxyxy2q_metadata_matches_both_angles_and_qubits() { + let source = QuantumOp::RXYXY2Q(-0.73, 0.41, 2, 5); + assert!(SeleneRuntime::source_op_matches_lowered_op( + &source, + &QuantumOp::RXYXY2Q(-0.73, 0.41, 5, 2), + )); + for other in [ + QuantumOp::RXYXY2Q(0.73, 0.41, 2, 5), + QuantumOp::RXYXY2Q(-0.73, -0.41, 2, 5), + QuantumOp::RXYXY2Q(-0.73, 0.41, 2, 4), + ] { + assert!(!SeleneRuntime::source_op_matches_lowered_op( + &source, &other + )); + } + let metadata = TraceMetadata::from([("source_label".to_string(), "xyxy".to_string())]); + let mut pending = VecDeque::from([SourceTraceMetadata { + op: source.clone(), + metadata: metadata.clone(), + }]); + let mut lowered = Vec::new(); + SeleneRuntime::push_lowered_ops_with_source_metadata( + &mut lowered, + vec![QuantumOp::Idle(3e-9, 2), source.clone()], + &mut pending, + ); + assert!(pending.is_empty()); + assert!(lowered[0].metadata.is_empty()); + assert_eq!(lowered[1], LoweredQuantumOp::new(source.clone(), metadata)); + assert_eq!( + SeleneRuntime::quantum_op_qubits(&source), + BTreeSet::from([2, 5]) + ); + assert_eq!(SeleneRuntime::two_qubit_gate_qubits(&source), Some((2, 5))); + let mut collector = OperationCollector::default(); + collector.operations.push(source.into()); + assert_eq!(collector_capacity(&collector), (6, 0)); } #[test] diff --git a/crates/pecos-quantum/src/circuit_display.rs b/crates/pecos-quantum/src/circuit_display.rs index 197a43bc8..25b453f0c 100644 --- a/crates/pecos-quantum/src/circuit_display.rs +++ b/crates/pecos-quantum/src/circuit_display.rs @@ -48,7 +48,7 @@ fn gate_symbol(gate_type: GateType) -> &'static str { GateType::RZ => "RZ", GateType::U => "U", GateType::RXY1Q => "RXY1Q", - GateType::RPP => "RPP", + GateType::RXYXY2Q => "RXYXY2Q", GateType::CX => "CX", GateType::CY => "CY", GateType::CZ => "CZ", @@ -224,7 +224,7 @@ fn gate_color(gate_type: GateType) -> CellColor { | GateType::SWAP | GateType::U | GateType::RXY1Q - | GateType::RPP + | GateType::RXYXY2Q | GateType::RXXRYYRZZ | GateType::U2q | GateType::TrackedPauliMeta => CellColor::None, diff --git a/crates/pecos-quantum/src/unitary_matrix.rs b/crates/pecos-quantum/src/unitary_matrix.rs index 505b36a91..d958070ce 100644 --- a/crates/pecos-quantum/src/unitary_matrix.rs +++ b/crates/pecos-quantum/src/unitary_matrix.rs @@ -1910,7 +1910,7 @@ fn gate_to_matrix(gate_type: GateType, qubits: &[usize], num_qubits: usize) -> D | GateType::RZZ | GateType::U | GateType::RXY1Q - | GateType::RPP + | GateType::RXYXY2Q | GateType::RXXRYYRZZ | GateType::U2q => { panic!( diff --git a/crates/pecos-selene-core/src/lib.rs b/crates/pecos-selene-core/src/lib.rs index a33b44b6a..d01903a45 100644 --- a/crates/pecos-selene-core/src/lib.rs +++ b/crates/pecos-selene-core/src/lib.rs @@ -40,6 +40,9 @@ pub trait SeleneSimBehavior: Send { /// Apply `RZZ(theta)` to a pair of qubits. Angle in radians. fn apply_rzz(&mut self, q1: QubitId, q2: QubitId, theta: f64) -> Result<()>; + /// Apply `RXYXY2Q(theta, phi)` to a pair of qubits. Angles in radians. + fn apply_rxyxy2q(&mut self, q1: QubitId, q2: QubitId, theta: f64, phi: f64) -> Result<()>; + /// Reset a single qubit to `|0>`. fn reset_qubit(&mut self, qubit: QubitId) -> Result<()>; @@ -168,8 +171,22 @@ impl SimulatorInterface for SeleneAdapter { self.check_qubit(qubit_id, "Reset")?; self.behavior.reset_qubit(QubitId(to_usize(qubit_id)))?; } - Operation::RPPGate { .. } => { - return Err(anyhow!("RPP gates are not supported by this simulator")); + Operation::RPPGate { + qubit_id_1, + qubit_id_2, + theta, + phi, + } => { + self.check_pair(qubit_id_1, qubit_id_2, "RXYXY2Q")?; + if qubit_id_1 == qubit_id_2 { + return Err(anyhow!("RXYXY2Q requires two distinct qubits")); + } + self.behavior.apply_rxyxy2q( + QubitId(to_usize(qubit_id_1)), + QubitId(to_usize(qubit_id_2)), + theta, + phi, + )?; } Operation::Custom { .. } => {} _ => return Err(anyhow!("Unsupported Selene operation")), diff --git a/crates/pecos-selene-core/tests/adapter_conformance_test.rs b/crates/pecos-selene-core/tests/adapter_conformance_test.rs index 7e67328ae..5c3166f70 100644 --- a/crates/pecos-selene-core/tests/adapter_conformance_test.rs +++ b/crates/pecos-selene-core/tests/adapter_conformance_test.rs @@ -5,6 +5,7 @@ use anyhow::{Result, anyhow}; use pecos_core::{Angle64, QubitId}; use pecos_selene_core::{SeleneAdapter, SeleneSimBehavior, to_usize}; +use pecos_simulators::clifford_rotation::CliffordRotation; use pecos_simulators::{ArbitraryRotationGateable, CliffordGateable, Stabilizer, StateVec}; use selene_core::simulator::conformance_testing::run_basic_tests; use selene_core::simulator::interface::SimulatorInterfaceFactory; @@ -47,6 +48,15 @@ impl SeleneSimBehavior for StateVecBehavior { Ok(()) } + fn apply_rxyxy2q(&mut self, q1: QubitId, q2: QubitId, theta: f64, phi: f64) -> Result<()> { + self.sim.rxyxy2q( + Angle64::from_radians(theta), + Angle64::from_radians(phi), + &[(q1, q2)], + ); + Ok(()) + } + fn reset_qubit(&mut self, qubit: QubitId) -> Result<()> { let results = self.sim.mz(&[qubit]); if results[0].outcome { @@ -213,6 +223,17 @@ impl SeleneSimBehavior for StabilizerBehavior { Ok(()) } + fn apply_rxyxy2q(&mut self, q1: QubitId, q2: QubitId, theta: f64, phi: f64) -> Result<()> { + self.sim + .try_rxyxy2q( + Angle64::from_radians(theta), + Angle64::from_radians(phi), + &[(q1, q2)], + ) + .map_err(|error| anyhow!(error))?; + Ok(()) + } + fn reset_qubit(&mut self, qubit: QubitId) -> Result<()> { self.sim.mpz(&[qubit]); Ok(()) @@ -275,3 +296,37 @@ fn selene_conformance_stabilizer() { let args = vec![String::new(), "--angle-threshold=0.001".to_string()]; run_basic_tests(factory, args); } + +#[test] +fn adapter_dispatches_rpp_and_checks_targets() { + use selene_core::operation::{BatchOperation, Operation}; + use selene_core::simulator::SimulatorInterface; + + let mut adapter = SeleneAdapter { + behavior: StateVecBehavior { + sim: StateVec::with_seed(2, 0), + }, + num_qubits: 2, + }; + adapter + .handle_operations(BatchOperation::error_model(vec![Operation::RPPGate { + qubit_id_1: 1, + qubit_id_2: 0, + theta: std::f64::consts::PI, + phi: 0.37, + }])) + .unwrap(); + assert!((adapter.behavior.sim.get_amplitude(3).norm_sqr() - 1.0).abs() < 1e-12); + for (qubit_id_1, qubit_id_2) in [(0, 2), (2, 0), (1, 1)] { + assert!( + adapter + .handle_operations(BatchOperation::error_model(vec![Operation::RPPGate { + qubit_id_1, + qubit_id_2, + theta: 0.0, + phi: 0.0, + }])) + .is_err() + ); + } +} diff --git a/crates/pecos-simulators/src/arbitrary_rotation_gateable.rs b/crates/pecos-simulators/src/arbitrary_rotation_gateable.rs index d57a80dc8..a26c786a2 100644 --- a/crates/pecos-simulators/src/arbitrary_rotation_gateable.rs +++ b/crates/pecos-simulators/src/arbitrary_rotation_gateable.rs @@ -129,13 +129,13 @@ pub trait ArbitraryRotationGateable: CliffordGateable { /// Rotate by theta about P(phi) tensor P(phi), where P(phi) = cos(phi) X + sin(phi) Y. /// - /// RPP = (Rz(phi) tensor Rz(phi)) Rxx(theta) (Rz(-phi) tensor Rz(-phi)). + /// RXYXY2Q = (Rz(phi) tensor Rz(phi)) Rxx(theta) (Rz(-phi) tensor Rz(-phi)). /// At phi = pi, the principal-value convention gives both inverse Z /// rotations the same sign. Each ion contributes a minus sign, so the /// two-qubit global phase still cancels. #[inline] - fn rpp(&mut self, theta: Angle64, phi: Angle64, pairs: &[(QubitId, QubitId)]) -> &mut Self { - // These basis changes implement one RPP gate inside the simulator. + fn rxyxy2q(&mut self, theta: Angle64, phi: Angle64, pairs: &[(QubitId, QubitId)]) -> &mut Self { + // These basis changes implement one RXYXY2Q gate inside the simulator. // Noise models still see a single two-qubit gate. for &(q0, q1) in pairs { self.rz(-phi, &[q0, q1]); @@ -535,7 +535,7 @@ mod tests { } #[test] - fn default_rpp_matches_matrix_exactly() { + fn default_rxyxy2q_matches_matrix_exactly() { use crate::StateVecSoA32; use num_complex::Complex64; @@ -555,7 +555,7 @@ mod tests { let phase = Complex64::from_polar(1.0, 2.0 * phi.to_radians_signed()); let zero = Complex64::new(0.0, 0.0); let diagonal = Complex64::new(c, 0.0); - // For RPP, phi changes the coupling between |00> and |11>, + // For RXYXY2Q, phi changes the coupling between |00> and |11>, // but leaves the |01> and |10> coupling alone. At phi = pi, // the principal-value Z rotations each introduce a minus // sign, so the signs from the two qubits should cancel. @@ -574,7 +574,7 @@ mod tests { sim.x(&[QubitId(qubit)]); } } - sim.rpp(theta, phi, &[(QubitId(0), QubitId(1))]); + sim.rxyxy2q(theta, phi, &[(QubitId(0), QubitId(1))]); for (row, expected_entry) in expected.iter().enumerate() { let actual = sim.get_amplitude(row); let actual = Complex64::new(f64::from(actual.re), f64::from(actual.im)); @@ -589,7 +589,7 @@ mod tests { } #[test] - fn default_rpp_batches_match_sequential_pairs() { + fn default_rxyxy2q_batches_match_sequential_pairs() { use crate::StateVecSoA32; let theta = Angle64::from_radians(0.73); @@ -610,9 +610,9 @@ mod tests { .x(&[QubitId(2), QubitId(4)]) .cx(&[(QubitId(0), QubitId(4))]); } - batched.rpp(theta, phi, pairs); + batched.rxyxy2q(theta, phi, pairs); for &pair in *pairs { - sequential.rpp(theta, phi, &[pair]); + sequential.rxyxy2q(theta, phi, &[pair]); } for basis in 0..32 { assert!( diff --git a/crates/pecos-simulators/src/circuit_executor.rs b/crates/pecos-simulators/src/circuit_executor.rs index 1a2f9ddbb..403093d60 100644 --- a/crates/pecos-simulators/src/circuit_executor.rs +++ b/crates/pecos-simulators/src/circuit_executor.rs @@ -233,8 +233,8 @@ fn execute_gate_command( GateType::RYY => { sim.try_ryy(gate.angles[0], &flat_to_pairs(qubits))?; } - GateType::RPP => { - sim.try_rpp(gate.angles[0], gate.angles[1], &flat_to_pairs(qubits))?; + GateType::RXYXY2Q => { + sim.try_rxyxy2q(gate.angles[0], gate.angles[1], &flat_to_pairs(qubits))?; } GateType::RXY1Q => { sim.try_rxy1q(gate.angles[0], gate.angles[1], qubits)?; diff --git a/crates/pecos-simulators/src/clifford_rotation.rs b/crates/pecos-simulators/src/clifford_rotation.rs index ff5fedb31..0c422dc7c 100644 --- a/crates/pecos-simulators/src/clifford_rotation.rs +++ b/crates/pecos-simulators/src/clifford_rotation.rs @@ -93,11 +93,11 @@ pub trait CliffordRotation: CliffordGateable { pairs: &[(QubitId, QubitId)], ) -> Result<&mut Self, String>; - /// Apply RPP when its Rz/Rxx decomposition consists of Clifford rotations. + /// Apply RXYXY2Q when its Rz/Rxx decomposition consists of Clifford rotations. /// /// # Errors /// Returns an error before changing the state if an angle is unsupported. - fn try_rpp( + fn try_rxyxy2q( &mut self, theta: Angle64, phi: Angle64, @@ -110,7 +110,7 @@ pub trait CliffordRotation: CliffordGateable { || pecos_core::try_simplify_rotation_snapped(GateType::RZ, phi).is_none() { return Err(format!( - "RPP(theta={theta}, phi={phi}) is not a supported Clifford rotation" + "RXYXY2Q(theta={theta}, phi={phi}) is not a supported Clifford rotation" )); } for &(q0, q1) in pairs { @@ -596,7 +596,7 @@ mod tests { } #[test] - fn try_rpp_matches_xx_and_yy_rotations() { + fn try_rxyxy2q_matches_xx_and_yy_rotations() { let angles = [ Angle64::ZERO, Angle64::QUARTER_TURN, @@ -620,7 +620,7 @@ mod tests { .x(&[QubitId(2), QubitId(4)]) .cx(&[(QubitId(0), QubitId(4))]); } - actual.try_rpp(theta, phi, pairs).unwrap(); + actual.try_rxyxy2q(theta, phi, pairs).unwrap(); // At phi = 0 or pi, both factors are X up to sign, so // their product is XX. At +/-pi/2, we get YY instead. // Applying each pair separately also checks overlapping @@ -643,14 +643,14 @@ mod tests { } #[test] - fn try_rpp_zero_theta_accepts_arbitrary_axis() { + fn try_rxyxy2q_zero_theta_accepts_arbitrary_axis() { let mut sim = SparseStab::new(3); sim.h(&qid(0)).sz(&qid(0)).cx(&[(QubitId(0), QubitId(2))]); let before = (sim.stab_tableau(), sim.destab_tableau()); // With no rotation, phi doesn't matter. In particular, we shouldn't // reject an axis that would require non-Clifford basis changes. - sim.try_rpp( + sim.try_rxyxy2q( Angle64::ZERO, Angle64::from_radians(0.123), &[(QubitId(0), QubitId(1)), (QubitId(1), QubitId(2))], @@ -660,7 +660,7 @@ mod tests { } #[test] - fn try_rpp_unsupported_angles_leave_state_unchanged() { + fn try_rxyxy2q_unsupported_angles_leave_state_unchanged() { let eighth = Angle64::QUARTER_TURN / 2u64; for (theta, phi) in [ (eighth, Angle64::QUARTER_TURN), @@ -675,7 +675,7 @@ mod tests { // Even if one angle is supported, we should reject the other // before applying any basis changes or touching either pair. assert!( - sim.try_rpp( + sim.try_rxyxy2q( theta, phi, &[(QubitId(0), QubitId(1)), (QubitId(1), QubitId(2))], diff --git a/exp/pecos-experimental/src/hugr_executor.rs b/exp/pecos-experimental/src/hugr_executor.rs index b40067a40..dd1ee6eba 100644 --- a/exp/pecos-experimental/src/hugr_executor.rs +++ b/exp/pecos-experimental/src/hugr_executor.rs @@ -324,7 +324,7 @@ where | GateType::RXX | GateType::RYY | GateType::RZZ - | GateType::RPP + | GateType::RXYXY2Q | GateType::RXXRYYRZZ | GateType::U2q | GateType::T diff --git a/python/selene-plugins/pecos-selene-general-noise/README.md b/python/selene-plugins/pecos-selene-general-noise/README.md index 9e9607bc4..2fd8b7390 100644 --- a/python/selene-plugins/pecos-selene-general-noise/README.md +++ b/python/selene-plugins/pecos-selene-general-noise/README.md @@ -49,12 +49,10 @@ Idle rates use seconds because the adapter converts Selene's nanosecond schedule to the units expected by PECOS. Local crosstalk is described with neutral qubit groups rather than a hard-coded device layout. -The adapter supports Selene's RXY, RZ, RZZ, reset, Boolean measurement, and -leakage-valued measurement operations. Selene `RPP` operations are rejected -until PECOS defines a device-neutral general-noise representation for them; -silently decomposing them would change which primitive gate-error channels are -applied. Device-specific `Custom` operations are likewise outside this generic -adapter. +The adapter supports Selene's RXY, RZ, RZZ, RPP, reset, Boolean measurement, and +leakage-valued measurement operations. Selene `RPP` operations map to PECOS's +`RXYXY2Q` gate, so the noise model applies one two-qubit gate-error channel. +Device-specific `Custom` operations are outside this generic adapter. ## Development diff --git a/python/selene-plugins/pecos-selene-general-noise/src/lib.rs b/python/selene-plugins/pecos-selene-general-noise/src/lib.rs index 41cb7b708..7ef177b0e 100644 --- a/python/selene-plugins/pecos-selene-general-noise/src/lib.rs +++ b/python/selene-plugins/pecos-selene-general-noise/src/lib.rs @@ -519,10 +519,26 @@ impl ErrorModelInterface for GeneralNoiseErrorModel { self.builder.pz(&[qubit]); self.last_operation_end[qubit] = Some(end); } - Operation::RPPGate { .. } => { - bail!( - "RPP operations do not yet have a PECOS general-noise gate representation" + Operation::RPPGate { + qubit_id_1, + qubit_id_2, + theta, + phi, + } => { + let first = self.qubit(qubit_id_1)?; + let second = self.qubit(qubit_id_2)?; + if first == second { + bail!("RXYXY2Q requires two distinct qubits"); + } + self.add_idle_before(first, start)?; + self.add_idle_before(second, start)?; + self.builder.rxyxy2q( + Angle64::from_radians(theta), + Angle64::from_radians(phi), + &[(first, second)], ); + self.last_operation_end[first] = Some(end); + self.last_operation_end[second] = Some(end); } Operation::Custom { custom_tag, .. } => { bail!( @@ -1984,6 +2000,110 @@ mod tests { } } + #[test] + fn rpp_round_trip_preserves_one_gate_and_both_qubit_times() { + let mut model = build_error_model("{}", 3); + let mut sim = ClassicalSimulator::with_qubits(3); + model.shot_start(0, 41).unwrap(); + model + .handle_operations( + runtime_batch( + vec![Operation::RPPGate { + qubit_id_1: 2, + qubit_id_2: 0, + theta: -0.73, + phi: 0.41, + }], + 20, + 7, + ), + &mut sim, + ) + .unwrap(); + assert_eq!(model.last_operation_end, vec![Some(27), None, Some(27)]); + let received = sim.received.concat(); + assert_eq!(received.len(), 1); + let Operation::RPPGate { + qubit_id_1, + qubit_id_2, + theta, + phi, + } = received[0] + else { + panic!("expected a single RPP operation, got {received:?}"); + }; + assert_eq!((qubit_id_1, qubit_id_2), (2, 0)); + assert!((theta + 0.73).abs() < 1e-12); + assert!((phi - 0.41).abs() < 1e-12); + // Either target is busy until 27ns, so a gate starting at 26ns + // must fail whichever of the two qubits it uses. + for qubit_id in [0, 2] { + assert!( + model + .handle_operations( + runtime_batch( + vec![Operation::RZGate { + qubit_id, + theta: 0.5, + }], + 26, + 1 + ), + &mut sim + ) + .is_err() + ); + } + } + + #[test] + fn rpp_uses_one_two_qubit_noise_channel() { + // Force an XI error for each two-qubit gate and Z for every + // single-qubit gate. An RPP should produce one XI error and no Z + // errors from its internal basis changes. + let mut model = build_error_model( + r#"{ + "two_qubit":{"probability":1.0,"pauli_model":{"XI":1.0}}, + "single_qubit":{"probability":1.0,"pauli_model":{"Z":1.0}} + }"#, + 2, + ); + let mut sim = ClassicalSimulator::with_qubits(2); + model.shot_start(0, 41).unwrap(); + model + .handle_operations( + runtime_batch( + vec![Operation::RPPGate { + qubit_id_1: 0, + qubit_id_2: 1, + theta: 0.73, + phi: 0.41, + }], + 0, + 1, + ), + &mut sim, + ) + .unwrap(); + let received = sim.received.concat(); + assert_eq!(received.len(), 2, "{received:?}"); + assert_eq!( + received + .iter() + .filter(|op| matches!(op, Operation::RPPGate { .. })) + .count(), + 1 + ); + assert_eq!( + received + .iter() + .filter(|op| matches!(op, Operation::RXYGate { theta, phi, .. } + if (*theta - PI).abs() < 1e-12 && phi.abs() < 1e-12)) + .count(), + 1 + ); + } + #[test] fn adapter_rejects_invalid_runtime_contracts() { let config: Config = @@ -2061,8 +2181,8 @@ mod tests { &mut simulator, ) .err() - .expect("an RPP operation must fail"); - assert!(error.to_string().contains("RPP operations do not yet have")); + .expect("an RPP operation on the same qubit twice must fail"); + assert!(error.to_string().contains("requires two distinct qubits")); let mut error_model = build_error_model("{}", 1); let error = error_model diff --git a/python/selene-plugins/pecos-selene-general-noise/src/simulator.rs b/python/selene-plugins/pecos-selene-general-noise/src/simulator.rs index ff5bdadc4..ed7a78e05 100644 --- a/python/selene-plugins/pecos-selene-general-noise/src/simulator.rs +++ b/python/selene-plugins/pecos-selene-general-noise/src/simulator.rs @@ -92,6 +92,16 @@ impl SeleneSimulator { phi: angle(1), })); } + GateType::RXYXY2Q => { + operations.extend(qubits.as_chunks::<2>().0.iter().map(|pair| { + Operation::RPPGate { + qubit_id_1: pair[0], + qubit_id_2: pair[1], + theta: angle(0), + phi: angle(1), + } + })); + } GateType::RZZ => { operations.extend(qubits.as_chunks::<2>().0.iter().map(|pair| { Operation::RZZGate { diff --git a/python/selene-plugins/pecos-selene-mast/src/lib.rs b/python/selene-plugins/pecos-selene-mast/src/lib.rs index 9882adda0..846dc415e 100644 --- a/python/selene-plugins/pecos-selene-mast/src/lib.rs +++ b/python/selene-plugins/pecos-selene-mast/src/lib.rs @@ -99,6 +99,24 @@ impl MastSimulator { Ok(()) } + fn rxyxy2q(&mut self, qubit1: u64, qubit2: u64, theta: f64, phi: f64) -> Result<()> { + if qubit1 >= self.n_qubits || qubit2 >= self.n_qubits || qubit1 == qubit2 { + return Err(anyhow!( + "RXYXY2Q requires two distinct qubits below {}, got ({qubit1}, {qubit2})", + self.n_qubits + )); + } + self.simulator.rxyxy2q( + Angle64::from_radians(theta), + Angle64::from_radians(phi), + &[( + QubitId(Self::to_usize(qubit1)), + QubitId(Self::to_usize(qubit2)), + )], + ); + Ok(()) + } + fn measure(&mut self, qubit: u64) -> Result { if qubit >= self.n_qubits { return Err(anyhow!( @@ -208,7 +226,14 @@ impl SimulatorInterface for MastSimulator { results.set_u64_result(result_id, u64::from(Self::measure(self, qubit_id)?)); } Operation::Reset { qubit_id } => Self::reset(self, qubit_id)?, - Operation::RPPGate { .. } => bail!("RPP gates are not supported by Mast"), + Operation::RPPGate { + qubit_id_1, + qubit_id_2, + theta, + phi, + } => { + self.rxyxy2q(qubit_id_1, qubit_id_2, theta, phi)?; + } Operation::Custom { .. } => {} _ => bail!("Unsupported Selene operation"), } @@ -277,4 +302,60 @@ mod tests { let args: Vec = vec![String::new()]; run_basic_tests(interface, args); } + + #[test] + fn rpp_batch_rotates_both_qubits_and_checks_targets() { + use selene_core::operation::{BatchOperation, Operation}; + use selene_core::simulator::{SimulatorInterface, interface::SimulatorInterfaceFactory}; + + let args = vec![String::new()]; + let mut sim = Arc::new(MastSimulatorFactory).init(3, &args).unwrap(); + sim.shot_start(0, 17).unwrap(); + // Two quarter turns give a half turn, so |00> becomes |11> for + // any shared axis. Qubit 1 isn't targeted and should stay at zero. + for _ in 0..2 { + sim.handle_operations(BatchOperation::error_model(vec![Operation::RPPGate { + qubit_id_1: 2, + qubit_id_2: 0, + theta: std::f64::consts::FRAC_PI_2, + phi: 0.37, + }])) + .unwrap(); + } + let results = sim + .handle_operations(BatchOperation::error_model(vec![ + Operation::Measure { + qubit_id: 0, + result_id: 0, + }, + Operation::Measure { + qubit_id: 1, + result_id: 1, + }, + Operation::Measure { + qubit_id: 2, + result_id: 2, + }, + ])) + .unwrap(); + assert_eq!( + results + .bool_results + .iter() + .map(|r| r.value) + .collect::>(), + vec![true, false, true] + ); + for (first, second) in [(0, 3), (3, 0), (1, 1)] { + assert!( + sim.handle_operations(BatchOperation::error_model(vec![Operation::RPPGate { + qubit_id_1: first, + qubit_id_2: second, + theta: 0.0, + phi: 0.0, + }])) + .is_err() + ); + } + } } diff --git a/python/selene-plugins/pecos-selene-stab-mps/src/lib.rs b/python/selene-plugins/pecos-selene-stab-mps/src/lib.rs index fd1e8ac97..cb871e58c 100644 --- a/python/selene-plugins/pecos-selene-stab-mps/src/lib.rs +++ b/python/selene-plugins/pecos-selene-stab-mps/src/lib.rs @@ -101,6 +101,24 @@ impl StabMpsSimulator { Ok(()) } + fn rxyxy2q(&mut self, qubit1: u64, qubit2: u64, theta: f64, phi: f64) -> Result<()> { + if qubit1 >= self.n_qubits || qubit2 >= self.n_qubits || qubit1 == qubit2 { + return Err(anyhow!( + "RXYXY2Q requires two distinct qubits below {}, got ({qubit1}, {qubit2})", + self.n_qubits + )); + } + self.simulator.rxyxy2q( + Angle64::from_radians(theta), + Angle64::from_radians(phi), + &[( + QubitId(Self::to_usize(qubit1)), + QubitId(Self::to_usize(qubit2)), + )], + ); + Ok(()) + } + fn measure(&mut self, qubit: u64) -> Result { if qubit >= self.n_qubits { return Err(anyhow!( @@ -202,8 +220,13 @@ impl SimulatorInterface for StabMpsSimulator { results.set_u64_result(result_id, u64::from(Self::measure(self, qubit_id)?)); } Operation::Reset { qubit_id } => Self::reset(self, qubit_id)?, - Operation::RPPGate { .. } => { - anyhow::bail!("RPP gates are not supported by StabMps") + Operation::RPPGate { + qubit_id_1, + qubit_id_2, + theta, + phi, + } => { + self.rxyxy2q(qubit_id_1, qubit_id_2, theta, phi)?; } Operation::Custom { .. } => {} _ => anyhow::bail!("Unsupported Selene operation"), @@ -289,4 +312,60 @@ mod tests { MeasurementMode::Exact ); } + + #[test] + fn rpp_batch_rotates_both_qubits_and_checks_targets() { + use selene_core::operation::{BatchOperation, Operation}; + use selene_core::simulator::{SimulatorInterface, interface::SimulatorInterfaceFactory}; + + let args = vec![String::new()]; + let mut sim = Arc::new(StabMpsSimulatorFactory).init(3, &args).unwrap(); + sim.shot_start(0, 17).unwrap(); + // Two quarter turns give a half turn, so |00> becomes |11> for + // any shared axis. Qubit 1 isn't targeted and should stay at zero. + for _ in 0..2 { + sim.handle_operations(BatchOperation::error_model(vec![Operation::RPPGate { + qubit_id_1: 2, + qubit_id_2: 0, + theta: std::f64::consts::FRAC_PI_2, + phi: 0.37, + }])) + .unwrap(); + } + let results = sim + .handle_operations(BatchOperation::error_model(vec![ + Operation::Measure { + qubit_id: 0, + result_id: 0, + }, + Operation::Measure { + qubit_id: 1, + result_id: 1, + }, + Operation::Measure { + qubit_id: 2, + result_id: 2, + }, + ])) + .unwrap(); + assert_eq!( + results + .bool_results + .iter() + .map(|r| r.value) + .collect::>(), + vec![true, false, true] + ); + for (first, second) in [(0, 3), (3, 0), (1, 1)] { + assert!( + sim.handle_operations(BatchOperation::error_model(vec![Operation::RPPGate { + qubit_id_1: first, + qubit_id_2: second, + theta: 0.0, + phi: 0.0, + }])) + .is_err() + ); + } + } } diff --git a/python/selene-plugins/pecos-selene-stab-vec/src/lib.rs b/python/selene-plugins/pecos-selene-stab-vec/src/lib.rs index 651d25594..73c563386 100644 --- a/python/selene-plugins/pecos-selene-stab-vec/src/lib.rs +++ b/python/selene-plugins/pecos-selene-stab-vec/src/lib.rs @@ -112,6 +112,24 @@ impl StabVecSimulator { Ok(()) } + fn rxyxy2q(&mut self, qubit1: u64, qubit2: u64, theta: f64, phi: f64) -> Result<()> { + if qubit1 >= self.n_qubits || qubit2 >= self.n_qubits || qubit1 == qubit2 { + return Err(anyhow!( + "RXYXY2Q requires two distinct qubits below {}, got ({qubit1}, {qubit2})", + self.n_qubits + )); + } + self.simulator.rxyxy2q( + Angle64::from_radians(theta), + Angle64::from_radians(phi), + &[( + QubitId(Self::to_usize(qubit1)), + QubitId(Self::to_usize(qubit2)), + )], + ); + Ok(()) + } + fn measure(&mut self, qubit: u64) -> Result { if qubit >= self.n_qubits { return Err(anyhow!( @@ -227,8 +245,13 @@ impl SimulatorInterface for StabVecSimulator { results.set_u64_result(result_id, u64::from(Self::measure(self, qubit_id)?)); } Operation::Reset { qubit_id } => Self::reset(self, qubit_id)?, - Operation::RPPGate { .. } => { - anyhow::bail!("RPP gates are not supported by StabVec") + Operation::RPPGate { + qubit_id_1, + qubit_id_2, + theta, + phi, + } => { + self.rxyxy2q(qubit_id_1, qubit_id_2, theta, phi)?; } Operation::Custom { .. } => {} _ => anyhow::bail!("Unsupported Selene operation"), @@ -298,4 +321,60 @@ mod tests { let args: Vec = vec![String::new()]; run_basic_tests(interface, args); } + + #[test] + fn rpp_batch_rotates_both_qubits_and_checks_targets() { + use selene_core::operation::{BatchOperation, Operation}; + use selene_core::simulator::{SimulatorInterface, interface::SimulatorInterfaceFactory}; + + let args = vec![String::new()]; + let mut sim = Arc::new(StabVecSimulatorFactory).init(3, &args).unwrap(); + sim.shot_start(0, 17).unwrap(); + // Two quarter turns give a half turn, so |00> becomes |11> for + // any shared axis. Qubit 1 isn't targeted and should stay at zero. + for _ in 0..2 { + sim.handle_operations(BatchOperation::error_model(vec![Operation::RPPGate { + qubit_id_1: 2, + qubit_id_2: 0, + theta: std::f64::consts::FRAC_PI_2, + phi: 0.37, + }])) + .unwrap(); + } + let results = sim + .handle_operations(BatchOperation::error_model(vec![ + Operation::Measure { + qubit_id: 0, + result_id: 0, + }, + Operation::Measure { + qubit_id: 1, + result_id: 1, + }, + Operation::Measure { + qubit_id: 2, + result_id: 2, + }, + ])) + .unwrap(); + assert_eq!( + results + .bool_results + .iter() + .map(|r| r.value) + .collect::>(), + vec![true, false, true] + ); + for (first, second) in [(0, 3), (3, 0), (1, 1)] { + assert!( + sim.handle_operations(BatchOperation::error_model(vec![Operation::RPPGate { + qubit_id_1: first, + qubit_id_2: second, + theta: 0.0, + phi: 0.0, + }])) + .is_err() + ); + } + } } diff --git a/python/selene-plugins/pecos-selene-stabilizer/src/lib.rs b/python/selene-plugins/pecos-selene-stabilizer/src/lib.rs index 0cec2d55e..1e702cc3d 100644 --- a/python/selene-plugins/pecos-selene-stabilizer/src/lib.rs +++ b/python/selene-plugins/pecos-selene-stabilizer/src/lib.rs @@ -18,7 +18,8 @@ use anyhow::{Result, anyhow}; use clap::Parser; -use pecos_core::QubitId; +use pecos_core::{Angle64, QubitId}; +use pecos_simulators::clifford_rotation::CliffordRotation; use pecos_simulators::{CliffordGateable, Stabilizer}; use selene_core::error_model::BatchResult; use selene_core::export_simulator_plugin; @@ -285,6 +286,41 @@ impl StabilizerSimulator { Ok(()) } + fn rxyxy2q(&mut self, qubit1: u64, qubit2: u64, theta: f64, phi: f64) -> Result<()> { + if qubit1 >= self.n_qubits || qubit2 >= self.n_qubits || qubit1 == qubit2 { + return Err(anyhow!( + "RXYXY2Q requires two distinct qubits below {}, got ({qubit1}, {qubit2})", + self.n_qubits + )); + } + let snap = |angle| match self.get_approximate_angle(angle) { + ApproxAngle::Zero => Ok(Angle64::ZERO), + ApproxAngle::FracPi2 => Ok(Angle64::QUARTER_TURN), + ApproxAngle::Pi => Ok(Angle64::HALF_TURN), + ApproxAngle::Frac3Pi2 => Ok(Angle64::THREE_QUARTERS_TURN), + ApproxAngle::NoSuitableApproximation => Err(anyhow!( + "RXYXY2Q(theta={theta}, phi={phi}) is not a supported Clifford rotation" + )), + }; + let theta = snap(theta)?; + let phi = if theta == Angle64::ZERO { + Angle64::ZERO + } else { + snap(phi)? + }; + self.simulator + .try_rxyxy2q( + theta, + phi, + &[( + QubitId(Self::to_usize(qubit1)), + QubitId(Self::to_usize(qubit2)), + )], + ) + .map_err(|error| anyhow!(error))?; + Ok(()) + } + fn measure(&mut self, qubit: u64) -> Result { if qubit >= self.n_qubits { return Err(anyhow!( @@ -411,8 +447,13 @@ impl SimulatorInterface for StabilizerSimulator { results.set_u64_result(result_id, u64::from(Self::measure(self, qubit_id)?)); } Operation::Reset { qubit_id } => Self::reset(self, qubit_id)?, - Operation::RPPGate { .. } => { - anyhow::bail!("RPP gates are not supported by Stabilizer") + Operation::RPPGate { + qubit_id_1, + qubit_id_2, + theta, + phi, + } => { + self.rxyxy2q(qubit_id_1, qubit_id_2, theta, phi)?; } Operation::Custom { .. } => {} _ => anyhow::bail!("Unsupported Selene operation"), @@ -476,4 +517,100 @@ mod tests { let args = vec![String::new(), "--angle-threshold=0.001".to_string()]; run_basic_tests(interface, args); } + + #[test] + fn rpp_batch_rotates_both_qubits_and_checks_targets() { + use selene_core::operation::{BatchOperation, Operation}; + use selene_core::simulator::{SimulatorInterface, interface::SimulatorInterfaceFactory}; + + let args = vec![String::new(), "--angle-threshold=0.001".to_string()]; + let mut sim = Arc::new(StabilizerSimulatorFactory).init(3, &args).unwrap(); + sim.shot_start(0, 17).unwrap(); + // Two quarter turns give a half turn, so |00> becomes |11> for + // any shared axis. Qubit 1 isn't targeted and should stay at zero. + for _ in 0..2 { + sim.handle_operations(BatchOperation::error_model(vec![Operation::RPPGate { + qubit_id_1: 2, + qubit_id_2: 0, + theta: std::f64::consts::FRAC_PI_2, + phi: std::f64::consts::FRAC_PI_2, + }])) + .unwrap(); + } + let results = sim + .handle_operations(BatchOperation::error_model(vec![ + Operation::Measure { + qubit_id: 0, + result_id: 0, + }, + Operation::Measure { + qubit_id: 1, + result_id: 1, + }, + Operation::Measure { + qubit_id: 2, + result_id: 2, + }, + ])) + .unwrap(); + assert_eq!( + results + .bool_results + .iter() + .map(|r| r.value) + .collect::>(), + vec![true, false, true] + ); + for (first, second) in [(0, 3), (3, 0), (1, 1)] { + assert!( + sim.handle_operations(BatchOperation::error_model(vec![Operation::RPPGate { + qubit_id_1: first, + qubit_id_2: second, + theta: 0.0, + phi: 0.0, + }])) + .is_err() + ); + } + } + + #[test] + fn rpp_rejects_unsupported_angles_before_changing_state() { + use super::*; + use pecos_simulators::StabilizerTableauSimulator; + let mut sim = StabilizerSimulator { + simulator: Stabilizer::with_seed(2, 0), + n_qubits: 2, + angle_threshold: 0.001, + }; + sim.simulator.h(&[QubitId(0)]); + let before = (sim.simulator.stab_tableau(), sim.simulator.destab_tableau()); + for (theta, phi) in [ + (0.37, std::f64::consts::FRAC_PI_2), + (std::f64::consts::FRAC_PI_2, 0.37), + ] { + assert!( + sim.handle_operations(BatchOperation::error_model(vec![Operation::RPPGate { + qubit_id_1: 0, + qubit_id_2: 1, + theta, + phi, + }])) + .is_err() + ); + assert_eq!( + (sim.simulator.stab_tableau(), sim.simulator.destab_tableau()), + before + ); + } + // A zero rotation is the identity even when phi isn't a Clifford angle. + sim.rxyxy2q(0, 1, 0.0, 0.37).unwrap(); + assert_eq!( + (sim.simulator.stab_tableau(), sim.simulator.destab_tableau()), + before + ); + // The plugin's configured threshold should also apply to this gate. + sim.rxyxy2q(0, 1, std::f64::consts::FRAC_PI_2 + 1e-5, 1e-5) + .unwrap(); + } } diff --git a/python/selene-plugins/pecos-selene-statevec/src/lib.rs b/python/selene-plugins/pecos-selene-statevec/src/lib.rs index 9e866be45..decc347fd 100644 --- a/python/selene-plugins/pecos-selene-statevec/src/lib.rs +++ b/python/selene-plugins/pecos-selene-statevec/src/lib.rs @@ -124,6 +124,24 @@ impl StateVecSimulator { Ok(()) } + fn rxyxy2q(&mut self, qubit1: u64, qubit2: u64, theta: f64, phi: f64) -> Result<()> { + if qubit1 >= self.n_qubits || qubit2 >= self.n_qubits || qubit1 == qubit2 { + return Err(anyhow!( + "RXYXY2Q requires two distinct qubits below {}, got ({qubit1}, {qubit2})", + self.n_qubits + )); + } + self.simulator.rxyxy2q( + Angle64::from_radians(theta), + Angle64::from_radians(phi), + &[( + QubitId(Self::to_usize(qubit1)), + QubitId(Self::to_usize(qubit2)), + )], + ); + Ok(()) + } + fn measure(&mut self, qubit: u64) -> Result { if qubit >= self.n_qubits { return Err(anyhow!( @@ -286,8 +304,13 @@ impl SimulatorInterface for StateVecSimulator { results.set_u64_result(result_id, u64::from(Self::measure(self, qubit_id)?)); } Operation::Reset { qubit_id } => Self::reset(self, qubit_id)?, - Operation::RPPGate { .. } => { - anyhow::bail!("RPP gates are not supported by StateVec") + Operation::RPPGate { + qubit_id_1, + qubit_id_2, + theta, + phi, + } => { + self.rxyxy2q(qubit_id_1, qubit_id_2, theta, phi)?; } Operation::Custom { .. } => {} _ => anyhow::bail!("Unsupported Selene operation"), @@ -390,4 +413,87 @@ mod tests { let args: Vec = vec![String::new()]; run_basic_tests(interface, args); } + + #[test] + fn rpp_batch_rotates_both_qubits_and_checks_targets() { + use selene_core::operation::{BatchOperation, Operation}; + use selene_core::simulator::{SimulatorInterface, interface::SimulatorInterfaceFactory}; + + let args = vec![String::new()]; + let mut sim = Arc::new(StateVecSimulatorFactory).init(3, &args).unwrap(); + sim.shot_start(0, 17).unwrap(); + // Two quarter turns give a half turn, so |00> becomes |11> for + // any shared axis. Qubit 1 isn't targeted and should stay at zero. + for _ in 0..2 { + sim.handle_operations(BatchOperation::error_model(vec![Operation::RPPGate { + qubit_id_1: 2, + qubit_id_2: 0, + theta: std::f64::consts::FRAC_PI_2, + phi: 0.37, + }])) + .unwrap(); + } + let results = sim + .handle_operations(BatchOperation::error_model(vec![ + Operation::Measure { + qubit_id: 0, + result_id: 0, + }, + Operation::Measure { + qubit_id: 1, + result_id: 1, + }, + Operation::Measure { + qubit_id: 2, + result_id: 2, + }, + ])) + .unwrap(); + assert_eq!( + results + .bool_results + .iter() + .map(|r| r.value) + .collect::>(), + vec![true, false, true] + ); + for (first, second) in [(0, 3), (3, 0), (1, 1)] { + assert!( + sim.handle_operations(BatchOperation::error_model(vec![Operation::RPPGate { + qubit_id_1: first, + qubit_id_2: second, + theta: 0.0, + phi: 0.0, + }])) + .is_err() + ); + } + } + + #[test] + fn rpp_batch_preserves_arbitrary_axis_phase() { + use super::*; + let theta: f64 = -0.73; + let phi: f64 = 0.41; + let mut sim = Arc::new(StateVecSimulatorFactory) + .init(2, &[String::new()]) + .unwrap(); + sim.handle_operations(BatchOperation::error_model(vec![Operation::RPPGate { + qubit_id_1: 0, + qubit_id_2: 1, + theta, + phi, + }])) + .unwrap(); + // Starting from |00>, the |11> amplitude is -i sin(theta/2) + // times exp(2i phi), so both signs and both angles matter here. + let zero = sim.simulator.get_amplitude(0); + let one = sim.simulator.get_amplitude(3); + assert!((zero.re - (theta / 2.0).cos()).abs() < 1e-12); + assert!(zero.im.abs() < 1e-12); + assert!((one.re - (theta / 2.0).sin() * (2.0 * phi).sin()).abs() < 1e-12); + assert!((one.im + (theta / 2.0).sin() * (2.0 * phi).cos()).abs() < 1e-12); + assert!(sim.simulator.get_amplitude(1).norm() < 1e-12); + assert!(sim.simulator.get_amplitude(2).norm() < 1e-12); + } } From fc712af258fff62889ea117da259694ef177895c Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Sat, 19 Sep 2026 14:19:22 -0600 Subject: [PATCH 4/5] Do not parse RXYXY2Q as a QASM-native gate since the QASM engine cannot run it --- crates/pecos-qasm/src/parser/native_gates.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/pecos-qasm/src/parser/native_gates.rs b/crates/pecos-qasm/src/parser/native_gates.rs index 954459bc3..acf1abf64 100644 --- a/crates/pecos-qasm/src/parser/native_gates.rs +++ b/crates/pecos-qasm/src/parser/native_gates.rs @@ -33,7 +33,6 @@ pub fn parse_native_gate(name: &str) -> Option { "RX" => Some(CoreGateType::RX), "RY" => Some(CoreGateType::RY), "RZZ" => Some(CoreGateType::RZZ), - "RXYXY2Q" => Some(CoreGateType::RXYXY2Q), "RXY1Q" | "R1XY" => Some(CoreGateType::RXY1Q), "U" => Some(CoreGateType::U), _ => None, @@ -136,6 +135,10 @@ mod tests { }; if is_qasm_native_gate(gate) { assert_eq!(parse_native_gate(&gate.to_string()), Some(gate)); + } else { + // A gate the engine cannot run must not parse as native either, + // or a program is accepted at parse time and rejected at run time. + assert_eq!(parse_native_gate(&gate.to_string()), None, "{gate}"); } } } From 4255ce057862b5034a89f3b4a343a1d98c155906 Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Sat, 19 Sep 2026 14:34:00 -0600 Subject: [PATCH 5/5] Execute RXYXY2Q in the QASM engine and expose it through pecos.inc --- crates/pecos-qasm/includes/pecos.inc | 3 + crates/pecos-qasm/src/engine.rs | 16 +++++- crates/pecos-qasm/src/parser/native_gates.rs | 3 +- .../clifford_rotation_simulation_test.rs | 57 +++++++++++++++++++ .../include_gate_conformance_test.rs | 27 +++++++++ 5 files changed, 104 insertions(+), 2 deletions(-) diff --git a/crates/pecos-qasm/includes/pecos.inc b/crates/pecos-qasm/includes/pecos.inc index 60fdc1c9d..b39c40d67 100644 --- a/crates/pecos-qasm/includes/pecos.inc +++ b/crates/pecos-qasm/includes/pecos.inc @@ -30,6 +30,9 @@ gate syydg a,b { SYYDG a,b; } gate szz a,b { SZZ a,b; } gate szzdg a,b { SZZDG a,b; } +// XY-plane two-qubit rotation: exp(-i theta/2 (XY_phi x XY_phi)) +gate rxyxy2q(theta,phi) a,b { RXYXY2Q(theta,phi) a,b; } + // --- Measurement --- // Note: Measurement is a built-in operation in QASM // measure q -> c; diff --git a/crates/pecos-qasm/src/engine.rs b/crates/pecos-qasm/src/engine.rs index 5a0553b26..0c409c554 100644 --- a/crates/pecos-qasm/src/engine.rs +++ b/crates/pecos-qasm/src/engine.rs @@ -626,6 +626,19 @@ impl QASMEngine { ); } } + GateType::RXYXY2Q => { + let theta = params[0]; + let phi = params[1]; + for chunk in qubits.chunks(2) { + if chunk.len() == 2 { + self.message_builder.rxyxy2q( + Angle64::from_radians(theta), + Angle64::from_radians(phi), + &[(chunk[0], chunk[1])], + ); + } + } + } GateType::U => { let theta = params[0]; let phi = params[1]; @@ -689,7 +702,7 @@ impl QASMEngine { | GateType::CH | GateType::SWAP => self.process_two_qubit_gate(gate.gate_type, &qubits), // Gates not yet supported in QASM engine - GateType::CCX | GateType::PX | GateType::Channel | GateType::RXYXY2Q => { + GateType::CCX | GateType::PX | GateType::Channel => { Err(PecosError::Processing(format!( "Gate type {:?} is not yet supported in the QASM engine", gate.gate_type @@ -701,6 +714,7 @@ impl QASMEngine { | GateType::RXX | GateType::RYY | GateType::RZZ + | GateType::RXYXY2Q | GateType::RXXRYYRZZ | GateType::U2q | GateType::RXY1Q diff --git a/crates/pecos-qasm/src/parser/native_gates.rs b/crates/pecos-qasm/src/parser/native_gates.rs index acf1abf64..deb7fcae5 100644 --- a/crates/pecos-qasm/src/parser/native_gates.rs +++ b/crates/pecos-qasm/src/parser/native_gates.rs @@ -33,6 +33,7 @@ pub fn parse_native_gate(name: &str) -> Option { "RX" => Some(CoreGateType::RX), "RY" => Some(CoreGateType::RY), "RZZ" => Some(CoreGateType::RZZ), + "RXYXY2Q" => Some(CoreGateType::RXYXY2Q), "RXY1Q" | "R1XY" => Some(CoreGateType::RXY1Q), "U" => Some(CoreGateType::U), _ => None, @@ -89,6 +90,7 @@ mod tests { | CoreGateType::Tdg | CoreGateType::U | CoreGateType::RXY1Q + | CoreGateType::RXYXY2Q | CoreGateType::CX | CoreGateType::CY | CoreGateType::CZ @@ -106,7 +108,6 @@ mod tests { | CoreGateType::RXX | CoreGateType::RYY | CoreGateType::RXXRYYRZZ - | CoreGateType::RXYXY2Q | CoreGateType::U2q | CoreGateType::CCX | CoreGateType::MX diff --git a/crates/pecos-qasm/tests/integration/clifford_rotation_simulation_test.rs b/crates/pecos-qasm/tests/integration/clifford_rotation_simulation_test.rs index d2f859fab..94489e8f0 100644 --- a/crates/pecos-qasm/tests/integration/clifford_rotation_simulation_test.rs +++ b/crates/pecos-qasm/tests/integration/clifford_rotation_simulation_test.rs @@ -627,3 +627,60 @@ fn qasm_u_s_gate() { assert_eq!(value, 1, "Two u(0,0,pi/2) = Z, H*Z*H = X"); } } + +#[test] +fn rxyxy2q_spellings_parse_and_execute_identically() { + // Uppercase native and the pecos.inc alias. theta = pi about the YY axis + // maps |00> to |11>. + let programs = [ + r" + OPENQASM 2.0; + qreg q[2]; + creg c[2]; + RXYXY2Q(pi, pi/2) q[0], q[1]; + measure q -> c; + ", + r#" + OPENQASM 2.0; + include "pecos.inc"; + qreg q[2]; + creg c[2]; + rxyxy2q(pi, pi/2) q[0], q[1]; + measure q -> c; + "#, + ]; + + for qasm in programs { + let results = qasm_engine() + .program(Qasm::from_string(qasm)) + .to_sim() + .seed(42) + .workers(1) + .run(4) + .unwrap(); + for shot in &results.shots { + assert_eq!(shot.data.get("c").unwrap().as_u32(), Some(3), "{qasm}"); + } + } + + // Both spellings must reach the engine as the native gate carrying both + // angles, in order. + for qasm in programs { + let program = QASMParser::parse_str(qasm).unwrap(); + let gate = program + .operations + .iter() + .find_map(|op| match op { + Operation::NativeGate(gate) if gate.gate_type == GateType::RXYXY2Q => Some(gate), + _ => None, + }) + .unwrap(); + assert_eq!( + gate.angles.as_slice(), + &[ + pecos_core::Angle64::HALF_TURN, + pecos_core::Angle64::QUARTER_TURN + ] + ); + } +} diff --git a/crates/pecos-qasm/tests/integration/include_gate_conformance_test.rs b/crates/pecos-qasm/tests/integration/include_gate_conformance_test.rs index 8d149aa98..3e0e4a13f 100644 --- a/crates/pecos-qasm/tests/integration/include_gate_conformance_test.rs +++ b/crates/pecos-qasm/tests/integration/include_gate_conformance_test.rs @@ -222,6 +222,31 @@ fn rxx_matrix(theta: f64) -> Matrix { ]) } +/// `exp(-i theta/2 (P (x) P))` with `P = cos(phi) X + sin(phi) Y`. Written out +/// directly from `P|0> = e^{i phi}|1>` and `P|1> = e^{-i phi}|0>`, so the +/// `|00> <-> |11>` coupling carries `e^{+-2i phi}` and `|01> <-> |10>` none. +fn rxyxy2q_matrix(theta: f64, phi: f64) -> Matrix { + let cosine = complex((theta / 2.0).cos(), 0.0); + let coupling = complex(0.0, -(theta / 2.0).sin()); + let twist = cis(2.0 * phi); + matrix([ + [ + cosine, + complex(0.0, 0.0), + complex(0.0, 0.0), + coupling * twist.conj(), + ], + [complex(0.0, 0.0), cosine, coupling, complex(0.0, 0.0)], + [complex(0.0, 0.0), coupling, cosine, complex(0.0, 0.0)], + [ + coupling * twist, + complex(0.0, 0.0), + complex(0.0, 0.0), + cosine, + ], + ]) +} + /// Conventional two-qubit root: `((1+i) I + (1-i) P) / 2` for an involution P. fn conventional_root(pauli: &Matrix) -> Matrix { let a = complex(0.5, 0.5); @@ -341,6 +366,7 @@ fn reference_matrix(name: &str, parameters: &[f64]) -> Matrix { "cphase90" => controlled(&phase_matrix(PI / 2.0)), "rzz" => rzz_matrix(parameters[0]), "rxx" => rxx_matrix(parameters[0]), + "rxyxy2q" => rxyxy2q_matrix(parameters[0], parameters[1]), "szz" | "ZZ" => szz_matrix(), "szzdg" => conventional_root_dagger(&zz_pauli()), "sxx" => conventional_root(&xx_pauli()), @@ -364,6 +390,7 @@ fn has_non_periodic_theta(name: &str) -> bool { | "cry" | "crz" | "rxx" + | "rxyxy2q" | "rzz" | "u" | "u3"