Skip to main content

quantrs2_sim/
specialized_simulator.rs

1//! Optimized state vector simulator using specialized gate implementations
2//!
3//! This simulator automatically detects and uses specialized gate implementations
4//! for improved performance compared to general matrix multiplication.
5
6use scirs2_core::parallel_ops::{
7    IndexedParallelIterator, IntoParallelRefMutIterator, ParallelIterator,
8};
9use scirs2_core::Complex64;
10use std::sync::Arc;
11
12use quantrs2_circuit::builder::{Circuit, Simulator};
13use quantrs2_core::{
14    error::{QuantRS2Error, QuantRS2Result},
15    gate::{multi, single, GateOp},
16    qubit::QubitId,
17    register::Register,
18};
19
20use crate::specialized_gates::{specialize_gate, SpecializedGate};
21use crate::statevector::StateVectorSimulator;
22use crate::utils::flip_bit;
23
24/// Configuration for specialized simulator
25#[derive(Debug, Clone)]
26pub struct SpecializedSimulatorConfig {
27    /// Use parallel execution
28    pub parallel: bool,
29    /// Enable gate fusion optimization
30    pub enable_fusion: bool,
31    /// Enable gate reordering optimization
32    pub enable_reordering: bool,
33    /// Cache specialized gate conversions
34    pub cache_conversions: bool,
35    /// Minimum qubit count for parallel execution
36    pub parallel_threshold: usize,
37}
38
39impl Default for SpecializedSimulatorConfig {
40    fn default() -> Self {
41        Self {
42            parallel: true,
43            enable_fusion: true,
44            enable_reordering: true,
45            cache_conversions: true,
46            parallel_threshold: 10,
47        }
48    }
49}
50
51/// Statistics about specialized gate usage
52#[derive(Debug, Clone, Default)]
53pub struct SpecializationStats {
54    /// Total gates processed
55    pub total_gates: usize,
56    /// Gates using specialized implementation
57    pub specialized_gates: usize,
58    /// Gates using generic implementation
59    pub generic_gates: usize,
60    /// Gates that were fused
61    pub fused_gates: usize,
62    /// Time saved by specialization (estimated ms)
63    pub time_saved_ms: f64,
64}
65
66/// Optimized state vector simulator with specialized gate implementations
67pub struct SpecializedStateVectorSimulator {
68    /// Configuration
69    config: SpecializedSimulatorConfig,
70    /// Base state vector simulator for fallback
71    base_simulator: StateVectorSimulator,
72    /// Statistics tracker
73    stats: SpecializationStats,
74    /// Cache for specialized gate conversions (simplified to avoid Clone issues)
75    conversion_cache: Option<Arc<dashmap::DashMap<String, bool>>>,
76    /// Reusable buffer for parallel gate application (avoids allocation per gate)
77    work_buffer: Vec<Complex64>,
78}
79
80impl SpecializedStateVectorSimulator {
81    /// Create a new specialized simulator
82    #[must_use]
83    pub fn new(config: SpecializedSimulatorConfig) -> Self {
84        let base_simulator = if config.parallel {
85            StateVectorSimulator::new()
86        } else {
87            StateVectorSimulator::sequential()
88        };
89
90        let conversion_cache = if config.cache_conversions {
91            Some(Arc::new(dashmap::DashMap::new()))
92        } else {
93            None
94        };
95
96        Self {
97            config,
98            base_simulator,
99            stats: SpecializationStats::default(),
100            conversion_cache,
101            work_buffer: Vec::new(),
102        }
103    }
104
105    /// Get specialization statistics
106    pub const fn get_stats(&self) -> &SpecializationStats {
107        &self.stats
108    }
109
110    /// Reset statistics
111    pub fn reset_stats(&mut self) {
112        self.stats = SpecializationStats::default();
113    }
114
115    /// Run a quantum circuit
116    pub fn run<const N: usize>(&mut self, circuit: &Circuit<N>) -> QuantRS2Result<Vec<Complex64>> {
117        let n_qubits = N;
118        let mut state = self.initialize_state(n_qubits);
119
120        // Process gates with optimization
121        let gates = if self.config.enable_reordering {
122            self.reorder_gates(circuit.gates())?
123        } else {
124            circuit.gates().to_vec()
125        };
126
127        // Apply gates with fusion if enabled
128        if self.config.enable_fusion {
129            self.apply_gates_with_fusion(&mut state, &gates, n_qubits)?;
130        } else {
131            for gate in gates {
132                self.apply_gate(&mut state, &gate, n_qubits)?;
133            }
134        }
135
136        Ok(state)
137    }
138
139    /// Initialize quantum state
140    fn initialize_state(&self, n_qubits: usize) -> Vec<Complex64> {
141        let size = 1 << n_qubits;
142        let mut state = vec![Complex64::new(0.0, 0.0); size];
143        state[0] = Complex64::new(1.0, 0.0);
144        state
145    }
146
147    /// Apply a single gate
148    fn apply_gate(
149        &mut self,
150        state: &mut [Complex64],
151        gate: &Arc<dyn GateOp + Send + Sync>,
152        n_qubits: usize,
153    ) -> QuantRS2Result<()> {
154        self.stats.total_gates += 1;
155
156        // Try to get specialized implementation
157        if let Some(specialized) = self.get_specialized_gate(gate.as_ref()) {
158            self.stats.specialized_gates += 1;
159            self.stats.time_saved_ms += self.estimate_time_saved(gate.as_ref());
160
161            let parallel = self.config.parallel && n_qubits >= self.config.parallel_threshold;
162            specialized.apply_specialized(state, n_qubits, parallel)
163        } else {
164            self.stats.generic_gates += 1;
165
166            // Fall back to generic implementation
167            match gate.num_qubits() {
168                1 => {
169                    let qubits = gate.qubits();
170                    let matrix = gate.matrix()?;
171                    self.apply_single_qubit_generic(state, &matrix, qubits[0], n_qubits)
172                }
173                2 => {
174                    let qubits = gate.qubits();
175                    let matrix = gate.matrix()?;
176                    self.apply_two_qubit_generic(state, &matrix, qubits[0], qubits[1], n_qubits)
177                }
178                _ => {
179                    // For multi-qubit gates, use general matrix application
180                    self.apply_multi_qubit_generic(state, gate.as_ref(), n_qubits)
181                }
182            }
183        }
184    }
185
186    /// Get specialized gate implementation with caching
187    fn get_specialized_gate(&self, gate: &dyn GateOp) -> Option<Box<dyn SpecializedGate>> {
188        // Simplified: always create new specialized gate to avoid Clone constraints
189        specialize_gate(gate)
190    }
191
192    /// Apply gates with fusion optimization
193    fn apply_gates_with_fusion(
194        &mut self,
195        state: &mut [Complex64],
196        gates: &[Arc<dyn GateOp + Send + Sync>],
197        n_qubits: usize,
198    ) -> QuantRS2Result<()> {
199        let mut i = 0;
200
201        while i < gates.len() {
202            // Try to fuse with next gate
203            if i + 1 < gates.len() {
204                if let (Some(gate1), Some(gate2)) = (
205                    self.get_specialized_gate(gates[i].as_ref()),
206                    self.get_specialized_gate(gates[i + 1].as_ref()),
207                ) {
208                    if gate1.can_fuse_with(gate2.as_ref()) {
209                        if let Some(fused) = gate1.fuse_with(gate2.as_ref()) {
210                            self.stats.fused_gates += 2;
211                            self.stats.total_gates += 1;
212
213                            let parallel =
214                                self.config.parallel && n_qubits >= self.config.parallel_threshold;
215                            fused.apply_specialized(state, n_qubits, parallel)?;
216
217                            i += 2;
218                            continue;
219                        }
220                    }
221                }
222            }
223
224            // Apply single gate
225            self.apply_gate(state, &gates[i], n_qubits)?;
226            i += 1;
227        }
228
229        Ok(())
230    }
231
232    /// Reorder gates for better cache locality *without* changing the
233    /// circuit's semantics.
234    ///
235    /// A blind `sort_by_key` on the first qubit id (the previous
236    /// implementation) can silently reorder gates whose relative order
237    /// matters -- e.g. it would happily move `X(0)` before `CNOT(0, 1)` even
238    /// though they act on a shared qubit and do not commute, corrupting the
239    /// computed state. This implementation only ever moves a gate earlier in
240    /// program order past gates it can *provably* commute with:
241    ///
242    /// * two gates acting on completely disjoint qubit sets always commute
243    ///   (they act on independent tensor factors), and
244    /// * two gates that are both diagonal in the computational basis (Z, S,
245    ///   T, RZ, phase, controlled-diagonal, global phase, ...) always
246    ///   commute with each other regardless of qubit overlap, since diagonal
247    ///   matrices always commute.
248    ///
249    /// This is a greedy selection sort: for each output position, the
250    /// earliest-by-first-qubit gate that can be proven to commute with every
251    /// gate between its current position and the target position is chosen.
252    /// Any gate that cannot be proven to commute stops the scan, so no
253    /// dependency-violating move is ever made.
254    fn reorder_gates(
255        &self,
256        gates: &[Arc<dyn GateOp + Send + Sync>],
257    ) -> QuantRS2Result<Vec<Arc<dyn GateOp + Send + Sync>>> {
258        let mut reordered: Vec<Arc<dyn GateOp + Send + Sync>> = gates.to_vec();
259        let key =
260            |gate: &Arc<dyn GateOp + Send + Sync>| gate.qubits().first().map_or(0, QubitId::id);
261
262        for i in 0..reordered.len() {
263            let mut best_j = i;
264            let mut best_key = key(&reordered[i]);
265
266            for j in (i + 1)..reordered.len() {
267                // gates[j] can only be considered as a candidate for
268                // position i if it provably commutes with every gate
269                // currently occupying positions i..j (i.e. every gate it
270                // would have to move past).
271                if !Self::commutes_with_all(reordered[j].as_ref(), &reordered[i..j]) {
272                    break;
273                }
274                let candidate_key = key(&reordered[j]);
275                if candidate_key < best_key {
276                    best_key = candidate_key;
277                    best_j = j;
278                }
279            }
280
281            if best_j != i {
282                let gate = reordered.remove(best_j);
283                reordered.insert(i, gate);
284            }
285        }
286
287        Ok(reordered)
288    }
289
290    /// Whether `candidate` provably commutes with every gate in `others`,
291    /// i.e. it is safe to move `candidate` past all of them without
292    /// changing the circuit's semantics.
293    fn commutes_with_all(candidate: &dyn GateOp, others: &[Arc<dyn GateOp + Send + Sync>]) -> bool {
294        others
295            .iter()
296            .all(|other| Self::gates_commute(candidate, other.as_ref()))
297    }
298
299    /// Whether two gates provably commute.
300    ///
301    /// This is intentionally conservative: it only returns `true` when
302    /// commutation is guaranteed by a structural property (disjoint qubits,
303    /// or both gates diagonal in the computational basis), never by
304    /// inspecting the numeric gate matrices. A `false` result may still
305    /// correspond to gates that happen to commute (e.g. two different CNOTs
306    /// sharing a qubit in specific configurations) -- that only forgoes an
307    /// optimization opportunity, it never risks correctness.
308    fn gates_commute(a: &dyn GateOp, b: &dyn GateOp) -> bool {
309        let qubits_a = a.qubits();
310        let qubits_b = b.qubits();
311        let disjoint = qubits_a.iter().all(|q| !qubits_b.contains(q));
312        if disjoint {
313            return true;
314        }
315        Self::is_diagonal_gate(a) && Self::is_diagonal_gate(b)
316    }
317
318    /// Whether a gate's matrix is diagonal in the computational basis.
319    ///
320    /// Any two diagonal matrices commute regardless of which qubits they
321    /// act on, so this is the basis for the only qubit-overlapping
322    /// commutation case `gates_commute` recognizes.
323    fn is_diagonal_gate(gate: &dyn GateOp) -> bool {
324        matches!(
325            gate.name(),
326            "Z" | "S" | "S†" | "T" | "T†" | "RZ" | "P" | "I" | "CZ" | "CRZ" | "CS" | "GlobalPhase"
327        )
328    }
329
330    /// Estimate time saved by using specialized implementation
331    fn estimate_time_saved(&self, gate: &dyn GateOp) -> f64 {
332        // Rough estimates based on gate type
333        match gate.name() {
334            "H" | "X" | "Y" | "Z" => 0.001, // Simple gates save ~1μs
335            "RX" | "RY" | "RZ" => 0.002,    // Rotation gates save ~2μs
336            "CNOT" | "CZ" => 0.005,         // Two-qubit gates save ~5μs
337            "Toffoli" => 0.010,             // Three-qubit gates save ~10μs
338            _ => 0.0,
339        }
340    }
341
342    /// Apply single-qubit gate (generic fallback) - optimized with reusable buffer
343    fn apply_single_qubit_generic(
344        &mut self,
345        state: &mut [Complex64],
346        matrix: &[Complex64],
347        target: QubitId,
348        n_qubits: usize,
349    ) -> QuantRS2Result<()> {
350        let target_idx = target.id() as usize;
351
352        if self.config.parallel && n_qubits >= self.config.parallel_threshold {
353            // Reuse work_buffer to avoid allocation per gate
354            if self.work_buffer.len() < state.len() {
355                self.work_buffer
356                    .resize(state.len(), Complex64::new(0.0, 0.0));
357            }
358            self.work_buffer[..state.len()].copy_from_slice(state);
359            let state_copy = &self.work_buffer[..state.len()];
360
361            state.par_iter_mut().enumerate().for_each(|(idx, amp)| {
362                let bit_val = (idx >> target_idx) & 1;
363                let paired_idx = idx ^ (1 << target_idx);
364
365                let idx0 = if bit_val == 0 { idx } else { paired_idx };
366                let idx1 = if bit_val == 0 { paired_idx } else { idx };
367
368                *amp = matrix[2 * bit_val] * state_copy[idx0]
369                    + matrix[2 * bit_val + 1] * state_copy[idx1];
370            });
371        } else {
372            // Sequential in-place update (already optimal - no allocation)
373            for i in 0..(1 << n_qubits) {
374                if (i >> target_idx) & 1 == 0 {
375                    let j = i | (1 << target_idx);
376                    let temp0 = state[i];
377                    let temp1 = state[j];
378                    state[i] = matrix[0] * temp0 + matrix[1] * temp1;
379                    state[j] = matrix[2] * temp0 + matrix[3] * temp1;
380                }
381            }
382        }
383
384        Ok(())
385    }
386
387    /// Apply two-qubit gate (generic fallback) - optimized with reusable buffer
388    fn apply_two_qubit_generic(
389        &mut self,
390        state: &mut [Complex64],
391        matrix: &[Complex64],
392        control: QubitId,
393        target: QubitId,
394        n_qubits: usize,
395    ) -> QuantRS2Result<()> {
396        let control_idx = control.id() as usize;
397        let target_idx = target.id() as usize;
398
399        if control_idx == target_idx {
400            return Err(QuantRS2Error::CircuitValidationFailed(
401                "Control and target must be different".into(),
402            ));
403        }
404
405        // Ensure work_buffer is large enough (reused across calls)
406        if self.work_buffer.len() < state.len() {
407            self.work_buffer
408                .resize(state.len(), Complex64::new(0.0, 0.0));
409        }
410
411        if self.config.parallel && n_qubits >= self.config.parallel_threshold {
412            // Copy state to work buffer for reading
413            self.work_buffer[..state.len()].copy_from_slice(state);
414            let state_copy = &self.work_buffer[..state.len()];
415
416            state.par_iter_mut().enumerate().for_each(|(idx, amp)| {
417                let ctrl_bit = (idx >> control_idx) & 1;
418                let tgt_bit = (idx >> target_idx) & 1;
419                let basis_idx = (ctrl_bit << 1) | tgt_bit;
420
421                let idx00 = idx & !(1 << control_idx) & !(1 << target_idx);
422                let idx01 = idx00 | (1 << target_idx);
423                let idx10 = idx00 | (1 << control_idx);
424                let idx11 = idx00 | (1 << control_idx) | (1 << target_idx);
425
426                *amp = matrix[4 * basis_idx] * state_copy[idx00]
427                    + matrix[4 * basis_idx + 1] * state_copy[idx01]
428                    + matrix[4 * basis_idx + 2] * state_copy[idx10]
429                    + matrix[4 * basis_idx + 3] * state_copy[idx11];
430            });
431        } else {
432            // Use work_buffer as temporary storage to avoid separate allocation
433            for i in 0..state.len() {
434                let ctrl_bit = (i >> control_idx) & 1;
435                let tgt_bit = (i >> target_idx) & 1;
436                let basis_idx = (ctrl_bit << 1) | tgt_bit;
437
438                let i00 = i & !(1 << control_idx) & !(1 << target_idx);
439                let i01 = i00 | (1 << target_idx);
440                let i10 = i00 | (1 << control_idx);
441                let i11 = i10 | (1 << target_idx);
442
443                self.work_buffer[i] = matrix[4 * basis_idx] * state[i00]
444                    + matrix[4 * basis_idx + 1] * state[i01]
445                    + matrix[4 * basis_idx + 2] * state[i10]
446                    + matrix[4 * basis_idx + 3] * state[i11];
447            }
448
449            state.copy_from_slice(&self.work_buffer[..state.len()]);
450        }
451
452        Ok(())
453    }
454
455    /// Apply multi-qubit gate (generic fallback) - optimized with reusable buffer
456    fn apply_multi_qubit_generic(
457        &mut self,
458        state: &mut [Complex64],
459        gate: &dyn GateOp,
460        _n_qubits: usize,
461    ) -> QuantRS2Result<()> {
462        // For now, convert to matrix and apply
463        // This is a placeholder for more sophisticated multi-qubit handling
464        let matrix = gate.matrix()?;
465        let qubits = gate.qubits();
466        let gate_qubits = qubits.len();
467        let gate_dim = 1 << gate_qubits;
468
469        if matrix.len() != gate_dim * gate_dim {
470            return Err(QuantRS2Error::InvalidInput(format!(
471                "Invalid matrix size for {gate_qubits}-qubit gate"
472            )));
473        }
474
475        // Ensure work_buffer is large enough (reused across calls)
476        if self.work_buffer.len() < state.len() {
477            self.work_buffer
478                .resize(state.len(), Complex64::new(0.0, 0.0));
479        }
480
481        // Apply gate by iterating over all basis states
482        for idx in 0..state.len() {
483            let mut basis_idx = 0;
484            for (i, &qubit) in qubits.iter().enumerate() {
485                if (idx >> qubit.id()) & 1 == 1 {
486                    basis_idx |= 1 << i;
487                }
488            }
489
490            let mut new_amp = Complex64::new(0.0, 0.0);
491            for j in 0..gate_dim {
492                let mut target_idx = idx;
493                for (i, &qubit) in qubits.iter().enumerate() {
494                    if (j >> i) & 1 != (idx >> qubit.id()) & 1 {
495                        target_idx ^= 1 << qubit.id();
496                    }
497                }
498
499                new_amp += matrix[basis_idx * gate_dim + j] * state[target_idx];
500            }
501
502            self.work_buffer[idx] = new_amp;
503        }
504
505        state.copy_from_slice(&self.work_buffer[..state.len()]);
506        Ok(())
507    }
508}
509
510/// Benchmark comparison between specialized and generic implementations
511#[must_use]
512pub fn benchmark_specialization(
513    n_qubits: usize,
514    n_gates: usize,
515) -> (f64, f64, SpecializationStats) {
516    use quantrs2_circuit::builder::Circuit;
517    use scirs2_core::random::prelude::*;
518    use std::time::Instant;
519
520    let mut rng = thread_rng();
521
522    // For benchmark purposes, we'll use a fixed-size circuit
523    // In practice, you'd want to handle different sizes more elegantly
524    assert!(
525        (n_qubits == 8),
526        "Benchmark currently only supports 8 qubits"
527    );
528
529    let mut circuit = Circuit::<8>::new();
530
531    for _ in 0..n_gates {
532        let gate_type = rng.random_range(0..5);
533        let qubit = QubitId(rng.random_range(0..n_qubits as u32));
534
535        match gate_type {
536            0 => {
537                let _ = circuit.h(qubit);
538            }
539            1 => {
540                let _ = circuit.x(qubit);
541            }
542            2 => {
543                let _ = circuit.ry(qubit, rng.random_range(0.0..std::f64::consts::TAU));
544            }
545            3 => {
546                if n_qubits > 1 {
547                    let qubit2 = QubitId(rng.random_range(0..n_qubits as u32));
548                    if qubit != qubit2 {
549                        let _ = circuit.cnot(qubit, qubit2);
550                    }
551                }
552            }
553            _ => {
554                let _ = circuit.z(qubit);
555            }
556        }
557    }
558
559    // Run with specialized simulator
560    let mut specialized_sim = SpecializedStateVectorSimulator::new(Default::default());
561    let start = Instant::now();
562    let _ = specialized_sim
563        .run(&circuit)
564        .expect("Specialized simulator benchmark failed");
565    let specialized_time = start.elapsed().as_secs_f64();
566
567    // Run with base simulator
568    let mut base_sim = StateVectorSimulator::new();
569    let start = Instant::now();
570    let _ = base_sim
571        .run(&circuit)
572        .expect("Base simulator benchmark failed");
573    let base_time = start.elapsed().as_secs_f64();
574
575    (specialized_time, base_time, specialized_sim.stats.clone())
576}
577
578#[cfg(test)]
579mod tests {
580    use super::*;
581    use quantrs2_circuit::builder::Circuit;
582    use quantrs2_core::gate::single::{Hadamard, PauliX};
583
584    #[test]
585    fn test_specialized_simulator() {
586        let mut circuit = Circuit::<2>::new();
587        let _ = circuit.h(QubitId(0));
588        let _ = circuit.cnot(QubitId(0), QubitId(1));
589
590        let mut sim = SpecializedStateVectorSimulator::new(Default::default());
591        let state = sim
592            .run(&circuit)
593            .expect("Failed to run specialized simulator test circuit");
594
595        // Should create Bell state |00> + |11>
596        let expected_amp = 1.0 / std::f64::consts::SQRT_2;
597        assert!((state[0].norm() - expected_amp).abs() < 1e-10);
598        assert!(state[1].norm() < 1e-10);
599        assert!(state[2].norm() < 1e-10);
600        assert!((state[3].norm() - expected_amp).abs() < 1e-10);
601
602        // Check stats
603        assert_eq!(sim.get_stats().total_gates, 2);
604        assert_eq!(sim.get_stats().specialized_gates, 2);
605        assert_eq!(sim.get_stats().generic_gates, 0);
606    }
607
608    /// Regression test for the P1 finding: `reorder_gates` used to sort
609    /// gates purely by first-qubit-id, with no regard for whether the
610    /// reordered gates actually commute. `X(1)` followed by
611    /// `CNOT(control=0, target=1)` do *not* commute (they share qubit 1),
612    /// so a naive sort (which would hoist the CNOT, whose first qubit is
613    /// 0, ahead of the X, whose first qubit is 1) changes the computed
614    /// state. Running the same circuit with reordering enabled and
615    /// disabled must now produce identical results.
616    #[test]
617    fn test_reorder_gates_preserves_semantics_for_noncommuting_gates() {
618        let mut circuit = Circuit::<2>::new();
619        let _ = circuit.x(QubitId(1));
620        let _ = circuit.cnot(QubitId(0), QubitId(1));
621
622        let reordering_config = SpecializedSimulatorConfig {
623            enable_reordering: true,
624            ..Default::default()
625        };
626        let mut sim_reordered = SpecializedStateVectorSimulator::new(reordering_config);
627        let state_reordered = sim_reordered.run(&circuit).expect("reordered run failed");
628
629        let no_reorder_config = SpecializedSimulatorConfig {
630            enable_reordering: false,
631            ..Default::default()
632        };
633        let mut sim_baseline = SpecializedStateVectorSimulator::new(no_reorder_config);
634        let state_baseline = sim_baseline
635            .run(&circuit)
636            .expect("baseline (unreordered) run failed");
637
638        for (i, (reordered_amp, baseline_amp)) in state_reordered
639            .iter()
640            .zip(state_baseline.iter())
641            .enumerate()
642        {
643            assert!(
644                (reordered_amp - baseline_amp).norm() < 1e-10,
645                "reordering changed circuit semantics at index {i}: {reordered_amp:?} vs {baseline_amp:?}"
646            );
647        }
648    }
649
650    /// Direct unit test on `reorder_gates`: a diagonal gate (`RZ`) may be
651    /// hoisted past a non-commuting, qubit-disjoint gate boundary check --
652    /// but a non-diagonal gate sharing a qubit with a preceding gate must
653    /// never be moved past it.
654    #[test]
655    fn test_gates_commute_structural_checks() {
656        use quantrs2_core::gate::single::RotationZ;
657
658        let x0 = PauliX { target: QubitId(0) };
659        let x0_again = PauliX { target: QubitId(0) };
660        let rz0 = RotationZ {
661            target: QubitId(0),
662            theta: 0.5,
663        };
664        let rz0_b = RotationZ {
665            target: QubitId(0),
666            theta: 1.5,
667        };
668        let x1 = PauliX { target: QubitId(1) };
669
670        // Two X gates on the same qubit do not commute in general (X does
671        // not commute with itself under this conservative structural
672        // check -- it's not on the recognized diagonal list), so this must
673        // be false even though X*X happens to be trivial.
674        assert!(!SpecializedStateVectorSimulator::gates_commute(
675            &x0, &x0_again
676        ));
677        // Two diagonal RZ gates on the same qubit always commute.
678        assert!(SpecializedStateVectorSimulator::gates_commute(&rz0, &rz0_b));
679        // Disjoint qubits always commute regardless of gate type.
680        assert!(SpecializedStateVectorSimulator::gates_commute(&x0, &x1));
681    }
682
683    #[test]
684    fn test_benchmark() {
685        let (spec_time, base_time, stats) = benchmark_specialization(8, 20);
686
687        println!(
688            "Specialized: {:.3}ms, Base: {:.3}ms",
689            spec_time * 1000.0,
690            base_time * 1000.0
691        );
692        println!("Stats: {stats:?}");
693
694        // Specialized should generally be faster
695        assert!(spec_time <= base_time * 1.1); // Allow 10% margin
696    }
697}