diff --git a/crates/pecos-core/src/gate_type.rs b/crates/pecos-core/src/gate_type.rs index e8a7914e4..488665d9b 100644 --- a/crates/pecos-core/src/gate_type.rs +++ b/crates/pecos-core/src/gate_type.rs @@ -17,10 +17,14 @@ pub enum GateType { X = 0b01, Z = 0b10, Y = 0b11, - // SX = 4, - // SXdg = 5, - // SY = 6 - // SYdg = 7 + /// sqrt(X) gate + SX = 4, + /// sqrt(X)-dagger gate + SXdg = 5, + /// sqrt(Y) gate + SY = 6, + /// sqrt(Y)-dagger gate + SYdg = 7, SZ = 8, SZdg = 9, H = 10, @@ -60,19 +64,15 @@ pub enum GateType { // G = 61 /// Controlled-RZ gate (2 qubits, 1 angle parameter) CRZ = 70, - - // RXX = 80 - // RYY = 81 + /// RXX rotation gate + RXX = 80, + /// RYY rotation gate + RYY = 81, RZZ = 82, // RXXYYZZ /// Toffoli gate (CCX, 3 qubits) CCX = 90, - /// Square root of X gate (also known as V gate) - SX = 24, - /// Inverse of square root of X gate (also known as Vdg gate) - SXdg = 25, - // MX = 100 // MnX = 101 // MY = 102 @@ -108,6 +108,10 @@ impl From for GateType { 1 => GateType::X, 2 => GateType::Z, 3 => GateType::Y, + 4 => GateType::SX, + 5 => GateType::SXdg, + 6 => GateType::SY, + 7 => GateType::SYdg, 8 => GateType::SZ, 9 => GateType::SZdg, 10 => GateType::H, @@ -118,8 +122,6 @@ impl From for GateType { 34 => GateType::Tdg, 35 => GateType::U, 36 => GateType::R1XY, - 24 => GateType::SX, - 25 => GateType::SXdg, 50 => GateType::CX, 51 => GateType::CY, 52 => GateType::CZ, @@ -127,6 +129,8 @@ impl From for GateType { 58 => GateType::SZZdg, 59 => GateType::SWAP, 70 => GateType::CRZ, + 80 => GateType::RXX, + 81 => GateType::RYY, 82 => GateType::RZZ, 90 => GateType::CCX, 104 => GateType::Measure, @@ -157,13 +161,15 @@ impl GateType { | GateType::X | GateType::Y | GateType::Z + | GateType::SX + | GateType::SXdg + | GateType::SY + | GateType::SYdg | GateType::SZ | GateType::SZdg | GateType::H | GateType::T | GateType::Tdg - | GateType::SX - | GateType::SXdg | GateType::CX | GateType::CY | GateType::CZ @@ -184,6 +190,8 @@ impl GateType { GateType::RX | GateType::RY | GateType::RZ + | GateType::RXX + | GateType::RYY | GateType::RZZ | GateType::CRZ | GateType::Idle => 1, @@ -210,6 +218,10 @@ impl GateType { | GateType::X | GateType::Y | GateType::Z + | GateType::SX + | GateType::SXdg + | GateType::SY + | GateType::SYdg | GateType::SZ | GateType::SZdg | GateType::H @@ -218,8 +230,6 @@ impl GateType { | GateType::RZ | GateType::T | GateType::Tdg - | GateType::SX - | GateType::SXdg | GateType::R1XY | GateType::U | GateType::Measure @@ -240,6 +250,8 @@ impl GateType { | GateType::SZZdg | GateType::SWAP | GateType::CRZ + | GateType::RXX + | GateType::RYY | GateType::RZZ => 2, // Three-qubit gates @@ -255,7 +267,13 @@ impl GateType { pub const fn angle_arity(self) -> usize { match self { // Rotation gates with angle parameters - GateType::RX | GateType::RY | GateType::RZ | GateType::RZZ | GateType::CRZ => 1, + GateType::RX + | GateType::RY + | GateType::RZ + | GateType::RXX + | GateType::RYY + | GateType::RZZ + | GateType::CRZ => 1, GateType::R1XY => 2, GateType::U => 3, // All other gates have no angle parameters @@ -298,6 +316,10 @@ impl fmt::Display for GateType { GateType::X => write!(f, "X"), GateType::Y => write!(f, "Y"), GateType::Z => write!(f, "Z"), + GateType::SX => write!(f, "SX"), + GateType::SXdg => write!(f, "SXdg"), + GateType::SY => write!(f, "SY"), + GateType::SYdg => write!(f, "SYdg"), GateType::SZ => write!(f, "SZ"), GateType::SZdg => write!(f, "SZdg"), GateType::H => write!(f, "H"), @@ -308,13 +330,13 @@ impl fmt::Display for GateType { GateType::Tdg => write!(f, "Tdg"), GateType::U => write!(f, "U"), GateType::R1XY => write!(f, "R1XY"), - GateType::SX => write!(f, "SX"), - GateType::SXdg => write!(f, "SXdg"), GateType::CX => write!(f, "CX"), GateType::CY => write!(f, "CY"), GateType::CZ => write!(f, "CZ"), GateType::SZZ => write!(f, "SZZ"), GateType::SZZdg => write!(f, "SZZdg"), + GateType::RXX => write!(f, "RXX"), + GateType::RYY => write!(f, "RYY"), GateType::SWAP => write!(f, "SWAP"), GateType::CRZ => write!(f, "CRZ"), GateType::RZZ => write!(f, "RZZ"), diff --git a/crates/pecos-core/src/gates.rs b/crates/pecos-core/src/gates.rs index 45eb17376..712616074 100644 --- a/crates/pecos-core/src/gates.rs +++ b/crates/pecos-core/src/gates.rs @@ -94,6 +94,12 @@ impl Gate { .collect() } + /// Create Identity gate on multiple qubits + #[must_use] + pub fn i(qubits: &[impl Into + Copy]) -> Self { + Self::simple(GateType::I, qubits.iter().map(|&q| q.into()).collect()) + } + /// Create X gate on multiple qubits #[must_use] pub fn x(qubits: &[impl Into + Copy]) -> Self { @@ -118,6 +124,54 @@ impl Gate { Self::simple(GateType::H, qubits.iter().map(|&q| q.into()).collect()) } + /// Create SX gate (sqrt-X) on multiple qubits + #[must_use] + pub fn sx(qubits: &[impl Into + Copy]) -> Self { + Self::simple(GateType::SX, qubits.iter().map(|&q| q.into()).collect()) + } + + /// Create `SXdg` gate (sqrt-X dagger) on multiple qubits + #[must_use] + pub fn sxdg(qubits: &[impl Into + Copy]) -> Self { + Self::simple(GateType::SXdg, qubits.iter().map(|&q| q.into()).collect()) + } + + /// Create SY gate (sqrt-Y) on multiple qubits + #[must_use] + pub fn sy(qubits: &[impl Into + Copy]) -> Self { + Self::simple(GateType::SY, qubits.iter().map(|&q| q.into()).collect()) + } + + /// Create `SYdg` gate (sqrt-Y dagger) on multiple qubits + #[must_use] + pub fn sydg(qubits: &[impl Into + Copy]) -> Self { + Self::simple(GateType::SYdg, qubits.iter().map(|&q| q.into()).collect()) + } + + /// Create SZ gate (sqrt-Z) on multiple qubits + #[must_use] + pub fn sz(qubits: &[impl Into + Copy]) -> Self { + Self::simple(GateType::SZ, qubits.iter().map(|&q| q.into()).collect()) + } + + /// Create `SZdg` gate (sqrt-Z dagger) on multiple qubits + #[must_use] + pub fn szdg(qubits: &[impl Into + Copy]) -> Self { + Self::simple(GateType::SZdg, qubits.iter().map(|&q| q.into()).collect()) + } + + /// Create T gate on multiple qubits + #[must_use] + pub fn t(qubits: &[impl Into + Copy]) -> Self { + Self::simple(GateType::T, qubits.iter().map(|&q| q.into()).collect()) + } + + /// Create Tdg gate on multiple qubits + #[must_use] + pub fn tdg(qubits: &[impl Into + Copy]) -> Self { + Self::simple(GateType::Tdg, qubits.iter().map(|&q| q.into()).collect()) + } + /// Create CX gate from flat qubit list (control1, target1, control2, target2, ...) /// /// # Panics @@ -139,6 +193,48 @@ impl Gate { Self::cx_vec(&flat_qubits) } + /// Create CY gate from flat qubit list (control1, target1, control2, target2, ...) + /// + /// # Panics + /// + /// Panics if the number of qubits is not even, as `CY` gates require pairs of qubits. + #[must_use] + pub fn cy_vec(qubits: &[impl Into + Copy]) -> Self { + assert!( + qubits.len().is_multiple_of(2), + "CY gate requires an even number of qubits" + ); + Self::simple(GateType::CY, qubits.iter().map(|&q| q.into()).collect()) + } + + /// Create CY gate on multiple qubit pairs + #[must_use] + pub fn cy(qubit_pairs: &[(impl Into + Copy, impl Into + Copy)]) -> Self { + let flat_qubits = Self::flatten_qubit_pairs(qubit_pairs); + Self::cy_vec(&flat_qubits) + } + + /// Create CZ gate from flat qubit list (control1, target1, control2, target2, ...) + /// + /// # Panics + /// + /// Panics if the number of qubits is not even, as `CZ` gates require pairs of qubits. + #[must_use] + pub fn cz_vec(qubits: &[impl Into + Copy]) -> Self { + assert!( + qubits.len().is_multiple_of(2), + "CZ gate requires an even number of qubits" + ); + Self::simple(GateType::CZ, qubits.iter().map(|&q| q.into()).collect()) + } + + /// Create CZ gate on multiple qubit pairs + #[must_use] + pub fn cz(qubit_pairs: &[(impl Into + Copy, impl Into + Copy)]) -> Self { + let flat_qubits = Self::flatten_qubit_pairs(qubit_pairs); + Self::cz_vec(&flat_qubits) + } + /// Create SZZ gate from flat qubit list (`qubit1_1`, `qubit2_1`, `qubit1_2`, `qubit2_2`, ...) /// /// # Panics @@ -181,6 +277,62 @@ impl Gate { Self::szzdg_vec(&flat_qubits) } + /// Create RXX gate from flat qubit list (`qubit1_1`, `qubit2_1`, `qubit1_2`, `qubit2_2`, ...) + /// + /// # Panics + /// + /// Panics if the number of qubits is not even, as `RXX` gates require pairs of qubits. + #[must_use] + pub fn rxx_vec(theta: Angle64, qubits: &[impl Into + Copy]) -> Self { + assert!( + qubits.len().is_multiple_of(2), + "RXX gate requires an even number of qubits" + ); + Self::with_angles( + GateType::RXX, + vec![theta], + qubits.iter().map(|&q| q.into()).collect(), + ) + } + + /// Create RXX gate on multiple qubit pairs + #[must_use] + pub fn rxx( + theta: Angle64, + qubit_pairs: &[(impl Into + Copy, impl Into + Copy)], + ) -> Self { + let flat_qubits = Self::flatten_qubit_pairs(qubit_pairs); + Self::rxx_vec(theta, &flat_qubits) + } + + /// Create RYY gate from flat qubit list (`qubit1_1`, `qubit2_1`, `qubit1_2`, `qubit2_2`, ...) + /// + /// # Panics + /// + /// Panics if the number of qubits is not even, as `RYY` gates require pairs of qubits. + #[must_use] + pub fn ryy_vec(theta: Angle64, qubits: &[impl Into + Copy]) -> Self { + assert!( + qubits.len().is_multiple_of(2), + "RYY gate requires an even number of qubits" + ); + Self::with_angles( + GateType::RYY, + vec![theta], + qubits.iter().map(|&q| q.into()).collect(), + ) + } + + /// Create RYY gate on multiple qubit pairs + #[must_use] + pub fn ryy( + theta: Angle64, + qubit_pairs: &[(impl Into + Copy, impl Into + Copy)], + ) -> Self { + let flat_qubits = Self::flatten_qubit_pairs(qubit_pairs); + Self::ryy_vec(theta, &flat_qubits) + } + /// Create RZZ gate from flat qubit list (`qubit1_1`, `qubit2_1`, `qubit1_2`, `qubit2_2`, ...) /// /// # Panics diff --git a/crates/pecos-engines/src/noise/biased_depolarizing.rs b/crates/pecos-engines/src/noise/biased_depolarizing.rs index 0419f143b..547aeb8bf 100644 --- a/crates/pecos-engines/src/noise/biased_depolarizing.rs +++ b/crates/pecos-engines/src/noise/biased_depolarizing.rs @@ -157,20 +157,22 @@ impl BiasedDepolarizingNoiseModel { for gate in gates { match gate.gate_type { GateType::X - | GateType::Y | GateType::Z - | GateType::SZ - | GateType::SZdg + | GateType::Y | GateType::SX | GateType::SXdg + | GateType::SY + | GateType::SYdg + | GateType::SZ + | GateType::SZdg | GateType::H - | GateType::T - | GateType::Tdg | GateType::RX | GateType::RY - | GateType::R1XY | GateType::RZ - | GateType::U => { + | GateType::T + | GateType::Tdg + | GateType::U + | GateType::R1XY => { NoiseUtils::add_gate_to_builder(&mut builder, gate); trace!("Applying single-qubit gate with possible fault"); self.apply_sq_faults(&mut builder, gate); @@ -178,11 +180,13 @@ impl BiasedDepolarizingNoiseModel { GateType::CX | GateType::CY | GateType::CZ - | GateType::RZZ | GateType::SZZ | GateType::SZZdg | GateType::SWAP - | GateType::CRZ => { + | GateType::CRZ + | GateType::RXX + | GateType::RYY + | GateType::RZZ => { NoiseUtils::add_gate_to_builder(&mut builder, gate); trace!("Applying two-qubit gate with possible fault"); self.apply_tq_faults(&mut builder, gate); diff --git a/crates/pecos-engines/src/noise/depolarizing.rs b/crates/pecos-engines/src/noise/depolarizing.rs index 484c2610b..c9b9dd0a7 100644 --- a/crates/pecos-engines/src/noise/depolarizing.rs +++ b/crates/pecos-engines/src/noise/depolarizing.rs @@ -163,19 +163,22 @@ impl DepolarizingNoiseModel { for gate in gates { match gate.gate_type { GateType::X - | GateType::Y | GateType::Z - | GateType::SZ - | GateType::SZdg + | GateType::Y | GateType::SX | GateType::SXdg + | GateType::SY + | GateType::SYdg + | GateType::SZ + | GateType::SZdg | GateType::H - | GateType::T - | GateType::Tdg | GateType::RX | GateType::RY - | GateType::R1XY - | GateType::U => { + | GateType::RZ + | GateType::T + | GateType::Tdg + | GateType::U + | GateType::R1XY => { NoiseUtils::add_gate_to_builder(&mut builder, gate); trace!("Applying single-qubit gate with possible fault"); self.apply_sq_faults(&mut builder, gate); @@ -183,11 +186,13 @@ impl DepolarizingNoiseModel { GateType::CX | GateType::CY | GateType::CZ - | GateType::RZZ | GateType::SZZ | GateType::SZZdg | GateType::SWAP - | GateType::CRZ => { + | GateType::CRZ + | GateType::RXX + | GateType::RYY + | GateType::RZZ => { NoiseUtils::add_gate_to_builder(&mut builder, gate); trace!("Applying two-qubit gate with possible fault"); self.apply_tq_faults(&mut builder, gate); @@ -198,9 +203,6 @@ impl DepolarizingNoiseModel { // Apply fault to each qubit pair self.apply_tq_faults(&mut builder, gate); } - GateType::RZ => { - NoiseUtils::add_gate_to_builder(&mut builder, gate); - } GateType::Measure | GateType::MeasureLeaked | GateType::MeasureFree => { trace!("Applying measurement with possible fault"); self.apply_meas_faults(&mut builder, gate); diff --git a/crates/pecos-engines/src/quantum.rs b/crates/pecos-engines/src/quantum.rs index 808d05ee8..93818c650 100644 --- a/crates/pecos-engines/src/quantum.rs +++ b/crates/pecos-engines/src/quantum.rs @@ -435,6 +435,12 @@ impl Engine for StateVecEngine { // No active operation needed in the simulator // QFree is a no-op for state vector simulation (qubit tracking is handled elsewhere) } + GateType::SY | GateType::SYdg | GateType::RXX | GateType::RYY => { + return Err(quantum_error(format!( + "Gate type {:?} is not yet supported by StateVecEngine", + cmd.gate_type + ))); + } GateType::QAlloc => { // Allocate qubits in |0⟩ state - for state vector sim, same as Prep for q in &cmd.qubits { diff --git a/crates/pecos-experimental/src/hugr_executor.rs b/crates/pecos-experimental/src/hugr_executor.rs index fb0b16de9..1956e3265 100644 --- a/crates/pecos-experimental/src/hugr_executor.rs +++ b/crates/pecos-experimental/src/hugr_executor.rs @@ -295,21 +295,25 @@ where } // Unsupported gates - GateType::RX + GateType::SX + | GateType::SXdg + | GateType::SY + | GateType::SYdg + | GateType::RX | GateType::RY | GateType::RZ + | GateType::RXX + | GateType::RYY + | GateType::RZZ | GateType::T | GateType::Tdg | GateType::U | GateType::R1XY | GateType::SZZ | GateType::SZZdg - | GateType::RZZ | GateType::SWAP | GateType::CRZ - | GateType::CCX - | GateType::SX - | GateType::SXdg => { + | GateType::CCX => { return Err(HugrExecutionError::UnsupportedGate { gate_type: gate.gate_type, gate_index: gate_idx, diff --git a/crates/pecos-qasm/src/engine.rs b/crates/pecos-qasm/src/engine.rs index c94a62f2a..6444c1d2a 100644 --- a/crates/pecos-qasm/src/engine.rs +++ b/crates/pecos-qasm/src/engine.rs @@ -597,11 +597,15 @@ impl QASMEngine { | GateType::MeasCrosstalkGlobalPayload | GateType::QFree => Ok(()), // No-op gates (QFree is just a marker) GateType::X - | GateType::Y | GateType::Z - | GateType::H + | GateType::Y + | GateType::SX + | GateType::SXdg + | GateType::SY + | GateType::SYdg | GateType::SZ | GateType::SZdg + | GateType::H | GateType::T | GateType::Tdg | GateType::Prep @@ -610,7 +614,7 @@ impl QASMEngine { self.process_two_qubit_gate(gate.gate_type, &qubits) } // Gates not yet supported in QASM engine - GateType::SX | GateType::SXdg | GateType::SWAP | GateType::CCX | GateType::CRZ => { + GateType::SWAP | GateType::CCX | GateType::CRZ => { Err(PecosError::Processing(format!( "Gate type {:?} is not yet supported in the QASM engine", gate.gate_type @@ -619,6 +623,8 @@ impl QASMEngine { GateType::RX | GateType::RY | GateType::RZ + | GateType::RXX + | GateType::RYY | GateType::RZZ | GateType::R1XY | GateType::U => { diff --git a/crates/pecos-quantum/src/lib.rs b/crates/pecos-quantum/src/lib.rs index bf052bae0..7a72bdc75 100644 --- a/crates/pecos-quantum/src/lib.rs +++ b/crates/pecos-quantum/src/lib.rs @@ -46,14 +46,21 @@ //! //! // Each tick() returns a handle for adding gates //! // Regular gates chain, but preps/measurements break the chain -//! circuit.tick().pz(0); // Tick 0: Prepare q0 (breaks chain) -//! circuit.tick().pz(1); // Tick 1: Prepare q1 (breaks chain) -//! circuit.tick().h(0).x(1); // Tick 2: H and X chain together -//! circuit.tick().cx(0, 1); // Tick 3: CNOT -//! circuit.tick().mz(0); // Tick 4: Measure q0 (breaks chain) -//! circuit.tick().mz(1); // Tick 5: Measure q1 (breaks chain) +//! circuit.tick().pz(&[0]); // Tick 0: Prepare q0 (breaks chain) +//! circuit.tick().pz(&[1]); // Tick 1: Prepare q1 (breaks chain) +//! circuit.tick().h(&[0]).x(&[1]); // Tick 2: H and X chain together +//! circuit.tick().cx(&[(0, 1)]); // Tick 3: CNOT +//! circuit.tick().mz(&[0]); // Tick 4: Measure q0 (breaks chain) +//! circuit.tick().mz(&[1]); // Tick 5: Measure q1 (breaks chain) //! //! assert_eq!(circuit.num_ticks(), 6); +//! +//! // All methods accept slices for bulk operations: +//! let mut circuit2 = TickCircuit::new(); +//! circuit2.tick().pz(&[0, 1, 2, 3]); // Prep multiple qubits +//! circuit2.tick().h(&[0, 1, 2, 3]); // H on multiple qubits +//! circuit2.tick().cx(&[(0, 1), (2, 3)]); // Multiple CX gates +//! circuit2.tick().mz(&[0, 1, 2, 3]); // Measure multiple qubits //! ``` mod circuit; diff --git a/crates/pecos-quantum/src/tick_circuit.rs b/crates/pecos-quantum/src/tick_circuit.rs index fd8bcd113..df2f9db08 100644 --- a/crates/pecos-quantum/src/tick_circuit.rs +++ b/crates/pecos-quantum/src/tick_circuit.rs @@ -31,26 +31,33 @@ //! let mut circuit = TickCircuit::new(); //! //! // Each tick() returns a handle - regular gates chain on the handle -//! circuit.tick().pz(0); // Tick 0: Prepare q0 (breaks chain) -//! circuit.tick().pz(1); // Tick 1: Prepare q1 (breaks chain) -//! circuit.tick().h(0).x(1); // Tick 2: H on q0, X on q1 (chains!) -//! circuit.tick().cx(0, 1); // Tick 3: CNOT -//! circuit.tick().mz(0); // Tick 4: Measure q0 (breaks chain) -//! circuit.tick().mz(1); // Tick 5: Measure q1 (breaks chain) +//! circuit.tick().pz(&[0]); // Tick 0: Prepare q0 (breaks chain) +//! circuit.tick().pz(&[1]); // Tick 1: Prepare q1 (breaks chain) +//! circuit.tick().h(&[0]).x(&[1]); // Tick 2: H on q0, X on q1 (chains!) +//! circuit.tick().cx(&[(0, 1)]); // Tick 3: CNOT +//! circuit.tick().mz(&[0]); // Tick 4: Measure q0 (breaks chain) +//! circuit.tick().mz(&[1]); // Tick 5: Measure q1 (breaks chain) //! //! assert_eq!(circuit.num_ticks(), 6); //! //! // Preps and measurements break the chain but allow .meta(): -//! circuit.tick().pz(0).meta("reason", pecos_quantum::Attribute::String("init".into())); -//! circuit.tick().mz(0).meta("basis", pecos_quantum::Attribute::String("Z".into())); +//! circuit.tick().pz(&[0]).meta("reason", pecos_quantum::Attribute::String("init".into())); +//! circuit.tick().mz(&[0]).meta("basis", pecos_quantum::Attribute::String("Z".into())); //! //! // Tick-level metadata: call meta() before adding gates //! use pecos_quantum::Attribute; //! let mut circuit2 = TickCircuit::new(); //! circuit2.tick() //! .meta("round", Attribute::Int(0)) // Tick metadata (no gates added yet) -//! .h(0) +//! .h(&[0]) //! .meta("duration", Attribute::Float(50.0)); // Gate metadata (after a gate) +//! +//! // Bulk operations - apply gates to multiple qubits at once: +//! let mut circuit3 = TickCircuit::new(); +//! circuit3.tick().pz(&[0, 1, 2, 3]); // Prep 4 qubits at once +//! circuit3.tick().h(&[0, 1, 2, 3]); // H on 4 qubits at once +//! circuit3.tick().cx(&[(0, 1), (2, 3)]); // 2 CX gates in parallel +//! circuit3.tick().mz(&[0, 1, 2, 3]); // Measure all 4 qubits //! ``` use pecos_core::gate_type::GateType; @@ -232,6 +239,99 @@ impl Tick { } Ok(self.add_gate(gate)) } + + /// Remove all gates that use any of the specified qubits. + /// + /// Returns the number of gates removed. + /// + /// # Example + /// + /// ``` + /// use pecos_quantum::{TickCircuit, QubitId}; + /// + /// let mut circuit = TickCircuit::new(); + /// circuit.tick().h(&[0]).x(&[1]).cx(&[(2, 3)]); + /// + /// let tick = circuit.get_tick_mut(0).unwrap(); + /// let removed = tick.discard(&[QubitId::from(0), QubitId::from(2)]); + /// + /// assert_eq!(removed, 2); // H on q0 and CX on q2,q3 removed + /// assert_eq!(tick.len(), 1); // Only X on q1 remains + /// ``` + pub fn discard(&mut self, qubits: &[QubitId]) -> usize { + let qubits_set: BTreeSet<_> = qubits.iter().copied().collect(); + + // Find indices of gates to remove (those using any of the specified qubits) + let indices_to_remove: Vec = self + .gates + .iter() + .enumerate() + .filter(|(_, gate)| gate.qubits.iter().any(|q| qubits_set.contains(q))) + .map(|(idx, _)| idx) + .collect(); + + let removed_count = indices_to_remove.len(); + + if removed_count == 0 { + return 0; + } + + // Remove gates in reverse order to preserve indices + for &idx in indices_to_remove.iter().rev() { + self.gates.remove(idx); + } + + // Rebuild gate_attrs with updated indices + let old_attrs = std::mem::take(&mut self.gate_attrs); + for (old_idx, attrs) in old_attrs { + // Count how many removed indices are before this one + let shift = indices_to_remove.iter().filter(|&&i| i < old_idx).count(); + if !indices_to_remove.contains(&old_idx) { + self.gate_attrs.insert(old_idx - shift, attrs); + } + } + + removed_count + } + + /// Remove a specific gate by index. + /// + /// Returns the removed gate, or `None` if the index is out of bounds. + /// + /// # Example + /// + /// ``` + /// use pecos_quantum::TickCircuit; + /// + /// let mut circuit = TickCircuit::new(); + /// circuit.tick().h(&[0]).x(&[1]).z(&[2]); + /// + /// let tick = circuit.get_tick_mut(0).unwrap(); + /// let removed = tick.remove_gate(1); // Remove X gate + /// + /// assert!(removed.is_some()); + /// assert_eq!(tick.len(), 2); // H and Z remain + /// ``` + pub fn remove_gate(&mut self, idx: usize) -> Option { + if idx >= self.gates.len() { + return None; + } + + let gate = self.gates.remove(idx); + + // Rebuild gate_attrs with updated indices + let old_attrs = std::mem::take(&mut self.gate_attrs); + for (old_idx, attrs) in old_attrs { + if old_idx < idx { + self.gate_attrs.insert(old_idx, attrs); + } else if old_idx > idx { + self.gate_attrs.insert(old_idx - 1, attrs); + } + // Skip old_idx == idx (removed gate's attrs) + } + + Some(gate) + } } /// A quantum circuit represented as a sequence of parallel time slices (ticks). @@ -249,14 +349,20 @@ impl Tick { /// /// // Each tick() returns a TickHandle for adding gates /// // Regular gates chain, but preps/measurements break the chain -/// circuit.tick().pz(0); // Tick 0: Prepare q0 (breaks chain) -/// circuit.tick().pz(1); // Tick 1: Prepare q1 (breaks chain) -/// circuit.tick().h(0).x(1); // Tick 2: H and X (chains!) -/// circuit.tick().cx(0, 1); // Tick 3: CNOT -/// circuit.tick().mz(0); // Tick 4: Measure q0 (breaks chain) -/// circuit.tick().mz(1); // Tick 5: Measure q1 (breaks chain) +/// circuit.tick().pz(&[0]); // Tick 0: Prepare q0 (breaks chain) +/// circuit.tick().pz(&[1]); // Tick 1: Prepare q1 (breaks chain) +/// circuit.tick().h(&[0]).x(&[1]); // Tick 2: H and X (chains!) +/// circuit.tick().cx(&[(0, 1)]); // Tick 3: CNOT +/// circuit.tick().mz(&[0]); // Tick 4: Measure q0 (breaks chain) +/// circuit.tick().mz(&[1]); // Tick 5: Measure q1 (breaks chain) /// /// assert_eq!(circuit.num_ticks(), 6); +/// +/// // Bulk operations - all methods accept slices: +/// circuit.tick().pz(&[0, 1, 2, 3]); // Prep multiple qubits +/// circuit.tick().h(&[0, 1, 2, 3]); // H on multiple qubits +/// circuit.tick().cx(&[(0, 1), (2, 3)]); // Multiple CX gates +/// circuit.tick().mz(&[0, 1, 2, 3]); // Measure multiple qubits /// ``` #[derive(Debug, Clone, Default)] pub struct TickCircuit { @@ -427,6 +533,339 @@ impl TickCircuit { pub fn circuit_attrs(&self) -> impl Iterator { self.circuit_attrs.iter() } + + // ========================================================================= + // Circuit manipulation + // ========================================================================= + + /// Clear the circuit and start fresh. + /// + /// This completely replaces the circuit with a new empty instance, + /// releasing any allocated memory. Use this when memory usage is a concern + /// or when you want absolute certainty of a fresh state. + /// + /// For performance-critical code or when creating many circuits in sequence, + /// consider using [`reset()`](Self::reset) instead, which preserves memory allocation. + /// + /// # Example + /// + /// ``` + /// use pecos_quantum::TickCircuit; + /// + /// let mut circuit = TickCircuit::new(); + /// circuit.tick().h(&[0]); + /// assert_eq!(circuit.num_ticks(), 1); + /// + /// circuit.clear(); + /// assert_eq!(circuit.num_ticks(), 0); + /// ``` + pub fn clear(&mut self) { + *self = Self::new(); + } + + /// Reset the circuit state while preserving allocated memory. + /// + /// Unlike [`clear()`](Self::clear), this method preserves the allocated memory + /// for better performance when reusing the same circuit multiple times. + /// This is the recommended method for performance-critical code, + /// especially when creating many circuits in sequence. + /// + /// # Example + /// + /// ``` + /// use pecos_quantum::TickCircuit; + /// + /// let mut circuit = TickCircuit::new(); + /// + /// // Build first circuit + /// circuit.tick().h(&[0]); + /// circuit.tick().cx(&[(0, 1)]); + /// assert_eq!(circuit.num_ticks(), 2); + /// + /// // Reset and build another circuit (memory preserved) + /// circuit.reset(); + /// assert_eq!(circuit.num_ticks(), 0); + /// + /// circuit.tick().x(&[0]); + /// assert_eq!(circuit.num_ticks(), 1); + /// ``` + pub fn reset(&mut self) { + self.ticks.clear(); + self.next_tick = 0; + self.circuit_attrs.clear(); + } + + /// Reserve empty ticks in advance. + /// + /// This preallocates `n` empty ticks, which can be useful when you know + /// the circuit structure ahead of time. + /// + /// # Example + /// + /// ``` + /// use pecos_quantum::TickCircuit; + /// + /// let mut circuit = TickCircuit::new(); + /// circuit.reserve_ticks(4); + /// + /// // Ticks 0-3 are now available (though empty) + /// assert_eq!(circuit.ticks().len(), 4); + /// + /// // tick() will start from tick 4 + /// circuit.tick().h(&[0]); + /// assert_eq!(circuit.num_ticks(), 5); + /// ``` + pub fn reserve_ticks(&mut self, n: usize) { + let target_len = self.ticks.len() + n; + self.ticks.reserve(n); + while self.ticks.len() < target_len { + self.ticks.push(Tick::new()); + } + self.next_tick = self.ticks.len(); + } + + /// Insert an empty tick at a specific position. + /// + /// All ticks at or after `idx` are shifted to the right. + /// Returns a [`TickHandle`] to the newly inserted tick. + /// + /// # Panics + /// + /// Panics if `idx > self.ticks().len()`. + /// + /// # Example + /// + /// ``` + /// use pecos_quantum::TickCircuit; + /// + /// let mut circuit = TickCircuit::new(); + /// circuit.tick().h(&[0]); // Tick 0 + /// circuit.tick().cx(&[(0, 1)]); // Tick 1 + /// + /// // Insert a new tick between them + /// circuit.insert_tick(1).x(&[1]); + /// + /// // Now: H at tick 0, X at tick 1, CX at tick 2 + /// assert_eq!(circuit.num_ticks(), 3); + /// ``` + pub fn insert_tick(&mut self, idx: usize) -> TickHandle<'_> { + assert!( + idx <= self.ticks.len(), + "insert_tick index {} out of bounds for circuit with {} ticks", + idx, + self.ticks.len() + ); + + self.ticks.insert(idx, Tick::new()); + self.next_tick = self.ticks.len(); + + TickHandle { + circuit: self, + tick_idx: idx, + last_gate_idx: None, + } + } + + /// Get a handle to an existing tick for adding more gates. + /// + /// This allows adding gates to a tick that was previously created, + /// which is useful when building circuits non-sequentially. + /// + /// # Panics + /// + /// Panics if `idx >= self.ticks().len()`. + /// + /// # Example + /// + /// ``` + /// use pecos_quantum::TickCircuit; + /// + /// let mut circuit = TickCircuit::new(); + /// circuit.reserve_ticks(3); + /// + /// // Add gates to specific ticks + /// circuit.tick_at(0).h(&[0]); + /// circuit.tick_at(2).cx(&[(0, 1)]); + /// circuit.tick_at(1).x(&[1]); // Fill in the middle later + /// + /// assert_eq!(circuit.num_ticks(), 3); + /// ``` + pub fn tick_at(&mut self, idx: usize) -> TickHandle<'_> { + assert!( + idx < self.ticks.len(), + "tick_at index {} out of bounds for circuit with {} ticks", + idx, + self.ticks.len() + ); + + TickHandle { + circuit: self, + tick_idx: idx, + last_gate_idx: None, + } + } + + /// Remove all gates that use any of the specified qubits from a tick. + /// + /// Returns the number of gates removed, or `None` if the tick index is out of bounds. + /// + /// # Example + /// + /// ``` + /// use pecos_quantum::TickCircuit; + /// + /// let mut circuit = TickCircuit::new(); + /// circuit.tick().h(&[0]).x(&[1]).cx(&[(2, 3)]); + /// + /// let removed = circuit.discard(&[0, 2], 0); + /// assert_eq!(removed, Some(2)); // H on q0 and CX on q2,q3 removed + /// assert_eq!(circuit.get_tick(0).unwrap().len(), 1); // Only X remains + /// ``` + pub fn discard( + &mut self, + qubits: &[impl Into + Copy], + tick_idx: usize, + ) -> Option { + let qubit_ids: Vec = qubits.iter().map(|&q| q.into()).collect(); + self.get_tick_mut(tick_idx) + .map(|tick| tick.discard(&qubit_ids)) + } + + // ========================================================================= + // Iteration helpers + // ========================================================================= + + /// Iterate over all gates in the circuit, across all ticks. + /// + /// Gates are yielded in tick order, then in order within each tick. + /// + /// # Examples + /// + /// ``` + /// use pecos_quantum::TickCircuit; + /// + /// let mut circuit = TickCircuit::new(); + /// circuit.tick().h(&[0, 1]); + /// circuit.tick().cx(&[(0, 1)]); + /// + /// for gate in circuit.iter_gates() { + /// println!("{:?} on {:?}", gate.gate_type, gate.qubits); + /// } + /// ``` + pub fn iter_gates(&self) -> impl Iterator { + self.ticks.iter().flat_map(Tick::gates) + } + + /// Iterate over all gates with their tick index. + /// + /// Yields `(tick_index, gate)` pairs. + /// + /// # Examples + /// + /// ``` + /// use pecos_quantum::TickCircuit; + /// + /// let mut circuit = TickCircuit::new(); + /// circuit.tick().h(&[0]); + /// circuit.tick().x(&[0]); + /// + /// for (tick_idx, gate) in circuit.iter_gates_with_tick() { + /// println!("Tick {}: {:?}", tick_idx, gate.gate_type); + /// } + /// ``` + pub fn iter_gates_with_tick(&self) -> impl Iterator { + self.ticks + .iter() + .enumerate() + .flat_map(|(tick_idx, tick)| tick.gates().iter().map(move |gate| (tick_idx, gate))) + } + + /// Iterate over ticks with their index. + /// + /// # Examples + /// + /// ``` + /// use pecos_quantum::TickCircuit; + /// + /// let mut circuit = TickCircuit::new(); + /// circuit.tick().h(&[0, 1, 2]); + /// circuit.tick().cx(&[(0, 1), (1, 2)]); + /// + /// for (tick_idx, tick) in circuit.iter_ticks() { + /// println!("Tick {} has {} gates", tick_idx, tick.len()); + /// } + /// ``` + pub fn iter_ticks(&self) -> impl Iterator { + self.ticks.iter().enumerate() + } + + /// Iterate over gates filtered by gate type. + /// + /// # Examples + /// + /// ``` + /// use pecos_quantum::TickCircuit; + /// use pecos_core::gate_type::GateType; + /// + /// let mut circuit = TickCircuit::new(); + /// circuit.tick().h(&[0, 1, 2]); + /// circuit.tick().x(&[0]).cx(&[(1, 2)]); + /// + /// // Get all H gates + /// let h_gates: Vec<_> = circuit.iter_gates_by_type(GateType::H).collect(); + /// assert_eq!(h_gates.len(), 1); // One Gate object with 3 qubits + /// ``` + pub fn iter_gates_by_type(&self, gate_type: GateType) -> impl Iterator { + self.iter_gates().filter(move |g| g.gate_type == gate_type) + } + + /// Get all qubits used in the circuit. + /// + /// # Examples + /// + /// ``` + /// use pecos_quantum::TickCircuit; + /// + /// let mut circuit = TickCircuit::new(); + /// circuit.tick().h(&[0, 1, 2]); + /// circuit.tick().cx(&[(0, 1)]); + /// + /// let qubits = circuit.all_qubits(); + /// assert_eq!(qubits.len(), 3); + /// ``` + #[must_use] + pub fn all_qubits(&self) -> BTreeSet { + self.iter_gates() + .flat_map(|gate| gate.qubits.iter().copied()) + .collect() + } + + /// Count gates by type across the entire circuit. + /// + /// Returns a map from `GateType` to count. + /// + /// # Examples + /// + /// ``` + /// use pecos_quantum::TickCircuit; + /// use pecos_core::gate_type::GateType; + /// + /// let mut circuit = TickCircuit::new(); + /// circuit.tick().h(&[0, 1, 2]); + /// circuit.tick().cx(&[(0, 1), (1, 2)]); + /// + /// let counts = circuit.gate_counts_by_type(); + /// assert_eq!(counts.get(&GateType::H), Some(&1)); // 1 H gate object (with 3 qubits) + /// assert_eq!(counts.get(&GateType::CX), Some(&1)); // 1 CX gate object (with 2 pairs) + /// ``` + #[must_use] + pub fn gate_counts_by_type(&self) -> BTreeMap { + let mut counts = BTreeMap::new(); + for gate in self.iter_gates() { + *counts.entry(gate.gate_type).or_insert(0) += 1; + } + counts + } } // ============================================================================ @@ -525,100 +964,334 @@ impl<'a> TickHandle<'a> { // Single-qubit gates // ========================================================================= - /// Apply a Hadamard gate. - pub fn h(&mut self, q: impl Into) -> &mut Self { - self.add_gate(Gate::h(&[q.into()])) + /// Apply Hadamard gate(s) to one or more qubits. + /// + /// # Examples + /// + /// ``` + /// use pecos_quantum::TickCircuit; + /// + /// let mut circuit = TickCircuit::new(); + /// // Single qubit + /// circuit.tick().h(&[0]); + /// // Multiple qubits in one call + /// circuit.tick().h(&[1, 2, 3]); + /// ``` + pub fn h(&mut self, qubits: &[impl Into + Copy]) -> &mut Self { + self.add_gate(Gate::h(qubits)) } - /// Apply a Pauli-X gate. - pub fn x(&mut self, q: impl Into) -> &mut Self { - self.add_gate(Gate::x(&[q.into()])) + /// Apply Pauli-X gate(s) to one or more qubits. + pub fn x(&mut self, qubits: &[impl Into + Copy]) -> &mut Self { + self.add_gate(Gate::x(qubits)) } - /// Apply a Pauli-Y gate. - pub fn y(&mut self, q: impl Into) -> &mut Self { - self.add_gate(Gate::y(&[q.into()])) + /// Apply Pauli-Y gate(s) to one or more qubits. + pub fn y(&mut self, qubits: &[impl Into + Copy]) -> &mut Self { + self.add_gate(Gate::y(qubits)) } - /// Apply a Pauli-Z gate. - pub fn z(&mut self, q: impl Into) -> &mut Self { - self.add_gate(Gate::z(&[q.into()])) + /// Apply Pauli-Z gate(s) to one or more qubits. + pub fn z(&mut self, qubits: &[impl Into + Copy]) -> &mut Self { + self.add_gate(Gate::z(qubits)) } - /// Apply an S gate (sqrt-Z). - pub fn sz(&mut self, q: impl Into) -> &mut Self { - self.add_gate(Gate::simple(GateType::SZ, vec![q.into()])) + /// Apply Identity gate(s) to one or more qubits. + /// + /// # Examples + /// + /// ``` + /// use pecos_quantum::TickCircuit; + /// + /// let mut circuit = TickCircuit::new(); + /// circuit.tick().i(&[0]); // Single qubit + /// circuit.tick().i(&[1, 2, 3]); // Multiple qubits + /// ``` + pub fn i(&mut self, qubits: &[impl Into + Copy]) -> &mut Self { + self.add_gate(Gate::i(qubits)) } - /// Apply an S-dagger gate. - pub fn szdg(&mut self, q: impl Into) -> &mut Self { - self.add_gate(Gate::simple(GateType::SZdg, vec![q.into()])) + /// Apply SX gate(s) (sqrt-X) to one or more qubits. + pub fn sx(&mut self, qubits: &[impl Into + Copy]) -> &mut Self { + self.add_gate(Gate::sx(qubits)) } - /// Apply a T gate. - pub fn t(&mut self, q: impl Into) -> &mut Self { - self.add_gate(Gate::simple(GateType::T, vec![q.into()])) + /// Apply SX-dagger gate(s) to one or more qubits. + pub fn sxdg(&mut self, qubits: &[impl Into + Copy]) -> &mut Self { + self.add_gate(Gate::sxdg(qubits)) } - /// Apply a T-dagger gate. - pub fn tdg(&mut self, q: impl Into) -> &mut Self { - self.add_gate(Gate::simple(GateType::Tdg, vec![q.into()])) + /// Apply SY gate(s) (sqrt-Y) to one or more qubits. + pub fn sy(&mut self, qubits: &[impl Into + Copy]) -> &mut Self { + self.add_gate(Gate::sy(qubits)) } - /// Apply an RX rotation. - pub fn rx(&mut self, theta: impl Into, q: impl Into) -> &mut Self { - self.add_gate(Gate::rx(theta.into(), &[q.into()])) + /// Apply SY-dagger gate(s) to one or more qubits. + pub fn sydg(&mut self, qubits: &[impl Into + Copy]) -> &mut Self { + self.add_gate(Gate::sydg(qubits)) } - /// Apply an RY rotation. - pub fn ry(&mut self, theta: impl Into, q: impl Into) -> &mut Self { - self.add_gate(Gate::ry(theta.into(), &[q.into()])) + /// Apply SZ gate(s) (sqrt-Z) to one or more qubits. + pub fn sz(&mut self, qubits: &[impl Into + Copy]) -> &mut Self { + self.add_gate(Gate::sz(qubits)) + } + + /// Apply SZ-dagger gate(s) to one or more qubits. + pub fn szdg(&mut self, qubits: &[impl Into + Copy]) -> &mut Self { + self.add_gate(Gate::szdg(qubits)) + } + + /// Apply T gate(s) to one or more qubits. + pub fn t(&mut self, qubits: &[impl Into + Copy]) -> &mut Self { + self.add_gate(Gate::t(qubits)) + } + + /// Apply T-dagger gate(s) to one or more qubits. + pub fn tdg(&mut self, qubits: &[impl Into + Copy]) -> &mut Self { + self.add_gate(Gate::tdg(qubits)) + } + + /// Apply RX rotation(s) to one or more qubits. + /// + /// # Examples + /// + /// ``` + /// use pecos_quantum::TickCircuit; + /// use std::f64::consts::PI; + /// + /// let mut circuit = TickCircuit::new(); + /// // Single qubit + /// circuit.tick().rx(PI / 2.0, &[0]); + /// // Multiple qubits with same angle + /// circuit.tick().rx(PI / 4.0, &[1, 2, 3]); + /// ``` + pub fn rx( + &mut self, + theta: impl Into, + qubits: &[impl Into + Copy], + ) -> &mut Self { + self.add_gate(Gate::rx(theta.into(), qubits)) + } + + /// Apply RY rotation(s) to one or more qubits. + pub fn ry( + &mut self, + theta: impl Into, + qubits: &[impl Into + Copy], + ) -> &mut Self { + self.add_gate(Gate::ry(theta.into(), qubits)) + } + + /// Apply RZ rotation(s) to one or more qubits. + pub fn rz( + &mut self, + theta: impl Into, + qubits: &[impl Into + Copy], + ) -> &mut Self { + self.add_gate(Gate::rz(theta.into(), qubits)) + } + + /// Apply R1XY rotation(s) to one or more qubits. + /// + /// This is a single-qubit rotation parameterized by two angles (theta, phi). + /// + /// # Examples + /// + /// ``` + /// use pecos_quantum::TickCircuit; + /// use std::f64::consts::PI; + /// + /// let mut circuit = TickCircuit::new(); + /// circuit.tick().r1xy(PI / 2.0, PI / 4.0, &[0]); + /// ``` + pub fn r1xy( + &mut self, + theta: impl Into, + phi: impl Into, + qubits: &[impl Into + Copy], + ) -> &mut Self { + self.add_gate(Gate::r1xy(theta.into(), phi.into(), qubits)) } - /// Apply an RZ rotation. - pub fn rz(&mut self, theta: impl Into, q: impl Into) -> &mut Self { - self.add_gate(Gate::rz(theta.into(), &[q.into()])) + /// Apply U gate(s) (general single-qubit unitary) to one or more qubits. + /// + /// The U gate is parameterized by three angles (theta, phi, lambda). + /// + /// # Examples + /// + /// ``` + /// use pecos_quantum::TickCircuit; + /// use std::f64::consts::PI; + /// + /// let mut circuit = TickCircuit::new(); + /// circuit.tick().u(PI / 2.0, 0.0, PI, &[0]); + /// ``` + pub fn u( + &mut self, + theta: impl Into, + phi: impl Into, + lambda: impl Into, + qubits: &[impl Into + Copy], + ) -> &mut Self { + self.add_gate(Gate::u(theta.into(), phi.into(), lambda.into(), qubits)) } // ========================================================================= // Two-qubit gates // ========================================================================= - /// Apply a CNOT (CX) gate. - pub fn cx(&mut self, ctrl: impl Into, tgt: impl Into) -> &mut Self { - self.add_gate(Gate::cx(&[(ctrl.into(), tgt.into())])) + /// Apply CNOT (CX) gate(s) to one or more qubit pairs. + /// + /// # Examples + /// + /// ``` + /// use pecos_quantum::TickCircuit; + /// + /// let mut circuit = TickCircuit::new(); + /// // Single pair + /// circuit.tick().cx(&[(0, 1)]); + /// // Multiple pairs in one call + /// circuit.tick().cx(&[(2, 3), (4, 5), (6, 7)]); + /// ``` + pub fn cx( + &mut self, + pairs: &[(impl Into + Copy, impl Into + Copy)], + ) -> &mut Self { + self.add_gate(Gate::cx(pairs)) + } + + /// Apply CY gate(s) to one or more qubit pairs. + /// + /// # Examples + /// + /// ``` + /// use pecos_quantum::TickCircuit; + /// + /// let mut circuit = TickCircuit::new(); + /// circuit.tick().cy(&[(0, 1)]); + /// circuit.tick().cy(&[(2, 3), (4, 5)]); + /// ``` + pub fn cy( + &mut self, + pairs: &[(impl Into + Copy, impl Into + Copy)], + ) -> &mut Self { + self.add_gate(Gate::cy(pairs)) + } + + /// Apply CZ gate(s) to one or more qubit pairs. + /// + /// # Examples + /// + /// ``` + /// use pecos_quantum::TickCircuit; + /// + /// let mut circuit = TickCircuit::new(); + /// circuit.tick().cz(&[(0, 1)]); + /// circuit.tick().cz(&[(2, 3), (4, 5)]); + /// ``` + pub fn cz( + &mut self, + pairs: &[(impl Into + Copy, impl Into + Copy)], + ) -> &mut Self { + self.add_gate(Gate::cz(pairs)) } - /// Apply an SZZ gate (sqrt-ZZ). - pub fn szz(&mut self, q1: impl Into, q2: impl Into) -> &mut Self { - self.add_gate(Gate::szz(&[(q1.into(), q2.into())])) + /// Apply SZZ gate(s) (sqrt-ZZ) to one or more qubit pairs. + pub fn szz( + &mut self, + pairs: &[(impl Into + Copy, impl Into + Copy)], + ) -> &mut Self { + self.add_gate(Gate::szz(pairs)) + } + + /// Apply SZZ-dagger gate(s) to one or more qubit pairs. + pub fn szzdg( + &mut self, + pairs: &[(impl Into + Copy, impl Into + Copy)], + ) -> &mut Self { + self.add_gate(Gate::szzdg(pairs)) } - /// Apply an SZZ-dagger gate. - pub fn szzdg(&mut self, q1: impl Into, q2: impl Into) -> &mut Self { - self.add_gate(Gate::szzdg(&[(q1.into(), q2.into())])) + /// Apply RXX rotation(s) to one or more qubit pairs. + /// + /// # Examples + /// + /// ``` + /// use pecos_quantum::TickCircuit; + /// use std::f64::consts::PI; + /// + /// let mut circuit = TickCircuit::new(); + /// circuit.tick().rxx(PI / 4.0, &[(0, 1)]); + /// ``` + pub fn rxx( + &mut self, + theta: impl Into, + pairs: &[(impl Into + Copy, impl Into + Copy)], + ) -> &mut Self { + self.add_gate(Gate::rxx(theta.into(), pairs)) } - /// Apply an RZZ rotation. + /// Apply RYY rotation(s) to one or more qubit pairs. + /// + /// # Examples + /// + /// ``` + /// use pecos_quantum::TickCircuit; + /// use std::f64::consts::PI; + /// + /// let mut circuit = TickCircuit::new(); + /// circuit.tick().ryy(PI / 4.0, &[(0, 1)]); + /// ``` + pub fn ryy( + &mut self, + theta: impl Into, + pairs: &[(impl Into + Copy, impl Into + Copy)], + ) -> &mut Self { + self.add_gate(Gate::ryy(theta.into(), pairs)) + } + + /// Apply RZZ rotation(s) to one or more qubit pairs. + /// + /// # Examples + /// + /// ``` + /// use pecos_quantum::TickCircuit; + /// use std::f64::consts::PI; + /// + /// let mut circuit = TickCircuit::new(); + /// // Single pair + /// circuit.tick().rzz(PI / 4.0, &[(0, 1)]); + /// // Multiple pairs with same angle + /// circuit.tick().rzz(PI / 2.0, &[(2, 3), (4, 5)]); + /// ``` pub fn rzz( &mut self, theta: impl Into, - q1: impl Into, - q2: impl Into, + pairs: &[(impl Into + Copy, impl Into + Copy)], ) -> &mut Self { - self.add_gate(Gate::rzz(theta.into(), &[(q1.into(), q2.into())])) + self.add_gate(Gate::rzz(theta.into(), pairs)) } // ========================================================================= // State preparation and measurement // ========================================================================= - /// Prepare a qubit in the |0⟩ state. + /// Prepare qubit(s) in the |0⟩ state. /// /// Returns a [`TickPrepHandle`] that allows attaching metadata via `.meta()`. /// This breaks the chain - only `.meta()` can be called on the result. - pub fn pz(mut self, q: impl Into) -> TickPrepHandle<'a> { - let gate_idx = self.add_gate_get_idx(Gate::prep(&[q.into()])); + /// + /// # Examples + /// + /// ``` + /// use pecos_quantum::TickCircuit; + /// + /// let mut circuit = TickCircuit::new(); + /// circuit.tick().pz(&[0]); // Single qubit + /// circuit.tick().pz(&[1, 2, 3]); // Multiple qubits + /// ``` + pub fn pz(mut self, qubits: &[impl Into + Copy]) -> TickPrepHandle<'a> { + let gate_idx = self.add_gate_get_idx(Gate::prep(qubits)); TickPrepHandle { circuit: self.circuit, tick_idx: self.tick_idx, @@ -626,19 +1299,29 @@ impl<'a> TickHandle<'a> { } } - /// Prepare a qubit (alias for pz). + /// Prepare qubit(s) (alias for pz). /// /// Returns a [`TickPrepHandle`] that allows attaching metadata via `.meta()`. - pub fn prep(self, q: impl Into) -> TickPrepHandle<'a> { - self.pz(q) + pub fn prep(self, qubits: &[impl Into + Copy]) -> TickPrepHandle<'a> { + self.pz(qubits) } - /// Measure a qubit in the Z basis. + /// Measure qubit(s) in the Z basis. /// /// Returns a [`TickMeasureHandle`] that allows attaching metadata via `.meta()`. /// This breaks the chain - only `.meta()` can be called on the result. - pub fn mz(mut self, q: impl Into) -> TickMeasureHandle<'a> { - let gate_idx = self.add_gate_get_idx(Gate::measure(&[q.into()])); + /// + /// # Examples + /// + /// ``` + /// use pecos_quantum::TickCircuit; + /// + /// let mut circuit = TickCircuit::new(); + /// circuit.tick().mz(&[0]); // Single qubit + /// circuit.tick().mz(&[1, 2, 3]); // Multiple qubits + /// ``` + pub fn mz(mut self, qubits: &[impl Into + Copy]) -> TickMeasureHandle<'a> { + let gate_idx = self.add_gate_get_idx(Gate::measure(qubits)); TickMeasureHandle { circuit: self.circuit, tick_idx: self.tick_idx, @@ -646,11 +1329,20 @@ impl<'a> TickHandle<'a> { } } - /// Measure and free a qubit (destructive measurement). + /// Measure and free qubit(s) (destructive measurement). /// /// Returns a [`TickMeasureHandle`] that allows attaching metadata via `.meta()`. - pub fn measure_free(mut self, q: impl Into) -> TickMeasureHandle<'a> { - let gate_idx = self.add_gate_get_idx(Gate::simple(GateType::MeasureFree, vec![q.into()])); + /// + /// # Examples + /// + /// ``` + /// use pecos_quantum::TickCircuit; + /// + /// let mut circuit = TickCircuit::new(); + /// circuit.tick().measure_free(&[0, 1]); + /// ``` + pub fn measure_free(mut self, qubits: &[impl Into + Copy]) -> TickMeasureHandle<'a> { + let gate_idx = self.add_gate_get_idx(Gate::measure_free(qubits)); TickMeasureHandle { circuit: self.circuit, tick_idx: self.tick_idx, @@ -662,24 +1354,50 @@ impl<'a> TickHandle<'a> { // Resource management // ========================================================================= - /// Allocate a qubit. - pub fn qalloc(&mut self, q: impl Into) -> &mut Self { - self.add_gate(Gate::qalloc(&[q.into()])) + /// Allocate one or more qubits. + /// + /// # Examples + /// + /// ``` + /// use pecos_quantum::TickCircuit; + /// + /// let mut circuit = TickCircuit::new(); + /// circuit.tick().qalloc(&[0, 1, 2, 3]); + /// ``` + pub fn qalloc(&mut self, qubits: &[impl Into + Copy]) -> &mut Self { + self.add_gate(Gate::qalloc(qubits)) } - /// Free a qubit. - pub fn qfree(&mut self, q: impl Into) -> &mut Self { - self.add_gate(Gate::qfree(&[q.into()])) + /// Free one or more qubits. + pub fn qfree(&mut self, qubits: &[impl Into + Copy]) -> &mut Self { + self.add_gate(Gate::qfree(qubits)) } // ========================================================================= // Timing // ========================================================================= - /// Insert an idle (wait) operation. - pub fn idle(&mut self, duration: impl Into, q: impl Into) -> &mut Self { + /// Insert an idle (wait) operation for one or more qubits. + /// + /// # Examples + /// + /// ``` + /// use pecos_quantum::TickCircuit; + /// + /// let mut circuit = TickCircuit::new(); + /// // Idle for 100 nanoseconds + /// circuit.tick().idle(100, &[0, 1, 2]); + /// ``` + pub fn idle( + &mut self, + duration: impl Into, + qubits: &[impl Into + Copy], + ) -> &mut Self { let ns: Nanoseconds = duration.into(); - self.add_gate(Gate::idle(ns.as_f64(), vec![q.into()])) + self.add_gate(Gate::idle( + ns.as_f64(), + qubits.iter().map(|&q| q.into()).collect(), + )) } } @@ -775,8 +1493,8 @@ impl From<&TickCircuit> for DagCircuit { /// use pecos_quantum::{DagCircuit, TickCircuit}; /// /// let mut tc = TickCircuit::new(); - /// tc.tick().h(0); - /// tc.tick().cx(0, 1); + /// tc.tick().h(&[0]); + /// tc.tick().cx(&[(0, 1)]); /// /// let dag = DagCircuit::from(&tc); /// assert_eq!(dag.gate_count(), 2); @@ -839,14 +1557,14 @@ mod tests { // Preps and measurements break the chain (return handles with only .meta()) // For multiple preps in the same tick, add gates directly to the tick - tc.tick().pz(0); // tick 0: first prep + tc.tick().pz(&[0]); // tick 0: first prep // If we want both preps in tick 0, we'd use the Tick API directly. // Here we use separate ticks for clarity: - tc.tick().pz(1); // tick 1: second prep - tc.tick().h(0); // tick 2 - tc.tick().cx(0, 1); // tick 3 - tc.tick().mz(0); // tick 4 - tc.tick().mz(1); // tick 5 + tc.tick().pz(&[1]); // tick 1: second prep + tc.tick().h(&[0]); // tick 2 + tc.tick().cx(&[(0, 1)]); // tick 3 + tc.tick().mz(&[0]); // tick 4 + tc.tick().mz(&[1]); // tick 5 assert_eq!(tc.num_ticks(), 6); assert_eq!(tc.gate_count(), 6); @@ -856,37 +1574,23 @@ mod tests { fn test_multiple_preps_same_tick() { let mut tc = TickCircuit::new(); - // To add multiple preps to the same tick, allocate the tick first - // then add gates directly - let tick_idx = tc.tick().index(); - // tick() consumed the handle by calling index(), so get the tick - tc.get_tick_mut(tick_idx) - .unwrap() - .add_gate(Gate::prep(&[0])); - tc.get_tick_mut(tick_idx) - .unwrap() - .add_gate(Gate::prep(&[1])); - - tc.tick().h(0); - tc.tick().cx(0, 1); - - // Multiple measurements in same tick - let meas_tick = tc.tick().index(); - tc.get_tick_mut(meas_tick) - .unwrap() - .add_gate(Gate::measure(&[0])); - tc.get_tick_mut(meas_tick) - .unwrap() - .add_gate(Gate::measure(&[1])); + // To add multiple preps to the same tick, use bulk prep + tc.tick().pz(&[0, 1]); // Both preps in tick 0 + + tc.tick().h(&[0]); + tc.tick().cx(&[(0, 1)]); + + // Multiple measurements in same tick using bulk measurement + tc.tick().mz(&[0, 1]); assert_eq!(tc.num_ticks(), 4); - assert_eq!(tc.gate_count(), 6); + assert_eq!(tc.gate_count(), 4); // 1 bulk prep, 1 H, 1 CX, 1 bulk measure // Check tick contents - assert_eq!(tc.get_tick(0).unwrap().len(), 2); // Two preps + assert_eq!(tc.get_tick(0).unwrap().len(), 1); // One bulk prep gate assert_eq!(tc.get_tick(1).unwrap().len(), 1); // One H assert_eq!(tc.get_tick(2).unwrap().len(), 1); // One CX - assert_eq!(tc.get_tick(3).unwrap().len(), 2); // Two measurements + assert_eq!(tc.get_tick(3).unwrap().len(), 1); // One bulk measurement } #[test] @@ -894,10 +1598,10 @@ mod tests { let mut tc = TickCircuit::new(); tc.tick() - .h(0) + .h(&[0]) .meta("duration", Attribute::Float(50.0)) .meta("error_rate", Attribute::Float(0.001)) - .x(1) + .x(&[1]) .meta("duration", Attribute::Float(50.0)); let tick = tc.get_tick(0).unwrap(); @@ -920,8 +1624,8 @@ mod tests { let mut tc = TickCircuit::new(); // meta() before any gates = tick-level metadata - tc.tick().meta("round", Attribute::Int(0)).h(0); - tc.tick().meta("round", Attribute::Int(1)).cx(0, 1); + tc.tick().meta("round", Attribute::Int(0)).h(&[0]); + tc.tick().meta("round", Attribute::Int(1)).cx(&[(0, 1)]); assert_eq!( tc.get_tick(0).unwrap().get_attr("round"), @@ -951,12 +1655,12 @@ mod tests { let mut tc = TickCircuit::new(); // Regular gates chain within a tick - tc.tick().h(0).x(1).y(2).z(3); - tc.tick().cx(0, 1).szz(2, 3); + tc.tick().h(&[0]).x(&[1]).y(&[2]).z(&[3]); + tc.tick().cx(&[(0, 1)]).szz(&[(2, 3)]); // But preps and measurements break the chain - tc.tick().pz(0); // breaks chain - tc.tick().mz(0); // breaks chain + tc.tick().pz(&[0]); // breaks chain + tc.tick().mz(&[0]); // breaks chain assert_eq!(tc.num_ticks(), 4); assert_eq!(tc.gate_count(), 8); @@ -968,10 +1672,12 @@ mod tests { // Preps and measurements allow .meta() before breaking tc.tick() - .pz(0) + .pz(&[0]) .meta("reason", Attribute::String("init".into())); - tc.tick().h(0); - tc.tick().mz(0).meta("basis", Attribute::String("Z".into())); + tc.tick().h(&[0]); + tc.tick() + .mz(&[0]) + .meta("basis", Attribute::String("Z".into())); assert_eq!(tc.num_ticks(), 3); @@ -990,7 +1696,7 @@ mod tests { fn test_circuit_meta() { let mut tc = TickCircuit::new(); tc.set_meta("name", Attribute::String("bell_state".to_string())); - tc.tick().h(0); + tc.tick().h(&[0]); assert_eq!( tc.get_meta("name"), @@ -1004,9 +1710,9 @@ mod tests { tc.set_meta("circuit_name", Attribute::String("test".to_string())); // Build a small circuit - tc.tick().h(0).x(1); // Tick 0: parallel H and X - tc.tick().cx(0, 1); // Tick 1: CX - tc.tick().h(0); // Tick 2: H + tc.tick().h(&[0]).x(&[1]); // Tick 0: parallel H and X + tc.tick().cx(&[(0, 1)]); // Tick 1: CX + tc.tick().h(&[0]); // Tick 2: H let dag = DagCircuit::from(&tc); @@ -1058,9 +1764,9 @@ mod tests { #[test] fn test_round_trip_tick_to_dag_to_tick() { let mut tc1 = TickCircuit::new(); - tc1.tick().h(0); - tc1.tick().cx(0, 1); - tc1.tick().h(1); + tc1.tick().h(&[0]); + tc1.tick().cx(&[(0, 1)]); + tc1.tick().h(&[1]); // Convert to DAG and back let dag = DagCircuit::from(&tc1); @@ -1085,8 +1791,8 @@ mod tests { tc1.tick() .meta("round", Attribute::Int(0)) .meta("syndrome_type", Attribute::String("X".to_string())) - .h(0); - tc1.tick().meta("round", Attribute::Int(1)).cx(0, 1); + .h(&[0]); + tc1.tick().meta("round", Attribute::Int(1)).cx(&[(0, 1)]); // Convert to DAG let dag = DagCircuit::from(&tc1); @@ -1130,7 +1836,7 @@ mod tests { #[test] fn test_active_qubits() { let mut tc = TickCircuit::new(); - tc.tick().h(0).x(1).cx(2, 3); + tc.tick().h(&[0]).x(&[1]).cx(&[(2, 3)]); let tick = tc.get_tick(0).unwrap(); let active = tick.active_qubits(); @@ -1146,7 +1852,7 @@ mod tests { #[test] fn test_uses_qubit() { let mut tc = TickCircuit::new(); - tc.tick().h(0).cx(1, 2); + tc.tick().h(&[0]).cx(&[(1, 2)]); let tick = tc.get_tick(0).unwrap(); @@ -1159,7 +1865,7 @@ mod tests { #[test] fn test_find_conflicts() { let mut tc = TickCircuit::new(); - tc.tick().h(0).cx(1, 2); + tc.tick().h(&[0]).cx(&[(1, 2)]); let tick = tc.get_tick(0).unwrap(); @@ -1194,7 +1900,7 @@ mod tests { fn test_try_add_gate_conflict() { let mut tc = TickCircuit::new(); let mut handle = tc.tick(); - handle.h(0); + handle.h(&[0]); // Try to add another gate on the same qubit - should fail let result = handle.try_add_gate(Gate::x(&[0])); @@ -1213,21 +1919,21 @@ mod tests { fn test_qubit_conflict_panics() { let mut tc = TickCircuit::new(); // This should panic because qubit 0 is used twice in the same tick - tc.tick().h(0).x(0); + tc.tick().h(&[0]).x(&[0]); } #[test] fn test_two_qubit_gate_conflict() { let mut tc = TickCircuit::new(); let mut handle = tc.tick(); - handle.cx(0, 1); + handle.cx(&[(0, 1)]); // Both qubits of CX should be marked as in use let result = handle.try_add_gate(Gate::h(&[0])); assert!(result.is_err()); let mut handle2 = tc.tick(); - handle2.cx(2, 3); + handle2.cx(&[(2, 3)]); let result = handle2.try_add_gate(Gate::h(&[3])); assert!(result.is_err()); } @@ -1241,7 +1947,7 @@ mod tests { ("error_rate".to_string(), Attribute::Float(0.001)), ]); - tc.tick().h(0).metas(attrs).x(1); + tc.tick().h(&[0]).metas(attrs).x(&[1]); let tick = tc.get_tick(0).unwrap(); assert_eq!( @@ -1264,7 +1970,7 @@ mod tests { ]); // metas() before any gates = tick-level metadata - tc.tick().metas(attrs).h(0); + tc.tick().metas(attrs).h(&[0]); let tick = tc.get_tick(0).unwrap(); assert_eq!(tick.get_attr("round"), Some(&Attribute::Int(0))); @@ -1284,7 +1990,7 @@ mod tests { ]); tc.set_metas(attrs); - tc.tick().h(0); + tc.tick().h(&[0]); assert_eq!( tc.get_meta("name"), @@ -1292,4 +1998,324 @@ mod tests { ); assert_eq!(tc.get_meta("version"), Some(&Attribute::Int(1))); } + + #[test] + fn test_bulk_operations() { + let mut tc = TickCircuit::new(); + + // Test bulk single-qubit gates + tc.tick().h(&[0, 1, 2, 3]); + assert_eq!(tc.get_tick(0).unwrap().len(), 1); // One gate with 4 qubits + + // Test bulk two-qubit gates + tc.tick().cx(&[(0, 1), (2, 3)]); + assert_eq!(tc.get_tick(1).unwrap().len(), 1); // One gate with 2 pairs + + // Test bulk prep and measure + tc.tick().pz(&[0, 1, 2, 3]); + tc.tick().mz(&[0, 1, 2, 3]); + assert_eq!(tc.get_tick(2).unwrap().len(), 1); + assert_eq!(tc.get_tick(3).unwrap().len(), 1); + + // Test bulk qalloc/qfree + tc.tick().qalloc(&[4, 5, 6]); + tc.tick().qfree(&[4, 5, 6]); + assert_eq!(tc.get_tick(4).unwrap().len(), 1); + assert_eq!(tc.get_tick(5).unwrap().len(), 1); + } + + #[test] + fn test_iteration_helpers() { + let mut tc = TickCircuit::new(); + tc.tick().h(&[0, 1]); + tc.tick().cx(&[(0, 1)]); + tc.tick().mz(&[0, 1]); + + // Test iter_gates + let gates: Vec<_> = tc.iter_gates().collect(); + assert_eq!(gates.len(), 3); + + // Test iter_gates_with_tick + let gates_with_tick: Vec<_> = tc.iter_gates_with_tick().collect(); + assert_eq!(gates_with_tick.len(), 3); + assert_eq!(gates_with_tick[0].0, 0); // First gate is in tick 0 + assert_eq!(gates_with_tick[1].0, 1); // Second gate is in tick 1 + + // Test iter_ticks + let ticks: Vec<_> = tc.iter_ticks().collect(); + assert_eq!(ticks.len(), 3); + + // Test iter_gates_by_type + let h_gates: Vec<_> = tc.iter_gates_by_type(GateType::H).collect(); + assert_eq!(h_gates.len(), 1); + + // Test all_qubits + let qubits = tc.all_qubits(); + assert_eq!(qubits.len(), 2); + assert!(qubits.contains(&QubitId::from(0))); + assert!(qubits.contains(&QubitId::from(1))); + + // Test gate_counts_by_type + let counts = tc.gate_counts_by_type(); + assert_eq!(counts.get(&GateType::H), Some(&1)); + assert_eq!(counts.get(&GateType::CX), Some(&1)); + assert_eq!(counts.get(&GateType::Measure), Some(&1)); + } + + #[test] + fn test_clear() { + let mut tc = TickCircuit::new(); + tc.set_meta("name", Attribute::String("test".to_string())); + tc.tick().h(&[0]); + tc.tick().cx(&[(0, 1)]); + + assert_eq!(tc.num_ticks(), 2); + assert!(tc.get_meta("name").is_some()); + + tc.clear(); + + assert_eq!(tc.num_ticks(), 0); + assert_eq!(tc.gate_count(), 0); + assert!(tc.get_meta("name").is_none()); + assert_eq!(tc.next_tick_index(), 0); + } + + #[test] + fn test_reset() { + let mut tc = TickCircuit::new(); + tc.set_meta("name", Attribute::String("test".to_string())); + tc.tick().h(&[0]); + tc.tick().cx(&[(0, 1)]); + + assert_eq!(tc.num_ticks(), 2); + + tc.reset(); + + assert_eq!(tc.num_ticks(), 0); + assert_eq!(tc.gate_count(), 0); + assert!(tc.get_meta("name").is_none()); + assert_eq!(tc.next_tick_index(), 0); + + // Can reuse the circuit + tc.tick().x(&[0]); + assert_eq!(tc.num_ticks(), 1); + } + + #[test] + fn test_reserve_ticks() { + let mut tc = TickCircuit::new(); + tc.reserve_ticks(4); + + assert_eq!(tc.ticks().len(), 4); + assert_eq!(tc.next_tick_index(), 4); + + // All ticks are empty + for tick in tc.ticks() { + assert!(tick.is_empty()); + } + + // New tick() starts after reserved ticks + tc.tick().h(&[0]); + assert_eq!(tc.ticks().len(), 5); + assert_eq!(tc.next_tick_index(), 5); + } + + #[test] + fn test_insert_tick() { + let mut tc = TickCircuit::new(); + tc.tick().h(&[0]); // Tick 0 + tc.tick().cx(&[(0, 1)]); // Tick 1 + + // Insert between them + tc.insert_tick(1).x(&[1]); + + assert_eq!(tc.num_ticks(), 3); + + // Check order: H at 0, X at 1, CX at 2 + let tick0 = tc.get_tick(0).unwrap(); + assert_eq!(tick0.gates()[0].gate_type, GateType::H); + + let tick1 = tc.get_tick(1).unwrap(); + assert_eq!(tick1.gates()[0].gate_type, GateType::X); + + let tick2 = tc.get_tick(2).unwrap(); + assert_eq!(tick2.gates()[0].gate_type, GateType::CX); + } + + #[test] + fn test_insert_tick_at_beginning() { + let mut tc = TickCircuit::new(); + tc.tick().h(&[0]); + tc.tick().x(&[1]); + + tc.insert_tick(0).z(&[2]); + + assert_eq!(tc.num_ticks(), 3); + + // Z should now be at tick 0 + let tick0 = tc.get_tick(0).unwrap(); + assert_eq!(tick0.gates()[0].gate_type, GateType::Z); + } + + #[test] + fn test_insert_tick_at_end() { + let mut tc = TickCircuit::new(); + tc.tick().h(&[0]); + + // Insert at the end (same as tick()) + tc.insert_tick(1).x(&[1]); + + assert_eq!(tc.num_ticks(), 2); + + let tick1 = tc.get_tick(1).unwrap(); + assert_eq!(tick1.gates()[0].gate_type, GateType::X); + } + + #[test] + fn test_tick_at() { + let mut tc = TickCircuit::new(); + tc.reserve_ticks(3); + + // Add gates to ticks out of order + tc.tick_at(2).cx(&[(0, 1)]); + tc.tick_at(0).h(&[0]); + tc.tick_at(1).x(&[1]); + + assert_eq!(tc.num_ticks(), 3); + + // Check each tick has the right gate + assert_eq!(tc.get_tick(0).unwrap().gates()[0].gate_type, GateType::H); + assert_eq!(tc.get_tick(1).unwrap().gates()[0].gate_type, GateType::X); + assert_eq!(tc.get_tick(2).unwrap().gates()[0].gate_type, GateType::CX); + } + + #[test] + fn test_tick_at_add_more_gates() { + let mut tc = TickCircuit::new(); + tc.tick().h(&[0]); // Tick 0 with H + + // Add more gates to tick 0 + tc.tick_at(0).x(&[1]); + + assert_eq!(tc.num_ticks(), 1); + assert_eq!(tc.get_tick(0).unwrap().len(), 2); + } + + #[test] + #[should_panic(expected = "tick_at index 5 out of bounds")] + fn test_tick_at_out_of_bounds() { + let mut tc = TickCircuit::new(); + tc.tick().h(&[0]); + tc.tick_at(5); // Should panic + } + + #[test] + #[should_panic(expected = "insert_tick index 5 out of bounds")] + fn test_insert_tick_out_of_bounds() { + let mut tc = TickCircuit::new(); + tc.tick().h(&[0]); + tc.insert_tick(5); // Should panic + } + + #[test] + fn test_tick_discard() { + let mut tc = TickCircuit::new(); + tc.tick().h(&[0]).x(&[1]).cx(&[(2, 3)]); + + let tick = tc.get_tick_mut(0).unwrap(); + assert_eq!(tick.len(), 3); + + // Discard gates using qubit 0 and qubit 2 + let removed = tick.discard(&[QubitId::from(0), QubitId::from(2)]); + + assert_eq!(removed, 2); // H on q0 and CX on q2,q3 + assert_eq!(tick.len(), 1); // Only X on q1 remains + assert_eq!(tick.gates()[0].gate_type, GateType::X); + } + + #[test] + fn test_tick_discard_no_match() { + let mut tc = TickCircuit::new(); + tc.tick().h(&[0]).x(&[1]); + + let tick = tc.get_tick_mut(0).unwrap(); + let removed = tick.discard(&[QubitId::from(5), QubitId::from(6)]); + + assert_eq!(removed, 0); + assert_eq!(tick.len(), 2); + } + + #[test] + fn test_tick_discard_preserves_attrs() { + let mut tc = TickCircuit::new(); + tc.tick() + .h(&[0]) + .meta("h_attr", Attribute::Int(1)) + .x(&[1]) + .meta("x_attr", Attribute::Int(2)) + .z(&[2]) + .meta("z_attr", Attribute::Int(3)); + + let tick = tc.get_tick_mut(0).unwrap(); + + // Remove the X gate (index 1) + let removed = tick.discard(&[QubitId::from(1)]); + assert_eq!(removed, 1); + assert_eq!(tick.len(), 2); + + // H attr should still be at index 0 + assert_eq!(tick.get_gate_attr(0, "h_attr"), Some(&Attribute::Int(1))); + // Z attr should now be at index 1 (shifted from 2) + assert_eq!(tick.get_gate_attr(1, "z_attr"), Some(&Attribute::Int(3))); + // X attr should be gone + assert!(tick.get_gate_attr(1, "x_attr").is_none()); + } + + #[test] + fn test_tick_remove_gate() { + let mut tc = TickCircuit::new(); + tc.tick().h(&[0]).x(&[1]).z(&[2]); + + let tick = tc.get_tick_mut(0).unwrap(); + assert_eq!(tick.len(), 3); + + let removed = tick.remove_gate(1); // Remove X + assert!(removed.is_some()); + assert_eq!(removed.unwrap().gate_type, GateType::X); + assert_eq!(tick.len(), 2); + + // Check remaining gates + assert_eq!(tick.gates()[0].gate_type, GateType::H); + assert_eq!(tick.gates()[1].gate_type, GateType::Z); + } + + #[test] + fn test_tick_remove_gate_out_of_bounds() { + let mut tc = TickCircuit::new(); + tc.tick().h(&[0]); + + let tick = tc.get_tick_mut(0).unwrap(); + let removed = tick.remove_gate(5); + assert!(removed.is_none()); + assert_eq!(tick.len(), 1); + } + + #[test] + fn test_circuit_discard() { + let mut tc = TickCircuit::new(); + tc.tick().h(&[0]).x(&[1]).cx(&[(2, 3)]); + + let removed = tc.discard(&[0, 2], 0); + assert_eq!(removed, Some(2)); + assert_eq!(tc.get_tick(0).unwrap().len(), 1); + } + + #[test] + fn test_circuit_discard_invalid_tick() { + let mut tc = TickCircuit::new(); + tc.tick().h(&[0]); + + let removed = tc.discard(&[0], 5); + assert_eq!(removed, None); + } } diff --git a/crates/pecos-quest/src/quantum_engine.rs b/crates/pecos-quest/src/quantum_engine.rs index ddc17f70e..7313d0765 100644 --- a/crates/pecos-quest/src/quantum_engine.rs +++ b/crates/pecos-quest/src/quantum_engine.rs @@ -238,6 +238,12 @@ impl Engine for QuestStateVecEngine { } } } + GateType::SY | GateType::SYdg | GateType::RXX | GateType::RYY => { + return Err(PecosError::Processing(format!( + "Gate type {:?} is not yet supported by QuestStateVecEngine", + cmd.gate_type + ))); + } } } @@ -494,6 +500,12 @@ impl Engine for QuestDensityMatrixEngine { } } } + GateType::SY | GateType::SYdg | GateType::RXX | GateType::RYY => { + return Err(PecosError::Processing(format!( + "Gate type {:?} is not yet supported by QuestDensityMatrixEngine", + cmd.gate_type + ))); + } } } @@ -1224,6 +1236,12 @@ impl Engine for QuestCudaStateVecEngine { | GateType::QFree => { // No operation needed (QFree is just a marker for qubit lifecycle) } + GateType::SY | GateType::SYdg | GateType::RXX | GateType::RYY => { + return Err(PecosError::Processing(format!( + "Gate type {:?} is not yet supported by QuestCudaStateVecEngine", + cmd.gate_type + ))); + } } } diff --git a/crates/pecos-qulacs/src/bridge.rs b/crates/pecos-qulacs/src/bridge.rs index 2da3fa1ad..ce298f2de 100644 --- a/crates/pecos-qulacs/src/bridge.rs +++ b/crates/pecos-qulacs/src/bridge.rs @@ -38,10 +38,9 @@ pub mod ffi { fn apply_sdag(state: Pin<&mut QulacsState>, qubit: usize); fn apply_t(state: Pin<&mut QulacsState>, qubit: usize); fn apply_tdag(state: Pin<&mut QulacsState>, qubit: usize); - fn apply_sqrt_x(state: Pin<&mut QulacsState>, qubit: usize); - fn apply_sqrt_xdag(state: Pin<&mut QulacsState>, qubit: usize); - fn apply_sqrt_y(state: Pin<&mut QulacsState>, qubit: usize); - fn apply_sqrt_ydag(state: Pin<&mut QulacsState>, qubit: usize); + + // NOTE: sqrt_x, sqrt_xdag, sqrt_y, sqrt_ydag removed - we use trait + // decompositions instead for consistency with StateVec. // Rotation gates fn apply_rx(state: Pin<&mut QulacsState>, qubit: usize, angle: f64); diff --git a/crates/pecos-qulacs/src/lib.rs b/crates/pecos-qulacs/src/lib.rs index fb2cbd34b..3c0c4659e 100644 --- a/crates/pecos-qulacs/src/lib.rs +++ b/crates/pecos-qulacs/src/lib.rs @@ -269,29 +269,9 @@ where self } - fn sx(&mut self, q: I) -> &mut Self { - let qulacs_qubit = self.convert_qubit_index(q.to_index()); - ffi::apply_sqrt_x(self.state.pin_mut(), qulacs_qubit); - self - } - - fn sxdg(&mut self, q: I) -> &mut Self { - let qulacs_qubit = self.convert_qubit_index(q.to_index()); - ffi::apply_sqrt_xdag(self.state.pin_mut(), qulacs_qubit); - self - } - - fn sy(&mut self, q: I) -> &mut Self { - let qulacs_qubit = self.convert_qubit_index(q.to_index()); - ffi::apply_sqrt_y(self.state.pin_mut(), qulacs_qubit); - self - } - - fn sydg(&mut self, q: I) -> &mut Self { - let qulacs_qubit = self.convert_qubit_index(q.to_index()); - ffi::apply_sqrt_ydag(self.state.pin_mut(), qulacs_qubit); - self - } + // NOTE: sx, sxdg, sy, sydg use the default trait implementations to ensure + // consistency with StateVec's decomposition. The native Qulacs sqrt gates + // have different conventions that cause state vector mismatches. fn cx(&mut self, q1: I, q2: I) -> &mut Self { let qulacs_q1 = self.convert_qubit_index(q1.to_index()); diff --git a/python/pecos-rslib/src/dag_circuit_bindings.rs b/python/pecos-rslib/src/dag_circuit_bindings.rs index 63ce48f2e..fd48e0602 100644 --- a/python/pecos-rslib/src/dag_circuit_bindings.rs +++ b/python/pecos-rslib/src/dag_circuit_bindings.rs @@ -229,6 +229,38 @@ impl PyGateType { } } + #[classattr] + #[pyo3(name = "SX")] + fn sx() -> Self { + Self { + inner: GateType::SX, + } + } + + #[classattr] + #[pyo3(name = "SXdg")] + fn sxdg() -> Self { + Self { + inner: GateType::SXdg, + } + } + + #[classattr] + #[pyo3(name = "SY")] + fn sy() -> Self { + Self { + inner: GateType::SY, + } + } + + #[classattr] + #[pyo3(name = "SYdg")] + fn sydg() -> Self { + Self { + inner: GateType::SYdg, + } + } + #[classattr] #[pyo3(name = "T")] fn t() -> Self { @@ -243,6 +275,12 @@ impl PyGateType { } } + #[classattr] + #[pyo3(name = "I")] + fn i() -> Self { + Self { inner: GateType::I } + } + #[classattr] #[pyo3(name = "CX")] fn cx() -> Self { @@ -251,6 +289,22 @@ impl PyGateType { } } + #[classattr] + #[pyo3(name = "CY")] + fn cy() -> Self { + Self { + inner: GateType::CY, + } + } + + #[classattr] + #[pyo3(name = "CZ")] + fn cz() -> Self { + Self { + inner: GateType::CZ, + } + } + #[classattr] #[pyo3(name = "RX")] fn rx() -> Self { @@ -275,6 +329,44 @@ impl PyGateType { } } + #[classattr] + #[pyo3(name = "RXX")] + fn rxx() -> Self { + Self { + inner: GateType::RXX, + } + } + + #[classattr] + #[pyo3(name = "RYY")] + fn ryy() -> Self { + Self { + inner: GateType::RYY, + } + } + + #[classattr] + #[pyo3(name = "RZZ")] + fn rzz_attr() -> Self { + Self { + inner: GateType::RZZ, + } + } + + #[classattr] + #[pyo3(name = "R1XY")] + fn r1xy() -> Self { + Self { + inner: GateType::R1XY, + } + } + + #[classattr] + #[pyo3(name = "U")] + fn u() -> Self { + Self { inner: GateType::U } + } + #[classattr] #[pyo3(name = "Measure")] fn measure() -> Self { @@ -441,6 +533,46 @@ impl PyGate { } } + /// Create an Identity gate. + #[staticmethod] + fn i(qubits: Vec) -> Self { + Self { + inner: Gate::i(&qubits), + } + } + + /// Create an SX gate (sqrt-X). + #[staticmethod] + fn sx(qubits: Vec) -> Self { + Self { + inner: Gate::sx(&qubits), + } + } + + /// Create an `SXdg` gate (sqrt-X dagger). + #[staticmethod] + fn sxdg(qubits: Vec) -> Self { + Self { + inner: Gate::sxdg(&qubits), + } + } + + /// Create an SY gate (sqrt-Y). + #[staticmethod] + fn sy(qubits: Vec) -> Self { + Self { + inner: Gate::sy(&qubits), + } + } + + /// Create an `SYdg` gate (sqrt-Y dagger). + #[staticmethod] + fn sydg(qubits: Vec) -> Self { + Self { + inner: Gate::sydg(&qubits), + } + } + /// Create a CX (CNOT) gate. #[staticmethod] fn cx(pairs: Vec<(usize, usize)>) -> Self { @@ -449,6 +581,22 @@ impl PyGate { } } + /// Create a CY gate. + #[staticmethod] + fn cy(pairs: Vec<(usize, usize)>) -> Self { + Self { + inner: Gate::cy(&pairs), + } + } + + /// Create a CZ gate. + #[staticmethod] + fn cz(pairs: Vec<(usize, usize)>) -> Self { + Self { + inner: Gate::cz(&pairs), + } + } + /// Create an RX gate. #[staticmethod] fn rx(angle: f64, qubits: Vec) -> Self { @@ -473,6 +621,57 @@ impl PyGate { } } + /// Create an RXX gate. + #[staticmethod] + fn rxx(angle: f64, pairs: Vec<(usize, usize)>) -> Self { + Self { + inner: Gate::rxx(Angle64::from_radians(angle), &pairs), + } + } + + /// Create an RYY gate. + #[staticmethod] + fn ryy(angle: f64, pairs: Vec<(usize, usize)>) -> Self { + Self { + inner: Gate::ryy(Angle64::from_radians(angle), &pairs), + } + } + + /// Create an RZZ gate. + #[staticmethod] + #[pyo3(name = "rzz")] + fn rzz_gate(angle: f64, pairs: Vec<(usize, usize)>) -> Self { + Self { + inner: Gate::rzz(Angle64::from_radians(angle), &pairs), + } + } + + /// Create an R1XY gate. + #[staticmethod] + fn r1xy(theta: f64, phi: f64, qubits: Vec) -> Self { + Self { + inner: Gate::r1xy( + Angle64::from_radians(theta), + Angle64::from_radians(phi), + &qubits, + ), + } + } + + /// Create a U gate. + #[staticmethod] + #[pyo3(name = "u")] + fn u_gate(theta: f64, phi: f64, lam: f64, qubits: Vec) -> Self { + Self { + inner: Gate::u( + Angle64::from_radians(theta), + Angle64::from_radians(phi), + Angle64::from_radians(lam), + &qubits, + ), + } + } + /// Create a Measure gate. #[staticmethod] fn measure(qubits: Vec) -> Self { @@ -1563,6 +1762,52 @@ impl PyTick { self.inner.uses_qubit(QubitId::from(qubit)) } + /// Check if any of the given qubits are already in use in this tick. + /// + /// Returns a list of conflicting qubit IDs. + fn find_conflicts(&self, qubits: Vec) -> Vec { + let qubit_ids: Vec = qubits.into_iter().map(QubitId::from).collect(); + self.inner + .find_conflicts(&qubit_ids) + .into_iter() + .map(|q| PyQubitId { inner: q }) + .collect() + } + + /// Add a gate to this tick. + /// + /// Returns the index of the added gate within this tick. + fn add_gate(&mut self, gate: &PyGate) -> usize { + self.inner.add_gate(gate.inner.clone()) + } + + /// Try to add a gate to this tick, returning an error if any qubit is already in use. + /// + /// Returns the gate index if successful. + /// + /// Raises: + /// `ValueError`: If any qubit in the gate is already used by another gate in this tick. + fn try_add_gate(&mut self, gate: &PyGate) -> PyResult { + self.inner + .try_add_gate(gate.inner.clone()) + .map_err(|e| PyErr::new::(e.to_string())) + } + + /// Remove all gates that use any of the specified qubits. + /// + /// Returns the number of gates removed. + fn discard(&mut self, qubits: Vec) -> usize { + let qubit_ids: Vec = qubits.into_iter().map(QubitId::from).collect(); + self.inner.discard(&qubit_ids) + } + + /// Remove a specific gate by index. + /// + /// Returns the removed gate, or None if the index is out of bounds. + fn remove_gate(&mut self, idx: usize) -> Option { + self.inner.remove_gate(idx).map(|g| PyGate { inner: g }) + } + fn __repr__(&self) -> String { format!("Tick(gates={})", self.inner.len()) } @@ -1644,6 +1889,154 @@ impl PyTickCircuit { .map(|attr| attribute_to_py(py, attr)) } + // ========================================================================= + // Circuit manipulation + // ========================================================================= + + /// Clear the circuit and start fresh. + /// + /// This completely replaces the circuit with a new empty instance. + /// For performance-critical code, consider using `reset()` instead. + fn clear(&mut self) { + self.inner.clear(); + } + + /// Reset the circuit state while preserving allocated memory. + /// + /// This is faster than `clear()` when reusing the same circuit multiple times. + fn reset(&mut self) { + self.inner.reset(); + } + + /// Reserve empty ticks in advance. + /// + /// Args: + /// n: The number of empty ticks to reserve. + fn reserve_ticks(&mut self, n: usize) { + self.inner.reserve_ticks(n); + } + + /// Insert an empty tick at a specific position. + /// + /// All ticks at or after `idx` are shifted to the right. + /// Returns a `TickHandle` to the newly inserted tick. + /// + /// Args: + /// idx: The position at which to insert the new tick. + /// + /// Raises: + /// `IndexError`: If `idx > num_ticks()`. + fn insert_tick(slf: Py, py: Python<'_>, idx: usize) -> PyResult { + { + let mut circuit = slf.borrow_mut(py); + let num_ticks = circuit.inner.ticks().len(); + if idx > num_ticks { + return Err(pyo3::exceptions::PyIndexError::new_err(format!( + "insert_tick index {idx} out of bounds for circuit with {num_ticks} ticks" + ))); + } + // Insert the tick + let _ = circuit.inner.insert_tick(idx); + } + // Return a handle to the inserted tick + Ok(PyTickHandle { + circuit: slf, + tick_idx: idx, + last_gate_idx: None, + }) + } + + /// Get a handle to an existing tick for adding more gates. + /// + /// This allows adding gates to a tick that was previously created. + /// + /// Args: + /// idx: The index of the tick to get a handle for. + /// + /// Raises: + /// `IndexError`: If `idx >= num_ticks()`. + fn tick_at(slf: Py, py: Python<'_>, idx: usize) -> PyResult { + { + let circuit = slf.borrow(py); + let num_ticks = circuit.inner.ticks().len(); + if idx >= num_ticks { + return Err(pyo3::exceptions::PyIndexError::new_err(format!( + "tick_at index {idx} out of bounds for circuit with {num_ticks} ticks" + ))); + } + } + Ok(PyTickHandle { + circuit: slf, + tick_idx: idx, + last_gate_idx: None, + }) + } + + // ========================================================================= + // Iteration helpers + // ========================================================================= + + /// Get all qubits used in the circuit. + /// + /// Returns: + /// A list of qubit IDs used in the circuit. + fn all_qubits(&self) -> Vec { + self.inner + .all_qubits() + .into_iter() + .map(usize::from) + .collect() + } + + /// Count gates by type across the entire circuit. + /// + /// Returns: + /// A dictionary mapping gate type names to counts. + fn gate_counts_by_type(&self, py: Python<'_>) -> PyResult> { + let counts = self.inner.gate_counts_by_type(); + let dict = PyDict::new(py); + for (gate_type, count) in counts { + dict.set_item(format!("{gate_type:?}"), count)?; + } + Ok(dict.into()) + } + + /// Get all gates in the circuit as a list. + /// + /// Returns: + /// A list of (`tick_index`, gate) tuples. + fn gates(&self) -> Vec<(usize, PyGate)> { + self.inner + .iter_gates_with_tick() + .map(|(tick_idx, gate)| { + ( + tick_idx, + PyGate { + inner: gate.clone(), + }, + ) + }) + .collect() + } + + /// Remove all gates that use any of the specified qubits from a tick. + /// + /// Args: + /// qubits: List of qubit IDs. Gates using any of these qubits will be removed. + /// `tick_idx`: The index of the tick to modify. + /// + /// Returns: + /// The number of gates removed, or None if the tick index is out of bounds. + /// + /// Example: + /// >>> circuit = `TickCircuit()` + /// >>> circuit.tick().h(0).x(1).cx(2, 3) + /// >>> circuit.discard([0, 2], 0) # Remove H on q0 and CX on q2,q3 + /// 2 + fn discard(&mut self, qubits: Vec, tick_idx: usize) -> Option { + self.inner.discard(&qubits, tick_idx) + } + /// Convert this `TickCircuit` to a `DagCircuit`. /// /// Gates are added in tick order, with qubit wires connecting @@ -1908,31 +2301,57 @@ impl PyTickHandle { Ok(slf) } - /// Apply an S gate (sqrt-Z). + /// Apply an Identity gate. + fn i(slf: Py, py: Python<'_>, q: usize) -> PyResult> { + slf.borrow_mut(py).add_gate_internal(py, Gate::i(&[q]))?; + Ok(slf) + } + + /// Apply an SX gate (sqrt-X). + fn sx(slf: Py, py: Python<'_>, q: usize) -> PyResult> { + slf.borrow_mut(py).add_gate_internal(py, Gate::sx(&[q]))?; + Ok(slf) + } + + /// Apply an SX-dagger gate. + fn sxdg(slf: Py, py: Python<'_>, q: usize) -> PyResult> { + slf.borrow_mut(py).add_gate_internal(py, Gate::sxdg(&[q]))?; + Ok(slf) + } + + /// Apply an SY gate (sqrt-Y). + fn sy(slf: Py, py: Python<'_>, q: usize) -> PyResult> { + slf.borrow_mut(py).add_gate_internal(py, Gate::sy(&[q]))?; + Ok(slf) + } + + /// Apply an SY-dagger gate. + fn sydg(slf: Py, py: Python<'_>, q: usize) -> PyResult> { + slf.borrow_mut(py).add_gate_internal(py, Gate::sydg(&[q]))?; + Ok(slf) + } + + /// Apply an SZ gate (sqrt-Z). fn sz(slf: Py, py: Python<'_>, q: usize) -> PyResult> { - slf.borrow_mut(py) - .add_gate_internal(py, Gate::simple(GateType::SZ, vec![QubitId::from(q)]))?; + slf.borrow_mut(py).add_gate_internal(py, Gate::sz(&[q]))?; Ok(slf) } - /// Apply an S-dagger gate. + /// Apply an SZ-dagger gate. fn szdg(slf: Py, py: Python<'_>, q: usize) -> PyResult> { - slf.borrow_mut(py) - .add_gate_internal(py, Gate::simple(GateType::SZdg, vec![QubitId::from(q)]))?; + slf.borrow_mut(py).add_gate_internal(py, Gate::szdg(&[q]))?; Ok(slf) } /// Apply a T gate. fn t(slf: Py, py: Python<'_>, q: usize) -> PyResult> { - slf.borrow_mut(py) - .add_gate_internal(py, Gate::simple(GateType::T, vec![QubitId::from(q)]))?; + slf.borrow_mut(py).add_gate_internal(py, Gate::t(&[q]))?; Ok(slf) } /// Apply a T-dagger gate. fn tdg(slf: Py, py: Python<'_>, q: usize) -> PyResult> { - slf.borrow_mut(py) - .add_gate_internal(py, Gate::simple(GateType::Tdg, vec![QubitId::from(q)]))?; + slf.borrow_mut(py).add_gate_internal(py, Gate::tdg(&[q]))?; Ok(slf) } @@ -1957,6 +2376,52 @@ impl PyTickHandle { Ok(slf) } + /// Apply an R1XY rotation (single-qubit gate with two angle parameters). + /// + /// Args: + /// theta: First rotation angle in radians. + /// phi: Second rotation angle in radians. + /// q: The qubit to rotate. + fn r1xy(slf: Py, py: Python<'_>, theta: f64, phi: f64, q: usize) -> PyResult> { + slf.borrow_mut(py).add_gate_internal( + py, + Gate::r1xy( + Angle64::from_radians(theta), + Angle64::from_radians(phi), + &[q], + ), + )?; + Ok(slf) + } + + /// Apply a U gate (general single-qubit unitary with three angle parameters). + /// + /// Args: + /// theta: First rotation angle in radians. + /// phi: Second rotation angle in radians. + /// lam: Third rotation angle (lambda) in radians. + /// q: The qubit to rotate. + #[pyo3(name = "u")] + fn u_gate( + slf: Py, + py: Python<'_>, + theta: f64, + phi: f64, + lam: f64, + q: usize, + ) -> PyResult> { + slf.borrow_mut(py).add_gate_internal( + py, + Gate::u( + Angle64::from_radians(theta), + Angle64::from_radians(phi), + Angle64::from_radians(lam), + &[q], + ), + )?; + Ok(slf) + } + // ========================================================================= // Two-qubit gates // ========================================================================= @@ -1968,6 +2433,20 @@ impl PyTickHandle { Ok(slf) } + /// Apply a CY gate. + fn cy(slf: Py, py: Python<'_>, ctrl: usize, tgt: usize) -> PyResult> { + slf.borrow_mut(py) + .add_gate_internal(py, Gate::cy(&[(ctrl, tgt)]))?; + Ok(slf) + } + + /// Apply a CZ gate. + fn cz(slf: Py, py: Python<'_>, ctrl: usize, tgt: usize) -> PyResult> { + slf.borrow_mut(py) + .add_gate_internal(py, Gate::cz(&[(ctrl, tgt)]))?; + Ok(slf) + } + /// Apply an SZZ gate (sqrt-ZZ). fn szz(slf: Py, py: Python<'_>, q1: usize, q2: usize) -> PyResult> { slf.borrow_mut(py) @@ -1982,6 +2461,20 @@ impl PyTickHandle { Ok(slf) } + /// Apply an RXX rotation. + fn rxx(slf: Py, py: Python<'_>, theta: f64, q1: usize, q2: usize) -> PyResult> { + slf.borrow_mut(py) + .add_gate_internal(py, Gate::rxx(Angle64::from_radians(theta), &[(q1, q2)]))?; + Ok(slf) + } + + /// Apply an RYY rotation. + fn ryy(slf: Py, py: Python<'_>, theta: f64, q1: usize, q2: usize) -> PyResult> { + slf.borrow_mut(py) + .add_gate_internal(py, Gate::ryy(Angle64::from_radians(theta), &[(q1, q2)]))?; + Ok(slf) + } + /// Apply an RZZ rotation. fn rzz(slf: Py, py: Python<'_>, theta: f64, q1: usize, q2: usize) -> PyResult> { slf.borrow_mut(py) diff --git a/python/pecos-rslib/src/qulacs_bindings.rs b/python/pecos-rslib/src/qulacs_bindings.rs index 738b95f74..9253d2f37 100644 --- a/python/pecos-rslib/src/qulacs_bindings.rs +++ b/python/pecos-rslib/src/qulacs_bindings.rs @@ -45,22 +45,23 @@ impl PyQulacs { self.inner.g(q1, q2); } "SXX" => { - self.inner.rxx(std::f64::consts::FRAC_PI_2, q1, q2); + // Use trait decomposition for consistency with StateVec + self.inner.sxx(q1, q2); } "SXXdg" => { - self.inner.rxx(-std::f64::consts::FRAC_PI_2, q1, q2); + self.inner.sxxdg(q1, q2); } "SYY" => { - self.inner.ryy(std::f64::consts::FRAC_PI_2, q1, q2); + self.inner.syy(q1, q2); } "SYYdg" => { - self.inner.ryy(-std::f64::consts::FRAC_PI_2, q1, q2); + self.inner.syydg(q1, q2); } "SZZ" | "SqrtZZ" => { - self.inner.rzz(std::f64::consts::FRAC_PI_2, q1, q2); + self.inner.szz(q1, q2); } "SZZdg" => { - self.inner.rzz(-std::f64::consts::FRAC_PI_2, q1, q2); + self.inner.szzdg(q1, q2); } _ => { return Err(PyErr::new::( diff --git a/python/quantum-pecos/src/pecos/circuits/logical_circuit.py b/python/quantum-pecos/src/pecos/circuits/logical_circuit.py index 146be3cf2..91f6314cf 100644 --- a/python/quantum-pecos/src/pecos/circuits/logical_circuit.py +++ b/python/quantum-pecos/src/pecos/circuits/logical_circuit.py @@ -67,6 +67,11 @@ def __init__( self.suppress_warning = suppress_warning + # Store logical gates separately (they have .circuits attribute that iter_ticks needs) + self._logical_gates: list[ + list[tuple[LogicalGateProtocol, LocationSet, dict[str, Any]]] + ] = [] + super().__init__(**params) def append( @@ -83,10 +88,18 @@ def append( gate_locations: Set of locations where the gate should be applied. **params: Additional parameters for the gate. """ + # Store the logical gate in our separate storage (for iter_ticks) if gate_locations is None and not isinstance(logical_gate, dict): - super().append(logical_gate, frozenset([None]), **params) + locations: LocationSet = frozenset([None]) else: - super().append(logical_gate, gate_locations, **params) + locations = gate_locations or set() + + # Add a new tick to logical gates storage + self._logical_gates.append([(logical_gate, locations, params)]) # type: ignore[arg-type] + + # Also call parent's append to maintain tick count + # Use a dummy symbol since logical gates aren't actual physical gates + self.add_ticks(1) if self.layout is None: qecc = logical_gate.qecc @@ -162,6 +175,28 @@ def discard(self, _locations: LocationSet, _tick: int = -1) -> NoReturn: msg = "!!!" raise NotImplementedError(msg) + def items( + self, + tick: int | None = None, + ) -> Iterator[tuple[Any, LocationSet, dict[str, Any]]]: + """Return an iterator over logical gates. + + Args: + tick: If specified, return only gates from that tick. + If None, return gates from all ticks. + + Yields: + Tuples of (logical_gate, locations, params). + """ + if tick is not None: + # Return gates from specific tick + if 0 <= tick < len(self._logical_gates): + yield from self._logical_gates[tick] + else: + # Return gates from all ticks + for tick_gates in self._logical_gates: + yield from tick_gates + def iter_ticks(self) -> Iterator[tuple[Any, tuple[int, int, int], dict[str, Any]]]: """An iterator for looping over the various quantum circuits comprising this data structure.""" for logical_tick in range(len(self)): @@ -180,26 +215,36 @@ def iter_ticks(self) -> Iterator[tuple[Any, tuple[int, int, int], dict[str, Any] def __iter__(self) -> Iterator[Any]: """Iterate over all logical gates in the circuit.""" - for element in self._ticks: - for gate, _, _ in element.items(): - yield gate + for gate, _, _ in self.items(): + yield gate def __str__(self) -> str: """Return string representation of the logical circuit.""" - return f"LogicalCircuit({self._ticks})" + ticks_repr = [] + for _tick_idx, tick_gates in enumerate(self._logical_gates): + gates_str = ", ".join( + f"{getattr(gate, 'symbol', str(gate))}: {locs}" + for gate, locs, _ in tick_gates + ) + ticks_repr.append(f"Tick({{{gates_str}}})") + return f"LogicalCircuit([{', '.join(ticks_repr)}])" def __repr__(self) -> str: """Return detailed string representation of the logical circuit.""" return self.__str__() + def __len__(self) -> int: + """Return the number of logical ticks in the circuit.""" + return len(self._logical_gates) + def __getitem__(self, tick: int | tuple[int, int, int]) -> ParamGateCollection: """Returns tick when instance[index] is used. Args: ---- - tick(int): Tick index of ``self._ticks``. + tick(int): Tick index of the circuit. """ if isinstance(tick, int): - return self._ticks[tick] + return super().__getitem__(tick) logical_tick, _, _ = tick return self[logical_tick] diff --git a/python/quantum-pecos/src/pecos/circuits/quantum_circuit.py b/python/quantum-pecos/src/pecos/circuits/quantum_circuit.py index dceaf0651..b7817bf49 100644 --- a/python/quantum-pecos/src/pecos/circuits/quantum_circuit.py +++ b/python/quantum-pecos/src/pecos/circuits/quantum_circuit.py @@ -11,21 +11,29 @@ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the # specific language governing permissions and limitations under the License. -"""Contains the class ``QuantumCircuit``, which is used to represent quantum circuits.""" +"""Contains the class ``QuantumCircuit``, which is used to represent quantum circuits. + +This implementation uses TickCircuit from the Rust backend as internal storage. +""" from __future__ import annotations import copy import json -from collections import defaultdict from collections.abc import MutableSequence -from typing import TYPE_CHECKING, NamedTuple +from typing import TYPE_CHECKING import pecos as pc from pecos.circuits import qc2phir +try: + from pecos_rslib import QubitConflictError, TickCircuit +except ImportError: + TickCircuit = None # type: ignore[misc, assignment] + QubitConflictError = None # type: ignore[misc, assignment] + if TYPE_CHECKING: - from collections.abc import Iterator + from collections.abc import Callable, Iterator from pecos.typing import JSONDict, JSONValue @@ -35,6 +43,139 @@ GateDict = dict[str, LocationSet] CircuitSetup = int | list[GateDict] | None +# Symbol to TickHandle method mapping for single-qubit gates +_SINGLE_QUBIT_GATES = { + "I": "i", + "H": "h", + "F": "f", + "FDG": "fdg", + "X": "x", + "Y": "y", + "Z": "z", + # sqrt gates + "SX": "sx", + "SXDG": "sxdg", + "SY": "sy", + "SYDG": "sydg", + "SZ": "sz", + "SZDG": "szdg", + # Aliases + "Q": "sx", + "QD": "sxdg", + "R": "sy", + "RD": "sydg", + "S": "sz", + "SD": "szdg", + "SDG": "szdg", # Also accept SDG as alias + "T": "t", + "TDG": "tdg", +} + +# Symbol to TickHandle method mapping for rotation gates (take angle parameter) +_ROTATION_GATES = { + "RX": "rx", + "RY": "ry", + "RZ": "rz", +} + +# Symbol to TickHandle method mapping for two-qubit gates +_TWO_QUBIT_GATES = { + "CX": "cx", + "CNOT": "cx", + "CY": "cy", + "CZ": "cz", + "SXX": "sxx", + "SXXDG": "sxxdg", + "SYY": "syy", + "SYYDG": "syydg", + "SZZ": "szz", + "SZZDG": "szzdg", +} + +# Symbol to TickHandle method mapping for two-qubit rotation gates +_TWO_QUBIT_ROTATION_GATES = { + "RXX": "rxx", + "RYY": "ryy", + "RZZ": "rzz", +} + +# Symbol to TickHandle method mapping for R1XY gate (takes theta, phi angles) +_R1XY_GATES = { + "R1XY": "r1xy", +} + +# Symbol to TickHandle method mapping for U gate (takes theta, phi, lambda angles) +_U_GATES = { + "U": "u", +} + +# Symbol to TickHandle method mapping for R2XXYYZZ gate (takes 3 angles: zz, yy, xx) +# This gate is decomposed into RZZ + RYY + RXX +_R2XXYYZZ_GATES = { + "R2XXYYZZ": "r2xxyyzz", + "RZZRYYRXX": "r2xxyyzz", # Alternative name + "RXXYYZZ": "r2xxyyzz", # Alternative name +} + +# SWAP gate - decomposed into CX gates: SWAP(a,b) = CX(a,b) CX(b,a) CX(a,b) +_SWAP_GATES = {"SWAP"} + +# Prep/measure gates +_PREP_GATES = { + "PREP", + "init", + "Init", + "init |0>", + "Init |0>", + "RESET", + "Reset", + "reset", +} +_MEASURE_GATES = {"MEASURE", "MZ", "measure", "Measure", "measure Z"} + +# GateType string to symbol mapping (for iteration) +_GATETYPE_TO_SYMBOL = { + "I": "I", + "H": "H", + "F": "F", + "Fdg": "FDG", + "X": "X", + "Y": "Y", + "Z": "Z", + "SX": "SX", + "SXdg": "SXDG", + "SY": "SY", + "SYdg": "SYDG", + "SZ": "SZ", + "SZdg": "SZDG", + "T": "T", + "Tdg": "TDG", + "RX": "RX", + "RY": "RY", + "RZ": "RZ", + "R1XY": "R1XY", + "U": "U", + "CX": "CX", + "CY": "CY", + "CZ": "CZ", + "SXX": "SXX", + "SXXdg": "SXXDG", + "SYY": "SYY", + "SYYdg": "SYYDG", + "SZZ": "SZZ", + "SZZdg": "SZZDG", + "RXX": "RXX", + "RYY": "RYY", + "RZZ": "RZZ", + "R2XXYYZZ": "R2XXYYZZ", + "Prep": "init |0>", + "Measure": "measure", + "MeasureFree": "measure", + "QAlloc": "QAlloc", + "QFree": "QFree", + "Idle": "Idle", +} + class QuantumCircuit(MutableSequence): """A representation of a quantum circuit. @@ -42,6 +183,7 @@ class QuantumCircuit(MutableSequence): Similar to [{gate_symbol: set of qudits, ...}, ...] where each element is a time step in which gates act in parallel. + This implementation uses TickCircuit from the Rust backend as internal storage. """ def __init__( @@ -55,41 +197,334 @@ def __init__( circuit_setup (None, int, list of dict): Initial circuit configuration. Can be None (empty circuit), int (number of initial ticks), or list of dicts (pre-configured ticks). **metadata: Additional metadata to associate with the circuit as keyword arguments. - - Attributes: - self._ticks(list of dict): A list of parallel gates. Each element is a dictionary of gate symbol => gate - set. - This gate dictionary is assumed to be a collection of gates that act in parallel on the qudits. - self.active_qudits(list): If `check_overlap` == True then ``active_qudits`` will be tracked; otherwise, - ``active_qudits`` will not be tracked. """ - self._gates_class = ParamGateCollection - self._ticks_class = list - self._ticks = self._ticks_class() + if TickCircuit is None: + msg = "TickCircuit not available. Please install pecos_rslib." + raise ImportError(msg) + + self._inner = TickCircuit() self.metadata = metadata - self.qudits = set() - # TODO: If all the gates on a qudit are discarded... then the qudit will not be removed from this set... fix + self._qudits: set[int] = set() + # Track logically reserved ticks (for backwards compatibility with empty tick creation) + self._reserved_ticks = 0 if "tracked_qudits" in metadata: - msg = "error" - raise Exception(msg) + msg = "tracked_qudits is not a valid metadata key" + raise ValueError(msg) if circuit_setup is not None: self._circuit_setup(circuit_setup) @property - def active_qudits(self) -> list[set[Location]]: + def qudits(self) -> set[int]: + """Returns all qudits used in the circuit.""" + return set(self._inner.all_qubits()) + + @qudits.setter + def qudits(self, value: set[int]) -> None: + """Setter for backwards compatibility.""" + self._qudits = value + + @property + def active_qudits(self) -> list[set[int]]: """Returns the active_qudits of all the ticks.""" - """ - if flat: - active = {} - for gates in self._ticks: - active.update(gates.active_qudits) + result = [] + for tick_idx in range(len(self)): + tick = self._inner.get_tick(tick_idx) + if tick is not None: + # Get individual qubits from all gates in the tick + active: set[int] = set() + for gate in tick.gates(): + for q in gate.qubits: + active.add(q) + result.append(active) + else: + result.append(set()) + return result + def _add_gate_to_tick( + self, + tick_handle: object, + symbol: str | object, + locations: LocationSet, + **params: JSONValue, + ) -> None: + """Add a gate to a tick handle based on symbol.""" + # Handle logical gate objects that have a .symbol attribute + if not isinstance(symbol, str): + symbol = symbol.symbol if hasattr(symbol, "symbol") else str(symbol) + symbol_upper = symbol.upper() + + # Convert locations to list, filtering out None values (placeholders for logical gates) + loc_list = [loc for loc in locations if loc is not None] + if not loc_list: + return + + # Serialize params for storage (handle tuples -> lists) + def make_serializable(obj: object) -> object: + if isinstance(obj, tuple): + return list(obj) + if isinstance(obj, frozenset): + return list(obj) + if isinstance(obj, set): + return list(obj) + return obj + + params_json = ( + json.dumps({k: make_serializable(v) for k, v in params.items()}) + if params + else "" + ) + + # Helper to store original symbol and params in metadata (idempotent - skips if qubit already used) + def add_with_symbol( + method: Callable[..., object], + *args: float, + ) -> object | None: + try: + result = method(*args) + except QubitConflictError: + # Qubit already in use in this tick - skip (idempotent behavior) + return None + else: + # Store original symbol and params for round-trip preservation + if hasattr(result, "meta"): + result.meta("_symbol", symbol) + if params_json: + result.meta("_params", params_json) + return result + + # Handle single-qubit gates + if symbol_upper in _SINGLE_QUBIT_GATES: + method_name = _SINGLE_QUBIT_GATES[symbol_upper] + if hasattr(tick_handle, method_name): + method = getattr(tick_handle, method_name) + for loc in loc_list: + if isinstance(loc, tuple): + for q in loc: + add_with_symbol(method, q) + else: + add_with_symbol(method, loc) + return + # Fall through to custom gate handler if method doesn't exist + + # Handle rotation gates + if symbol_upper in _ROTATION_GATES: + method_name = _ROTATION_GATES[symbol_upper] + if hasattr(tick_handle, method_name): + method = getattr(tick_handle, method_name) + angle = params.get("angle", params.get("theta", 0.0)) + for loc in loc_list: + if isinstance(loc, tuple): + for q in loc: + add_with_symbol(method, angle, q) + else: + add_with_symbol(method, angle, loc) + return + # Fall through to custom gate handler if method doesn't exist + + # Handle two-qubit gates + if symbol_upper in _TWO_QUBIT_GATES: + method_name = _TWO_QUBIT_GATES[symbol_upper] + if hasattr(tick_handle, method_name): + method = getattr(tick_handle, method_name) + for loc in loc_list: + if isinstance(loc, tuple) and len(loc) == 2: + add_with_symbol(method, loc[0], loc[1]) + return + # Fall through to custom gate handler if method doesn't exist + + # Handle two-qubit rotation gates + if symbol_upper in _TWO_QUBIT_ROTATION_GATES: + method_name = _TWO_QUBIT_ROTATION_GATES[symbol_upper] + if hasattr(tick_handle, method_name): + method = getattr(tick_handle, method_name) + angle = params.get("angle", params.get("theta", 0.0)) + for loc in loc_list: + if isinstance(loc, tuple) and len(loc) == 2: + add_with_symbol(method, angle, loc[0], loc[1]) + return + # Fall through to custom gate handler if method doesn't exist + + # Handle R1XY gate (takes theta, phi angles) + if symbol_upper in _R1XY_GATES: + method_name = _R1XY_GATES[symbol_upper] + if hasattr(tick_handle, method_name): + method = getattr(tick_handle, method_name) + # Handle angles tuple or individual theta/phi params + angles = params.get("angles") + if angles is not None and len(angles) >= 2: + theta = angles[0] + phi = angles[1] + else: + theta = params.get("theta", params.get("angle", 0.0)) + phi = params.get("phi", 0.0) + for loc in loc_list: + if isinstance(loc, tuple): + for q in loc: + add_with_symbol(method, theta, phi, q) + else: + add_with_symbol(method, theta, phi, loc) + return + # Fall through to custom gate handler if method doesn't exist + + # Handle U gate (takes theta, phi, lambda angles) + if symbol_upper in _U_GATES: + method_name = _U_GATES[symbol_upper] + if hasattr(tick_handle, method_name): + method = getattr(tick_handle, method_name) + # Handle angles tuple or individual theta/phi/lambda params + angles = params.get("angles") + if angles is not None and len(angles) >= 3: + theta = angles[0] + phi = angles[1] + lambda_ = angles[2] + else: + theta = params.get("theta", 0.0) + phi = params.get("phi", 0.0) + lambda_ = params.get("lambda", params.get("lambda_", 0.0)) + for loc in loc_list: + if isinstance(loc, tuple): + for q in loc: + add_with_symbol(method, theta, phi, lambda_, q) + else: + add_with_symbol(method, theta, phi, lambda_, loc) + return + # Fall through to custom gate handler if method doesn't exist + + # Handle R2XXYYZZ gate (takes 3 angles: zz, yy, xx) + # R2XXYYZZ is not a native GateType. We store it as RZZ with metadata + # containing all three angles and the original symbol. When iterating, + # _iter_tick reconstructs the R2XXYYZZ gate from this metadata. + if symbol_upper in _R2XXYYZZ_GATES: + # Handle angles tuple or individual parameters + angles = params.get("angles") + if angles is not None and len(angles) >= 3: + zz_angle = angles[0] + yy_angle = angles[1] + xx_angle = angles[2] + else: + zz_angle = params.get("zz", 0.0) + yy_angle = params.get("yy", 0.0) + xx_angle = params.get("xx", 0.0) + + for loc in loc_list: + if isinstance(loc, tuple) and len(loc) == 2: + # Store as RZZ with R2XXYYZZ metadata + result = tick_handle.rzz(zz_angle, loc[0], loc[1]) + if hasattr(result, "meta"): + result.meta("_symbol", symbol) + # Store all three angles as comma-separated string + result.meta( + "_r2xxyyzz_angles", + f"{zz_angle},{yy_angle},{xx_angle}", + ) + if params_json: + result.meta("_params", params_json) + return + + # Handle SWAP gate - stored as CX with metadata + # SWAP is not a native GateType. We store it as CX with metadata + # indicating it's a SWAP. The simulator bindings handle SWAP directly. + if symbol_upper in _SWAP_GATES: + for loc in loc_list: + if isinstance(loc, tuple) and len(loc) == 2: + # Store as CX with SWAP metadata + result = tick_handle.cx(loc[0], loc[1]) + if hasattr(result, "meta"): + result.meta("_symbol", symbol) + if params_json: + result.meta("_params", params_json) + return + + # Handle prep gates - idempotent (skip if qubit already used in tick) + if symbol in _PREP_GATES or symbol_upper == "PREP": + for loc in loc_list: + if isinstance(loc, tuple): + for q in loc: + try: + result = tick_handle.pz(q) + result.meta("_symbol", symbol) + if params_json: + result.meta("_params", params_json) + except QubitConflictError: # noqa: PERF203 + pass # Qubit already initialized in this tick + else: + try: + result = tick_handle.pz(loc) + result.meta("_symbol", symbol) + if params_json: + result.meta("_params", params_json) + except QubitConflictError: + pass # Qubit already initialized in this tick + return + + # Handle measure gates - idempotent (skip if qubit already used in tick) + if symbol in _MEASURE_GATES or symbol_upper == "MEASURE": + for loc in loc_list: + if isinstance(loc, tuple): + for q in loc: + try: + result = tick_handle.mz(q) + result.meta("_symbol", symbol) + if params_json: + result.meta("_params", params_json) + except QubitConflictError: # noqa: PERF203 + pass # Qubit already measured in this tick + else: + try: + result = tick_handle.mz(loc) + result.meta("_symbol", symbol) + if params_json: + result.meta("_params", params_json) + except QubitConflictError: + pass # Qubit already measured in this tick + return + + # Fallback: try to use the symbol directly as a method name + method_name = symbol.lower() + if hasattr(tick_handle, method_name): + method = getattr(tick_handle, method_name) + for loc in loc_list: + if isinstance(loc, tuple): + if len(loc) == 2: + add_with_symbol(method, loc[0], loc[1]) + else: + for q in loc: + add_with_symbol(method, q) + else: + add_with_symbol(method, loc) else: - """ - - return [gates.active_qudits for gates in self._ticks] + # Store unrecognized gates using a no-op gate with metadata + # This allows round-trip preservation for simulator-specific gates + # Use I gate (identity) as carrier for unknown single-qubit gates + for loc in loc_list: + if isinstance(loc, tuple): + if len(loc) == 2: + # Two-qubit gate - use CX as carrier + result = tick_handle.cx(loc[0], loc[1]) + if hasattr(result, "meta"): + result.meta("_symbol", symbol) + result.meta("_custom_gate", "true") + if params_json: + result.meta("_params", params_json) + else: + # Multi-qubit locations as individual qubits + for q in loc: + result = tick_handle.i(q) + if hasattr(result, "meta"): + result.meta("_symbol", symbol) + result.meta("_custom_gate", "true") + if params_json: + result.meta("_params", params_json) + else: + # Single-qubit gate - use I (identity) as carrier + result = tick_handle.i(loc) + if hasattr(result, "meta"): + result.meta("_symbol", symbol) + result.meta("_custom_gate", "true") + if params_json: + result.meta("_params", params_json) def append( self, @@ -97,25 +532,27 @@ def append( locations: LocationSet | None = None, **params: JSONValue, ) -> None: - """Adds a new gate=>gate_locations (set) pair to the end of ``self.gates``. + """Adds a new gate=>gate_locations (set) pair to the end of the circuit. Args: symbol(str or dict): A gate dictionary of gate symbol => set of qudit ids or tuples of qudit ids locations: Set of qudit ids or tuples of qudit ids where the gate is applied. If None, symbol must be a gate dict. **params: Additional parameters for the gate (e.g., angle values for rotation gates) - - Example: - >>> quantum_circuit = QuantumCircuit() - >>> quantum_circuit.append({"X": {0, 1, 5, 6}, "Z": {7, 8, 9}}) - >>> quantum_circuit.append("X", {0, 1, 3, 7}) - - This then creates a new time step at the end of ``self._ticks`` and adds the gate to it. - """ - gates = self._gates_class(self, symbol, locations, **params) + # If locations is None then assume symbol is a gate_dict + gate_dict: GateDict = symbol if locations is None else {symbol: locations} # type: ignore[assignment] - self._ticks.append(gates) + tick_handle = self._inner.tick() + + for gate_symbol, gate_locations in gate_dict.items(): + if gate_locations: + self._add_gate_to_tick( + tick_handle, + gate_symbol, + gate_locations, + **params, + ) def update( self, @@ -126,7 +563,7 @@ def update( emptyappend: bool = False, **params: JSONValue, ) -> None: - """Updates that last group of parallel gates to include the gate acting on the set of qudits. + """Updates a group of parallel gates to include the gate acting on the set of qudits. Args: symbol(str or dict): A gate dictionary of gate symbol => set of qudit ids or tuples of qudit ids @@ -135,12 +572,43 @@ def update( tick(int): The time (tick) when the update should occur. emptyappend(bool): Whether it is allowed to add an empty tick if the QuantumCircuit is empty. **params: Additional parameters for the gate (e.g., angle values for rotation gates) - """ - if emptyappend and len(self) == 0: - self.add_ticks(1) + # If locations is None then assume symbol is a gate_dict + gate_dict: GateDict = symbol if locations is None else {symbol: locations} # type: ignore[assignment] + + # Get logical and physical tick counts + logical_ticks = len(self) # includes reserved ticks + # Use next_tick_index() to get actual tick count including empty ticks + # (num_ticks() excludes trailing empty ticks which breaks reserved ticks) + physical_ticks = self._inner.next_tick_index() + + # Handle empty circuit case with negative tick index + if logical_ticks == 0 and tick < 0: + if emptyappend: + # Create a new tick + tick_handle = self._inner.tick() + else: + # Cannot update empty circuit with negative tick without emptyappend + return + else: + # Handle negative indices (use logical_ticks for the calculation) + actual_tick = tick if tick >= 0 else logical_ticks + tick + + # If we're trying to access a tick that doesn't exist physically yet, create it + tick_handle = ( + self._inner.tick() + if actual_tick >= physical_ticks + else self._inner.tick_at(actual_tick) + ) - self._ticks[tick].add(symbol, locations, **params) + for gate_symbol, gate_locations in gate_dict.items(): + if gate_locations: + self._add_gate_to_tick( + tick_handle, + gate_symbol, + gate_locations, + **params, + ) def discard(self, locations: LocationSet, tick: int = -1) -> None: """Discards ``locations`` for tick ``tick``. @@ -149,26 +617,32 @@ def discard(self, locations: LocationSet, tick: int = -1) -> None: locations: Set of qudit ids or tuples of qudit ids to discard from the tick tick: The time (tick) index from which to discard the locations. Defaults to -1 (last tick). """ - self._ticks[tick].discard(locations) + # Handle negative indices + actual_tick = tick if tick >= 0 else len(self) + tick + + # Convert locations to list of qubits + qubits = [] + for loc in locations: + if isinstance(loc, tuple): + qubits.extend(loc) + else: + qubits.append(loc) - def add_ticks(self, num_ticks: int) -> None: - """Makes sure that QuantumCircuit has at least `num_tick` number of ticks. + self._inner.discard(qubits, actual_tick) - If the number of ticks in the data structure is less than `num_tick` then empty ticks are appended - until the total number of ticks == `num_ticks`. + def add_ticks(self, num_ticks: int) -> None: + """Appends empty ticks to the circuit. Args: num_ticks: The number of empty ticks to append to the circuit - - Returns: Nothing """ - for _ in range(num_ticks): - self.append({}) + self._reserved_ticks += num_ticks + self._inner.reserve_ticks(num_ticks) def items( self, tick: int | None = None, - ) -> Iterator[tuple[str, LocationSet, JSONDict]]: + ) -> Iterator[tuple[str, set[Location], JSONDict]]: """An iterator through all gates/qudits in the quantum circuit. If ``tick`` is not None then it will iterate over only the qudits/qudits in the corresponding tick. @@ -177,25 +651,133 @@ def items( tick: The time (tick) index to iterate over. If None, iterates over all ticks. """ if tick is None: - for gates in self._ticks: - for symbol, locations, params in gates.items(): - yield symbol, locations, params - + for tick_idx in range(len(self)): + yield from self._iter_tick(tick_idx) else: - for symbol, locations, params in self._ticks[tick].items(): - yield symbol, locations, params + actual_tick = tick if tick >= 0 else len(self) + tick + yield from self._iter_tick(actual_tick) + + def _iter_tick( + self, + tick_idx: int, + ) -> Iterator[tuple[str, set[Location], JSONDict]]: + """Iterate over gates in a specific tick. + + Gates with the same symbol and params are grouped together with their + locations merged into a single set, matching the original input format. + """ + tick_obj = self._inner.get_tick(tick_idx) + if tick_obj is None: + return + + # Collect gates and group by (symbol, params_json) to merge locations + # Use a dict to preserve insertion order and group gates + grouped: dict[tuple[str, str], tuple[set[Location], JSONDict]] = {} - def iter_ticks(self) -> Iterator[tuple[int, ParamGateCollection, JSONDict]]: + for gate_idx, gate in enumerate(tick_obj.gates()): + # Check for stored original symbol in metadata + stored_symbol = tick_obj.get_gate_attr(gate_idx, "_symbol") + + if stored_symbol is not None: + symbol = stored_symbol + else: + gate_type_str = str(gate.gate_type) + # Extract gate type name from "GateType.H" format + if "." in gate_type_str: + gate_type_str = gate_type_str.split(".")[-1] + symbol = _GATETYPE_TO_SYMBOL.get(gate_type_str, gate_type_str) + + qubits = list(gate.qubits) + if len(qubits) == 1: + location: Location = qubits[0] + else: + location = tuple(qubits) + + # Extract params from gate (angles, etc.) + params: JSONDict = {} + + # Check for stored params (general case) + stored_params_json = tick_obj.get_gate_attr(gate_idx, "_params") + if stored_params_json is not None: + try: + stored_params = json.loads(stored_params_json) + # Convert lists back to tuples for "angles" + if "angles" in stored_params and isinstance( + stored_params["angles"], + list, + ): + stored_params["angles"] = tuple(stored_params["angles"]) + # Fix JSON type issues (e.g., var_output keys become strings) + stored_params = self._fix_json_meta(stored_params) + params.update(stored_params) + except json.JSONDecodeError: + pass + + # Check for custom gate params (stored as JSON in metadata) - legacy + custom_params_json = tick_obj.get_gate_attr(gate_idx, "_custom_params") + if custom_params_json is not None: + try: + custom_params = json.loads(custom_params_json) + # Convert lists back to tuples for "angles" + if "angles" in custom_params and isinstance( + custom_params["angles"], + list, + ): + custom_params["angles"] = tuple(custom_params["angles"]) + # Fix JSON type issues (e.g., var_output keys become strings) + custom_params = self._fix_json_meta(custom_params) + params.update(custom_params) + except json.JSONDecodeError: + pass + + # Check for R2XXYYZZ special case (stored as RZZ with metadata) + if ( + r2xxyyzz_angles := tick_obj.get_gate_attr(gate_idx, "_r2xxyyzz_angles") + ) is not None and stored_symbol in _R2XXYYZZ_GATES: + # Reconstruct R2XXYYZZ angles from metadata + angle_parts = r2xxyyzz_angles.split(",") + if len(angle_parts) >= 3: + params["angles"] = [float(a) for a in angle_parts[:3]] + elif hasattr(gate, "angles"): + angles = gate.angles + if angles: + if len(angles) == 1: + # Single angle gates (RX, RY, RZ, RXX, RYY, RZZ) + params["angle"] = angles[0] + elif len(angles) == 2: + # Two angle gates (R1XY) + params["theta"] = angles[0] + params["phi"] = angles[1] + elif len(angles) == 3: + # Three angle gates (U) + params["theta"] = angles[0] + params["phi"] = angles[1] + params["lambda"] = angles[2] + + # Create a hashable key from symbol and params + # Sort params keys for consistent hashing + params_key = json.dumps(params, sort_keys=True) if params else "" + key = (symbol, params_key) + + if key in grouped: + # Add location to existing group + grouped[key][0].add(location) + else: + # Create new group + grouped[key] = ({location}, params) + + # Yield grouped results + for (symbol, _), (locations, params) in grouped.items(): + yield symbol, locations, params + + def iter_ticks(self) -> Iterator[tuple[TickView, int, JSONDict]]: """Iterate over circuit time ticks. Yields: - Tuples containing gate collection, tick number, and metadata. + Tuples containing gate collection view, tick number, and metadata. """ - for tick in range(len(self)): - gates = self[tick] - yield gates, tick, self.metadata - # TODO: note this is the circuit params - # TODO: need something like: params {logical_circuit: ..., gate: ..., qecc: ...} + for tick_idx in range(len(self)): + yield TickView(self, tick_idx), tick_idx, self.metadata def insert( self, @@ -208,15 +790,27 @@ def insert( tick: The time (tick) index where the item should be inserted item: Either a gate dictionary or a tuple of (gate_dict, params) to insert at the specified tick """ - gate_dict, params = item - gates = self._gates_class(self, gate_dict, **params) - self._ticks.insert(tick, gates) + if isinstance(item, tuple): + gate_dict, params = item + else: + gate_dict, params = item, {} + + tick_handle = self._inner.insert_tick(tick) + + for gate_symbol, gate_locations in gate_dict.items(): + if gate_locations: + self._add_gate_to_tick( + tick_handle, + gate_symbol, + gate_locations, + **params, + ) def _circuit_setup(self, circuit_setup: CircuitSetup) -> None: if isinstance(circuit_setup, int): - # Reserve ticks - self.add_ticks(circuit_setup) - + # Reserve empty ticks (logically, not physically in the Rust backend) + self._reserved_ticks = circuit_setup + self._inner.reserve_ticks(circuit_setup) else: # Build circuit from other description (a shallow copy). for other_tick in circuit_setup: @@ -289,22 +883,42 @@ def to_phir_json(self) -> str: """Converts this QuantumCircuit into the PHIR/JSON format.""" return qc2phir.to_phir_json(self) - def __getitem__(self, tick: int) -> ParamGateCollection: + def __getitem__(self, tick: int) -> TickView: """Returns tick when instance[index] is used. Args: - tick(int): Tick index of ``self._ticks``. + tick(int): Tick index of the circuit. """ - return self._ticks[tick] + actual_tick = tick if tick >= 0 else len(self) + tick + return TickView(self, actual_tick) def __setitem__(self, tick: int, item: tuple[GateDict, JSONDict]) -> None: """Set gate collection at specified tick.""" + actual_tick = tick if tick >= 0 else len(self) + tick gate_dict, params = item - self._ticks[tick] = self._gates_class(self, gate_dict, **params) + + # Get qubits to discard first + tick_obj = self._inner.get_tick(actual_tick) + if tick_obj is not None: + qubits_to_discard = list(tick_obj.active_qubits()) + if qubits_to_discard: + self._inner.discard(qubits_to_discard, actual_tick) + + # Add new gates + tick_handle = self._inner.tick_at(actual_tick) + for gate_symbol, gate_locations in gate_dict.items(): + if gate_locations: + self._add_gate_to_tick( + tick_handle, + gate_symbol, + gate_locations, + **params, + ) def __len__(self) -> int: """Used to return number of ticks when len() is used on an instance of this class.""" - return len(self._ticks) + # Return max of actual ticks and reserved ticks (for backwards compatibility) + return max(self._inner.num_ticks(), self._reserved_ticks) def __delitem__(self, tick: int) -> None: """Used to delete a tick. For example: del instance[key]. @@ -312,20 +926,25 @@ def __delitem__(self, tick: int) -> None: Args: tick: The time (tick) index to delete (replace with an empty tick) """ - self._ticks[tick] = self._gates_class(self) + actual_tick = tick if tick >= 0 else len(self) + tick + tick_obj = self._inner.get_tick(actual_tick) + if tick_obj is not None: + qubits_to_discard = list(tick_obj.active_qubits()) + if qubits_to_discard: + self._inner.discard(qubits_to_discard, actual_tick) def __str__(self) -> str: """String returned when a string representation is requested. This occurs during printing.""" str_list = [] - for gates in self._ticks: + for tick_idx in range(len(self)): tick_list = [] - for symbol, locations, params in gates.items(): + for symbol, locations, params in self._iter_tick(tick_idx): if len(params) == 0: tick_list.append(f"'{symbol}': {locations}") else: tick_list.append(f"'{symbol}': loc: {locations} - params={params}") - tick_list = ", ".join(tick_list) - str_list.append(f"{{{tick_list}}}") + tick_list_str = ", ".join(tick_list) + str_list.append(f"{{{tick_list_str}}}") if self.metadata: return "QuantumCircuit(params={}, ticks=[{}])".format( @@ -342,13 +961,9 @@ def __copy__(self) -> QuantumCircuit: """Create a shallow copy.""" newone = QuantumCircuit() newone.metadata = dict(self.metadata) - # Use a public property to access the number of ticks - num_ticks = len(self) - # Add empty ticks first - newone.add_ticks(num_ticks) - # Then populate each tick with gates - for i in range(num_ticks): - for symbol, locations, params in self[i].items(): + # Copy gates tick by tick + for i in range(len(self)): + for symbol, locations, params in self._iter_tick(i): newone.update(symbol, locations, tick=i, **params) return newone @@ -361,141 +976,98 @@ def __iter__(self) -> Iterator[tuple[str, LocationSet, JSONDict]]: return self.items() -class ParamGateCollection: - """Data structure for a tick.""" - - class Gate(NamedTuple): - """Gate representation with symbol, parameters, and locations.""" +class TickView: + """A view into a specific tick of the circuit. - symbol: str - params: JSONDict - locations: set[int | tuple[int]] + Provides the same interface as the old ParamGateCollection for backwards compatibility. + """ - def __init__( - self, - circuit: QuantumCircuit, - symbol: str | GateDict | None = None, - locations: LocationSet | None = None, - **params: JSONValue, - ) -> None: - """Initialize a ParamGateCollection. + def __init__(self, circuit: QuantumCircuit, tick_idx: int) -> None: + """Initialize a TickView. Args: - ---- - circuit: The parent QuantumCircuit this collection belongs to. - symbol: Optional gate symbol or gate dictionary to initialize with. - locations: Optional set of qudit locations where the gate is applied. - **params: Additional parameters for the gate. + circuit: The parent QuantumCircuit. + tick_idx: The tick index this view represents. """ - self.circuit = circuit - self.metadata = circuit.metadata - self.active_qudits = set() - self.symbols = defaultdict(list) + self._circuit = circuit + self._tick_idx = tick_idx + + @property + def circuit(self) -> QuantumCircuit: + """Returns the parent circuit (for backwards compatibility).""" + return self._circuit + + @property + def active_qudits(self) -> set[Location]: + """Returns the active qudits for this tick.""" + tick = self._circuit._inner.get_tick(self._tick_idx) # noqa: SLF001 + if tick is None: + return set() - self.add(symbol, locations, **params) + active: set[Location] = set() + for gate in tick.gates(): + qubits = list(gate.qubits) + if len(qubits) == 1: + active.add(qubits[0]) + else: + active.add(tuple(qubits)) + return active + + @property + def metadata(self) -> JSONDict: + """Returns the circuit metadata.""" + return self._circuit.metadata def add( self, symbol: str | GateDict | None, locations: LocationSet | None = None, **params: JSONValue, - ) -> ParamGateCollection: - """Add a gate to the collection. + ) -> TickView: + """Add a gate to this tick. Args: symbol: Gate symbol or gate dictionary. locations: Set of qudit locations where the gate is applied. **params: Additional parameters for the gate. """ - # If locations is None then assume symbol is a gate_dict. - gate_dict = symbol if locations is None else {symbol: locations} - - self._verify_qudits(gate_dict) - - for gate_symbol, gate_locations in gate_dict.items(): - # if gate_locations: #TODO: Why was this here? - - for gate in self.symbols[gate_symbol]: - if params == gate.params: - gate.locations.update(gate_locations) - break - else: - self.symbols[gate_symbol].append( - self.Gate(gate_symbol, params, gate_locations), - ) + gate_dict: GateDict = symbol if locations is None else {symbol: locations} # type: ignore[assignment] + + if gate_dict: + tick_handle = self._circuit._inner.tick_at(self._tick_idx) # noqa: SLF001 + for gate_symbol, gate_locations in gate_dict.items(): + if gate_locations: + self._circuit._add_gate_to_tick( # noqa: SLF001 + tick_handle, + gate_symbol, + gate_locations, + **params, + ) return self - def discard(self, locations: LocationSet) -> ParamGateCollection: - """Remove gate locations. + def discard(self, locations: LocationSet) -> TickView: + """Remove gate locations from this tick. Args: - locations: Set of qudit ids or tuples of qudit ids to remove from the gates in this collection + locations: Set of qudit ids or tuples of qudit ids to remove. """ - for gate_list in self.symbols.values(): - for gate in gate_list: - for location in locations: - if location in gate.locations: - gate.locations.discard(location) - break - - # symbols: dict-> symbol: list[gate] - - # Remove keys with empty locations - # -------------------------------- - # remove symbol from dictionary - for symbol in list(self.symbols.keys()): - for gate in self.symbols[symbol]: - if not gate.locations: - self.symbols[symbol].remove(gate) - - # Update active_qudits - # -------------------- - # Remove locations from active_qudits - for location in locations: - if isinstance(location, tuple): - for loc in location: - self.active_qudits.discard(loc) + qubits = [] + for loc in locations: + if isinstance(loc, tuple): + qubits.extend(loc) else: - self.active_qudits.discard(location) + qubits.append(loc) + self._circuit._inner.discard(qubits, self._tick_idx) # noqa: SLF001 return self - def _verify_qudits(self, gate_dict: GateDict) -> None: - """Verifies that all qudits are being acted on in parallel during a time step (tick). - - The qudit ids are added to ``self.active_qudits``. - - Args: - gate_dict: Dictionary mapping gate symbols to sets of qudit locations to verify - Raises: - Exception: If qudit ids are not int or if non-parallel gates are found (i.e., a qudit ha already been acted - on by a gate. - - """ - for qudit_locations in gate_dict.values(): - for location in qudit_locations: - # Make sure we can iterate over q. - q_iter = (location,) if not isinstance(location, tuple) else location - - for qi in q_iter: - self.circuit.qudits.add(qi) - - if qi in self.active_qudits: - msg = f"Qudit {qi!s} has already been acted on by a gate!" - raise Exception( - msg, - ) - self.active_qudits.add(qi) - def items( self, _tick: None = None, ) -> Iterator[tuple[str, set[Location], JSONDict]]: """Generator to return a dictionary-like iter.""" - for gate_symbol, gate_list in self.symbols.items(): - for gate in gate_list: - yield gate_symbol, gate.locations, gate.params + yield from self._circuit._iter_tick(self._tick_idx) # noqa: SLF001 def __str__(self) -> str: """Return string representation of the tick.""" @@ -505,10 +1077,14 @@ def __str__(self) -> str: tick_list.append(f"'{symbol}': {locations}") else: tick_list.append(f"'{symbol}': loc: {locations} - params={params}") - tick_list = ", ".join(tick_list) + tick_list_str = ", ".join(tick_list) - return f"Tick({{{tick_list}}})" + return f"Tick({{{tick_list_str}}})" def __repr__(self) -> str: """Return detailed string representation of the tick.""" return self.__str__() + + +# Keep ParamGateCollection as an alias for backwards compatibility +ParamGateCollection = TickView