Skip to main content

cqlib_core/qis/state/
statevector.rs

1// This code is part of Cqlib.
2// Modified to support reproducible state sampling.
3//
4// (C) Copyright China Telecom Quantum Group 2026
5//
6// This code is licensed under the Apache License, Version 2.0. You may
7// obtain a copy of this license in the LICENSE.txt file in the root directory
8// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
9//
10// Any modifications or derivative works of this code must retain this
11// copyright notice, and modified files need to carry a notice indicating
12// that they have been altered from the originals.
13//
14// Modified to validate terminal measurements and correct density matrix reset.
15
16//! Statevector quantum simulation.
17//!
18//! This module provides a high-performance statevector simulator for quantum circuits.
19//! It supports:
20//! - Single-qubit and multi-qubit gate operations
21//! - Controlled and multi-controlled gates
22//! - Parameterized rotations (RX, RY, RZ, etc.)
23//! - Native gates for superconducting qubits (fSim, XY)
24//! - Parallel execution for large statevectors using Rayon
25//!
26//! # Performance Features
27//! - Automatic parallelization threshold (14+ qubits)
28//! - Specialized kernels for common gates (avoiding generic matrix multiplication)
29//! - Memory-efficient in-place operations
30//! - Contiguous memory layout compatible with C/NumPy
31//!
32//! # Example
33//! ```rust
34//! use cqlib_core::qis::Statevector;
35//!
36//! // Create Bell state |Φ+⟩ = (|00⟩ + |11⟩)/√2
37//! let mut sv = Statevector::new(2);
38//! sv.apply_h(0);
39//! sv.apply_cx(0, 1);
40//!
41//! let probs = sv.probabilities();
42//! assert!((probs[0] - 0.5).abs() < 1e-10);
43//! assert!((probs[3] - 0.5).abs() < 1e-10);
44//! ```
45
46use super::aligned_buffer::AlignedBuffer;
47use crate::circuit::Measurement;
48use crate::circuit::circuit_impl::Circuit;
49use crate::circuit::circuit_param::CircuitParam;
50use crate::circuit::error::CircuitError;
51use crate::circuit::gate::StandardGate;
52use crate::circuit::gate::directive::Directive;
53use crate::circuit::gate::instruction::Instruction;
54use crate::device::{ExecutionResult, Outcome};
55use crate::qis::error::QisError;
56use crate::qis::observable::Observable;
57use crate::qis::pauli::{PauliString, Phase};
58use num_complex::Complex64;
59use rand::{Rng, SeedableRng, rngs::SmallRng};
60use rayon::prelude::*;
61use smallvec::SmallVec;
62use std::collections::HashMap;
63use std::f64::consts::FRAC_1_SQRT_2;
64
65// Statevector gates reduce to: for each (α, β) pair separated by `dist`,
66//     new_α = u00·α + u01·β
67//     new_β = u10·α + u11·β
68// where u_ij are complex scalars fixed for the entire gate.
69//
70// LLVM auto-vectorizes *real* multiplications well (H gate, Z gate), but
71// struggles with complex×complex: `(ac−bd, ad+bc)` requires a cross-term
72// subtraction that breaks the sequential dependency pattern. The AVX2 path
73// below uses `_mm256_addsub_pd` to express this in 5 SIMD ops per 2 complex
74// numbers, delivering ~4× throughput vs. scalar on this hot loop.
75//
76// Memory layout (interleaved): [re₀, im₀, re₁, im₁, re₂, im₂, …]
77//
78// The trick (Goto–van de Geijn complex GEMV):
79//   Given scalar u = (u_re, u_im), vector of 2 complexes a = [a0, a1]:
80//     a_perm = permute(a, 0101b)     → [a0.im, a0.re, a1.im, a1.re]  (swap re/im)
81//     term1  = a  * broadcast(u_re)  → [a0.re·u_re, a0.im·u_re, …]
82//     term2  = a_perm * broadcast(u_im) → [a0.im·u_im, a0.re·u_im, …]
83//     result = addsub(term1, term2)  → even indices subtract, odd add:
84//              [a0.re·u_re − a0.im·u_im,   a0.im·u_re + a0.re·u_im, …]
85//              = [re(u·a0), im(u·a0), re(u·a1), im(u·a1)] ✓
86
87/// Computes `u·a` for two consecutive Complex64 values using AVX2.
88///
89/// # Safety
90/// Caller must ensure:
91/// - `avx2` + `fma` features are enabled at runtime.
92/// - `a_ptr` points to at least 4 aligned `f64` words (= 2 Complex64, = 32 bytes).
93#[cfg(target_arch = "x86_64")]
94#[target_feature(enable = "avx2,fma")]
95#[inline]
96unsafe fn cmul2_avx2(
97    a_ptr: *const f64,
98    u_re: std::arch::x86_64::__m256d,
99    u_im: std::arch::x86_64::__m256d,
100) -> std::arch::x86_64::__m256d {
101    use std::arch::x86_64::*;
102    // SAFETY: caller guarantees alignment and valid pointer.
103    unsafe {
104        let a = _mm256_load_pd(a_ptr); // [a0.re, a0.im, a1.re, a1.im]
105        let a_perm = _mm256_permute_pd(a, 0b0101); // [a0.im, a0.re, a1.im, a1.re]
106        let term1 = _mm256_mul_pd(a, u_re); // [a.re·u_re, a.im·u_re, …]
107        // addsub: even=subtract, odd=add → [re(u·a), im(u·a), …]
108        _mm256_addsub_pd(term1, _mm256_mul_pd(a_perm, u_im))
109    }
110}
111
112/// AVX2 kernel for a single-qubit gate on one chunk `[lower | upper]`.
113///
114/// Processes pairs (α, β) where lower[i] = α, upper[i] = β, applying:
115///   new_α = u00·α + u01·β
116///   new_β = u10·α + u11·β
117///
118/// Requires `dist` ≥ 2 (handled by the caller which falls back to scalar).
119///
120/// # Safety
121/// - `avx2` + `fma` features enabled.
122/// - `lower.as_ptr()` and `upper.as_ptr()` are 32-byte aligned (guaranteed by
123///   our `AlignedBuffer<Complex64>` and the chunk layout: `dist` is always a
124///   power of 2 ≥ 2, so the upper pointer is also 32-byte aligned).
125/// - `lower.len() == upper.len()` and both are multiples of 2.
126#[cfg(target_arch = "x86_64")]
127#[target_feature(enable = "avx2,fma")]
128unsafe fn sqg_kernel_avx2(
129    lower: &mut [Complex64],
130    upper: &mut [Complex64],
131    u00: Complex64,
132    u01: Complex64,
133    u10: Complex64,
134    u11: Complex64,
135) {
136    use std::arch::x86_64::*;
137    // SAFETY: avx2+fma enabled; caller guarantees alignment and length.
138    unsafe {
139        // Broadcast scalar components.
140        let v00_re = _mm256_set1_pd(u00.re);
141        let v00_im = _mm256_set1_pd(u00.im);
142        let v01_re = _mm256_set1_pd(u01.re);
143        let v01_im = _mm256_set1_pd(u01.im);
144        let v10_re = _mm256_set1_pd(u10.re);
145        let v10_im = _mm256_set1_pd(u10.im);
146        let v11_re = _mm256_set1_pd(u11.re);
147        let v11_im = _mm256_set1_pd(u11.im);
148
149        let n = lower.len(); // number of Complex64 elements
150        let mut i = 0;
151
152        // Main loop: 2 Complex64 per iteration (= 4 f64 = 1 × __m256d).
153        while i + 2 <= n {
154            let a_ptr = lower.as_ptr().add(i) as *const f64;
155            let b_ptr = upper.as_ptr().add(i) as *const f64;
156
157            // u00·α + u01·β
158            let ua = cmul2_avx2(a_ptr, v00_re, v00_im);
159            let ub = cmul2_avx2(b_ptr, v01_re, v01_im);
160            let new_alpha = _mm256_add_pd(ua, ub);
161
162            // u10·α + u11·β
163            let wa = cmul2_avx2(a_ptr, v10_re, v10_im);
164            let wb = cmul2_avx2(b_ptr, v11_re, v11_im);
165            let new_beta = _mm256_add_pd(wa, wb);
166
167            _mm256_store_pd(lower.as_mut_ptr().add(i) as *mut f64, new_alpha);
168            _mm256_store_pd(upper.as_mut_ptr().add(i) as *mut f64, new_beta);
169
170            i += 2;
171        }
172
173        // Scalar tail (handles dist == 1, i.e., qubit 0).
174        while i < n {
175            let a = lower[i];
176            let b = upper[i];
177            lower[i] = u00 * a + u01 * b;
178            upper[i] = u10 * a + u11 * b;
179            i += 1;
180        }
181    }
182}
183
184/// Scalar fallback for one chunk of a single-qubit gate.
185#[inline(always)]
186fn sqg_kernel_scalar(
187    lower: &mut [Complex64],
188    upper: &mut [Complex64],
189    u00: Complex64,
190    u01: Complex64,
191    u10: Complex64,
192    u11: Complex64,
193) {
194    lower
195        .iter_mut()
196        .zip(upper.iter_mut())
197        .for_each(|(alpha, beta)| {
198            let a = *alpha;
199            let b = *beta;
200            *alpha = u00 * a + u01 * b;
201            *beta = u10 * a + u11 * b;
202        });
203}
204
205/// Runtime-dispatched single-qubit gate kernel.
206///
207/// Dispatches to AVX2 (x86_64) or scalar. On AArch64, LLVM auto-vectorizes
208/// the scalar path using NEON, which is sufficient for interleaved Complex64.
209///
210/// # Safety
211/// `lower.len() == upper.len()` must hold; `lower.as_ptr()` must be 32-byte
212/// aligned (guaranteed by `AlignedBuffer` + power-of-2 `dist` offset).
213#[inline]
214#[allow(unreachable_code)] // scalar path unreachable when AVX2 always taken
215fn sqg_kernel(
216    lower: &mut [Complex64],
217    upper: &mut [Complex64],
218    u00: Complex64,
219    u01: Complex64,
220    u10: Complex64,
221    u11: Complex64,
222) {
223    #[cfg(target_arch = "x86_64")]
224    if is_x86_feature_detected!("avx2") && is_x86_feature_detected!("fma") {
225        // SAFETY: avx2+fma confirmed; AlignedBuffer guarantees 64-byte base
226        // alignment; dist is always a power of 2, so upper ptr is also aligned.
227        return unsafe { sqg_kernel_avx2(lower, upper, u00, u01, u10, u11) };
228    }
229    sqg_kernel_scalar(lower, upper, u00, u01, u10, u11);
230}
231
232/// Threshold for parallel execution (in number of qubits).
233///
234/// For small systems, the overhead of thread pool scheduling exceeds
235/// the performance gains from parallelization. Systems with fewer than
236/// 2^14 = 16384 amplitudes use serial execution.
237const PARALLEL_THRESHOLD: usize = 14;
238
239/// Conditionally executes serial or parallel iteration based on qubit count.
240///
241/// Switches between Rayon parallel iteration and standard serial iteration
242/// to avoid thread pool overhead for small quantum states.
243///
244/// # Arguments
245/// * `$num_qubits` - Number of qubits (determines execution mode)
246/// * `$data` - Mutable slice to process
247/// * `$chunk_size` - Size of each chunk for processing
248/// * `$body` - Closure to apply to each chunk
249///
250/// # Example
251/// ```rust,ignore
252/// with_maybe_par!(num_qubits, data, chunk_size, |chunk| {
253///     for elem in chunk { *elem *= 2.0; }
254/// });
255/// ```
256macro_rules! with_maybe_par {
257    ($num_qubits:expr, $data:expr, $chunk_size:expr, $body:expr) => {{
258        use rayon::prelude::*;
259        use std::iter::Iterator;
260
261        if $num_qubits < PARALLEL_THRESHOLD {
262            $data.chunks_exact_mut($chunk_size).for_each($body);
263        } else {
264            $data.par_chunks_exact_mut($chunk_size).for_each($body);
265        }
266    }};
267}
268
269/// Quantum statevector representing a pure quantum state.
270///
271/// A statevector describes the quantum state of an N-qubit system as a vector
272/// of 2^N complex amplitudes. The state |ψ⟩ = Σᵢ αᵢ|i⟩ is stored with αᵢ
273/// as the amplitude for basis state |i⟩ (i in binary representation).
274///
275/// # Memory Layout
276/// The `data` vector uses contiguous memory layout (compatible with C/NumPy),
277/// where the amplitude at index `i` corresponds to basis state |i⟩ with
278/// qubit indices mapping to bits from least significant (qubit 0) to most.
279///
280/// # Memory Layout
281///
282/// The `data` buffer uses a 64-byte aligned allocation so that SIMD kernels
283/// (AVX2, SSE2) can safely use aligned load/store instructions.
284///
285/// # Example
286/// ```rust
287/// use cqlib_core::qis::Statevector;
288///
289/// // Create a 2-qubit state in |00⟩
290/// let sv = Statevector::new(2);
291/// assert_eq!(sv.num_qubits, 2);
292/// assert_eq!(sv.data().len(), 4); // 2^2 amplitudes
293///
294/// // |00⟩ state: amplitude 1.0 at index 0, 0.0 elsewhere
295/// assert_eq!(sv.data()[0], num_complex::Complex64::new(1.0, 0.0));
296/// ```
297#[derive(Debug, Clone)]
298pub struct Statevector {
299    /// Complex amplitudes for each basis state. Length is 2^N where N = num_qubits.
300    /// 64-byte aligned for SIMD compatibility (see [`AlignedBuffer`]).
301    data: AlignedBuffer<Complex64>,
302    /// Circuit qubit identifiers mapped to storage positions.
303    qubit_map: super::QubitMap,
304    /// Number of qubits in the system.
305    pub num_qubits: usize,
306}
307
308impl Statevector {
309    /// Returns the complex amplitudes as a shared slice.
310    ///
311    /// Length is `2^num_qubits`. The amplitude at index `i` is the coefficient of basis
312    /// state |i⟩ in the computational basis.
313    ///
314    /// # Example
315    /// ```rust
316    /// use cqlib_core::qis::Statevector;
317    ///
318    /// let sv = Statevector::new(2); // |00⟩
319    /// assert_eq!(sv.data().len(), 4);
320    /// assert_eq!(sv.data()[0], num_complex::Complex64::new(1.0, 0.0));
321    /// ```
322    pub fn data(&self) -> &[Complex64] {
323        &self.data
324    }
325
326    /// Returns the complex amplitudes as a mutable slice.
327    ///
328    /// Mutating the slice can break normalization; callers are responsible for
329    /// preserving a valid statevector.
330    pub fn data_mut(&mut self) -> &mut [Complex64] {
331        &mut self.data
332    }
333
334    /// Creates a new statevector initialized to the |0...0⟩ state.
335    ///
336    /// The statevector represents the quantum state as a vector of 2^N complex amplitudes,
337    /// where N is the number of qubits. All amplitudes are initialized to zero except
338    /// the first element (|0...0⟩) which is set to 1.0.
339    ///
340    /// # Arguments
341    /// * `num_qubits` - Number of qubits in the system
342    ///
343    /// # Returns
344    /// A new `Statevector` instance in the ground state
345    ///
346    /// # Example
347    /// ```rust
348    /// use cqlib_core::qis::Statevector;
349    ///
350    /// let sv = Statevector::new(2); // |00⟩ state
351    /// assert_eq!(sv.num_qubits, 2);
352    /// assert_eq!(sv.data().len(), 4); // 2^2 amplitudes
353    /// ```
354    pub fn new(num_qubits: usize) -> Self {
355        let size = 1 << num_qubits;
356        // 64-byte aligned allocation — zero-initialised (all amplitudes = 0+0i).
357        let mut data = AlignedBuffer::<Complex64>::new_zeroed(size);
358        data[0] = Complex64::new(1.0, 0.0); // |0...0⟩
359        Statevector {
360            data,
361            num_qubits,
362            qubit_map: None,
363        }
364    }
365
366    /// Creates a statevector from initial amplitudes with normalization check.
367    ///
368    /// # Arguments
369    /// * `num_qubits` - Number of qubits in the system
370    /// * `initial_state` - Vector of 2^N complex amplitudes
371    ///
372    /// # Errors
373    /// Returns an error if:
374    /// - `initial_state` length doesn't match 2^num_qubits
375    /// - State is not normalized (sum of probabilities ≠ 1.0 within a dimension-scaled tolerance)
376    ///
377    /// # Example
378    /// ```rust
379    /// use cqlib_core::qis::Statevector;
380    /// use num_complex::Complex64;
381    ///
382    /// // Create |+0⟩ = (|00⟩ + |10⟩)/√2
383    /// let amps = vec![
384    ///     Complex64::new(std::f64::consts::FRAC_1_SQRT_2, 0.0), // |00⟩
385    ///     Complex64::new(0.0, 0.0),                             // |01⟩
386    ///     Complex64::new(std::f64::consts::FRAC_1_SQRT_2, 0.0), // |10⟩
387    ///     Complex64::new(0.0, 0.0),                             // |11⟩
388    /// ];
389    /// let sv = Statevector::from_state(2, amps);
390    /// ```
391    pub fn from_state(num_qubits: usize, initial_state: Vec<Complex64>) -> Result<Self, QisError> {
392        let size = 1 << num_qubits;
393        if initial_state.len() != size {
394            return Err(QisError::InvalidStateDimension(initial_state.len()));
395        }
396        // Verify normalization. The tolerance scales with vector length because
397        // summing many amplitudes accumulates floating-point roundoff.
398        let norm: f64 = initial_state.iter().map(|c| c.norm_sqr()).sum();
399        let tolerance = 1e-10 * (size.max(1) as f64);
400        if (norm - 1.0).abs() >= tolerance {
401            return Err(QisError::NotNormalized);
402        }
403
404        // Copy caller-provided Vec into 64-byte aligned buffer.
405        let mut data = AlignedBuffer::<Complex64>::new_zeroed(size);
406        data.as_mut_slice().copy_from_slice(&initial_state);
407        Ok(Statevector {
408            data,
409            num_qubits,
410            qubit_map: None,
411        })
412    }
413
414    /// Constructs a statevector by simulating a quantum circuit.
415    ///
416    /// Executes the circuit gates sequentially to evolve the initial |0...0⟩ state.
417    /// Complex gates are decomposed before execution.
418    ///
419    /// # Arguments
420    /// * `circuit` - The quantum circuit to simulate
421    ///
422    /// # Returns
423    /// * `Ok(Statevector)` - The resulting state after circuit execution
424    /// * `Err(QisError)` - If the circuit contains unsupported operations
425    ///   (control flow gates, symbolic parameters) or decomposition fails
426    ///
427    /// # Supported Instructions
428    /// - Standard single and multi-qubit gates
429    /// - Controlled gates (CX, CY, CZ, CRX, CRY, CRZ)
430    /// - Multi-controlled gates (CCX/Toffoli)
431    /// - Unitary gates with matrix representation
432    /// - Barriers and delays (ignored)
433    ///
434    /// # Example
435    /// ```rust
436    /// use cqlib_core::circuit::{Circuit, Qubit};
437    /// use cqlib_core::qis::Statevector;
438    ///
439    /// // Create Bell state: |Φ+⟩ = (|00⟩ + |11⟩)/√2
440    /// let mut circuit = Circuit::new(2);
441    /// circuit.h(Qubit::new(0)).unwrap();
442    /// circuit.cx(Qubit::new(0), Qubit::new(1)).unwrap();
443    ///
444    /// let sv = Statevector::from_circuit(&circuit).unwrap();
445    /// ```
446    pub fn from_circuit(circuit: &Circuit) -> Result<Self, QisError> {
447        let mut sv = Statevector::new(circuit.num_qubits());
448        sv.apply_circuit(circuit)?;
449        Ok(sv)
450    }
451
452    /// Applies a quantum circuit to this statevector in-place.
453    ///
454    /// Once bound to circuit qubit IDs, subsequent circuits must have the same
455    /// IDs in the same order. The mapping is committed only on success; gates
456    /// completed before an error are not rolled back.
457    ///
458    /// As in `from_circuit`, terminal measurements are ignored. A measured qubit
459    /// used by a later gate or Reset is rejected before the state is changed.
460    /// Independent gates, repeated measurements, Barrier, Delay, and Store are allowed.
461    ///
462    /// The circuit is first decomposed into basic gates via [`Circuit::decompose`],
463    /// then each operation is applied sequentially to the current state.
464    ///
465    /// # Arguments
466    /// * `circuit` - The circuit to apply. Must have the same number of qubits
467    ///   as this statevector.
468    ///
469    /// # Returns
470    /// * `Ok(())` - If all operations were applied successfully
471    /// * `Err(QisError)` - If the circuit cannot be simulated
472    ///
473    /// # Errors
474    /// - [`QisError::InvalidStateDimension`] if `circuit.num_qubits() != self.num_qubits`
475    /// - [`QisError::UnsupportedOperation`] if the circuit contains classical control flow
476    /// - [`QisError::CircuitError`] if a gate lacks a matrix representation, contains
477    ///   unresolved symbolic parameters, or a referenced qubit is not found
478    ///
479    /// # Supported Instructions
480    /// - Standard single and multi-qubit gates
481    /// - Controlled gates (CX, CY, CZ, CRX, CRY, CRZ)
482    /// - Multi-controlled gates (CCX / Toffoli)
483    /// - Unitary gates with an explicit matrix representation
484    /// - `Reset`
485    /// - Barriers and delays (ignored)
486    /// - Measurement output declarations from `Circuit::measure*` (ignored)
487    pub fn apply_circuit(&mut self, circuit: &Circuit) -> Result<(), QisError> {
488        if circuit.num_qubits() != self.num_qubits {
489            return Err(QisError::InvalidStateDimension(circuit.num_qubits()));
490        }
491        if circuit
492            .operations()
493            .iter()
494            .any(|op| matches!(op.instruction, Instruction::ClassicalControl(_)))
495        {
496            return Err(QisError::UnsupportedOperation(
497                "classical control flow is not supported in statevector simulation".to_string(),
498            ));
499        }
500        // Decompose circuit to basic gates
501        let circuit = circuit.decompose()?;
502        super::validate_terminal_measurements(&circuit)?;
503        let sv = self;
504
505        let qubit_map = super::prepare_qubit_map(&circuit.qubits(), &sv.qubit_map)?;
506
507        // Precompute all parameter values
508        let parameter_values: Vec<Option<f64>> = circuit
509            .parameters()
510            .iter()
511            .map(|p| p.evaluate(&None).ok())
512            .collect();
513
514        for op in circuit.operations() {
515            // Resolve parameters using precomputed values
516            let params: Vec<f64> = op
517                .params
518                .iter()
519                .map(|p| match p {
520                    CircuitParam::Fixed(v) => Ok(*v),
521                    CircuitParam::Index(idx) => parameter_values
522                        .get(*idx as usize)
523                        .copied()
524                        .flatten()
525                        .ok_or(QisError::CircuitError(CircuitError::SymbolicParameterError)),
526                })
527                .collect::<Result<Vec<_>, QisError>>()?;
528
529            // Get storage positions
530            let qubit_indices: Result<Vec<usize>, QisError> =
531                op.qubits
532                    .iter()
533                    .map(|q| {
534                        qubit_map.get(q).copied().ok_or_else(|| {
535                            QisError::CircuitError(CircuitError::QubitNotFound(q.id()))
536                        })
537                    })
538                    .collect();
539            let qubit_indices = qubit_indices?;
540
541            match &op.instruction {
542                Instruction::Standard(gate) => {
543                    sv.apply_standard_gate(*gate, &qubit_indices, &params)?;
544                }
545                Instruction::McGate(mc_gate) => {
546                    let num_controls = mc_gate.num_ctrl_qubits();
547                    let base_gate = mc_gate.base_gate();
548
549                    if num_controls == 1 {
550                        // Single-controlled gate
551                        let control = qubit_indices[0];
552                        let target = qubit_indices[1];
553                        match base_gate {
554                            StandardGate::X => sv.apply_cx(control, target)?,
555                            StandardGate::Y => sv.apply_cy(control, target)?,
556                            StandardGate::Z => sv.apply_cz(control, target)?,
557                            StandardGate::RX => {
558                                let theta = params[0];
559                                sv.apply_crx(control, target, theta)?;
560                            }
561                            StandardGate::RY => {
562                                let theta = params[0];
563                                sv.apply_cry(control, target, theta)?;
564                            }
565                            StandardGate::RZ => {
566                                let theta = params[0];
567                                sv.apply_crz(control, target, theta)?;
568                            }
569                            _ => {
570                                let matrix = mc_gate.matrix(&params).map_err(|_| {
571                                    QisError::CircuitError(CircuitError::NoMatrixRepresentation)
572                                })?;
573                                sv.apply_unitary_gate(&qubit_indices, &matrix)?;
574                            }
575                        }
576                    } else if num_controls == 2 && *base_gate == StandardGate::X {
577                        // Toffoli gate
578                        let c0 = qubit_indices[0];
579                        let c1 = qubit_indices[1];
580                        let target = qubit_indices[2];
581                        sv.apply_ccx(c0, c1, target)?;
582                    } else {
583                        let matrix = mc_gate.matrix(&params).map_err(|_| {
584                            QisError::CircuitError(CircuitError::NoMatrixRepresentation)
585                        })?;
586                        sv.apply_unitary_gate(&qubit_indices, &matrix)?;
587                    }
588                }
589                Instruction::UnitaryGate(u_gate) => {
590                    if let Some(matrix) = u_gate.matrix() {
591                        sv.apply_unitary_gate(&qubit_indices, matrix)?;
592                    } else {
593                        return Err(QisError::CircuitError(CircuitError::NoMatrixRepresentation));
594                    }
595                }
596                Instruction::CircuitGate(_) => {
597                    return Err(QisError::CircuitError(CircuitError::InvalidOperation(
598                        "CircuitGate should have been decomposed".to_string(),
599                    )));
600                }
601                Instruction::Directive(directive) => match directive {
602                    Directive::Barrier => {}
603                    Directive::Measure => {
604                        return Err(QisError::UnsupportedOperation(
605                            "legacy Measure directive is not supported by statevector circuit evolution; use Circuit::measure as an output declaration".to_string(),
606                        ));
607                    }
608                    Directive::Reset => {
609                        let qubit = qubit_indices[0];
610                        sv.reset(qubit)?;
611                    }
612                },
613                Instruction::Delay => {
614                    // Ignore delays
615                    continue;
616                }
617                Instruction::ClassicalControl(_) => {
618                    return Err(QisError::UnsupportedOperation(
619                        "classical control flow is not supported in statevector simulation"
620                            .to_string(),
621                    ));
622                }
623                Instruction::ClassicalData(_) => {}
624            }
625        }
626
627        sv.qubit_map = Some(qubit_map);
628        Ok(())
629    }
630
631    /// Applies a standard gate to the statevector.
632    ///
633    /// Internal dispatcher that routes to specialized implementations
634    /// based on gate type.
635    ///
636    /// # Arguments
637    /// * `gate` - The standard gate to apply
638    /// * `qubits` - Target qubit indices
639    /// * `params` - Gate parameters (for parameterized gates)
640    pub fn apply_standard_gate(
641        &mut self,
642        gate: StandardGate,
643        qubits: &[usize],
644        params: &[f64],
645    ) -> Result<(), QisError> {
646        if qubits.len() != gate.num_qubits() {
647            return Err(QisError::InvalidParameterValue(format!(
648                "Gate {:?} requires {} qubits, got {}",
649                gate,
650                gate.num_qubits(),
651                qubits.len()
652            )));
653        }
654        if params.len() != gate.num_params() {
655            return Err(QisError::InvalidParameterValue(format!(
656                "Gate {:?} requires {} parameters, got {}",
657                gate,
658                gate.num_params(),
659                params.len()
660            )));
661        }
662        match gate {
663            // Single-qubit gates
664            StandardGate::I => { /* no-op */ }
665            StandardGate::X => self.apply_x(qubits[0])?,
666            StandardGate::Y => self.apply_y(qubits[0])?,
667            StandardGate::Z => self.apply_z(qubits[0])?,
668            StandardGate::H => self.apply_h(qubits[0])?,
669            StandardGate::S => self.apply_s(qubits[0])?,
670            StandardGate::SDG => self.apply_sdg(qubits[0])?,
671            StandardGate::T => self.apply_t(qubits[0])?,
672            StandardGate::TDG => self.apply_tdg(qubits[0])?,
673            StandardGate::RX => self.apply_rx(qubits[0], params[0])?,
674            StandardGate::RY => self.apply_ry(qubits[0], params[0])?,
675            StandardGate::RZ => self.apply_rz(qubits[0], params[0])?,
676            StandardGate::Phase => self.apply_phase(qubits[0], params[0])?,
677            StandardGate::X2P => self.apply_x2p(qubits[0])?,
678            StandardGate::X2M => self.apply_x2m(qubits[0])?,
679            StandardGate::Y2P => self.apply_y2p(qubits[0])?,
680            StandardGate::Y2M => self.apply_y2m(qubits[0])?,
681            StandardGate::RXY => self.apply_rxy(qubits[0], params[0], params[1])?,
682            StandardGate::XY => self.apply_xy(qubits[0], params[0])?,
683            StandardGate::XY2P => self.apply_xy2p(qubits[0], params[0])?,
684            StandardGate::XY2M => self.apply_xy2m(qubits[0], params[0])?,
685            StandardGate::U => {
686                self.apply_u(qubits[0], params[0], params[1], params[2])?;
687            }
688            StandardGate::GPhase => self.apply_gphase(params[0])?,
689
690            // Two-qubit gates
691            StandardGate::CX => self.apply_cx(qubits[0], qubits[1])?,
692            StandardGate::CY => self.apply_cy(qubits[0], qubits[1])?,
693            StandardGate::CZ => self.apply_cz(qubits[0], qubits[1])?,
694            StandardGate::SWAP => self.apply_swap(qubits[0], qubits[1])?,
695            StandardGate::RXX => self.apply_rxx(qubits[0], qubits[1], params[0])?,
696            StandardGate::RYY => self.apply_ryy(qubits[0], qubits[1], params[0])?,
697            StandardGate::RZZ => self.apply_rzz(qubits[0], qubits[1], params[0])?,
698            StandardGate::RZX => self.apply_rzx(qubits[0], qubits[1], params[0])?,
699
700            // Controlled rotation gates
701            StandardGate::CRX => self.apply_crx(qubits[0], qubits[1], params[0])?,
702            StandardGate::CRY => self.apply_cry(qubits[0], qubits[1], params[0])?,
703            StandardGate::CRZ => self.apply_crz(qubits[0], qubits[1], params[0])?,
704
705            // Three-qubit gates
706            StandardGate::CCX => self.apply_ccx(qubits[0], qubits[1], qubits[2])?,
707
708            // Simulator gates
709            StandardGate::FSIM => self.apply_fsim(qubits[0], qubits[1], params[0], params[1])?,
710        }
711        Ok(())
712    }
713
714    /// Computes the measurement probability distribution over all basis states.
715    ///
716    /// Returns P(|i⟩) = |αᵢ|² for each basis state |i⟩.
717    /// Uses parallel iteration for large statevectors.
718    ///
719    /// # Returns
720    /// Vector of probabilities summing to 1.0 (within floating-point precision).
721    ///
722    /// # Example
723    /// ```rust
724    /// use cqlib_core::qis::Statevector;
725    ///
726    /// let mut sv = Statevector::new(1);
727    /// sv.apply_h(0);
728    /// let probs = sv.probabilities();
729    /// // |+⟩ state: P(|0⟩) = P(|1⟩) = 0.5
730    /// assert!((probs[0] - 0.5).abs() < 1e-10);
731    /// assert!((probs[1] - 0.5).abs() < 1e-10);
732    /// ```
733    pub fn probabilities(&self) -> Vec<f64> {
734        if self.num_qubits < PARALLEL_THRESHOLD {
735            self.data.iter().map(|c| c.norm_sqr()).collect()
736        } else {
737            self.data.par_iter().map(|c| c.norm_sqr()).collect()
738        }
739    }
740
741    /// Validates that a qubit index is within bounds.
742    fn validate_qubit(&self, qubit: usize) -> Result<(), QisError> {
743        if self.num_qubits == 0 {
744            return Err(QisError::InvalidParameterValue(
745                "no valid qubit indices in a zero-qubit system".to_string(),
746            ));
747        }
748        if qubit >= self.num_qubits {
749            return Err(QisError::IndexOutOfBounds {
750                index: qubit,
751                max: self.num_qubits.saturating_sub(1),
752            });
753        }
754        Ok(())
755    }
756
757    /// Validates that two qubit indices are within bounds and distinct.
758    fn validate_two_qubits(&self, q0: usize, q1: usize) -> Result<(), QisError> {
759        self.validate_qubit(q0)?;
760        self.validate_qubit(q1)?;
761        if q0 == q1 {
762            return Err(QisError::InvalidParameterValue(
763                "Qubit indices must be distinct".to_string(),
764            ));
765        }
766        Ok(())
767    }
768
769    /// Validates that all qubit indices are within bounds and distinct.
770    fn validate_qubits(&self, qubits: &[usize]) -> Result<(), QisError> {
771        for (i, &q) in qubits.iter().enumerate() {
772            self.validate_qubit(q)?;
773            for &other in &qubits[..i] {
774                if q == other {
775                    return Err(QisError::InvalidParameterValue(format!(
776                        "Duplicate qubit index {} in gate operation",
777                        q
778                    )));
779                }
780            }
781        }
782        Ok(())
783    }
784
785    /// Applies an arbitrary single-qubit gate.
786    ///
787    /// General-purpose method for applying any 2x2 unitary matrix.
788    /// For standard gates, prefer specialized methods (apply_h, apply_x, etc.)
789    /// which have optimized implementations.
790    ///
791    /// # Arguments
792    /// * `qubit` - Target qubit index
793    /// * `matrix` - 2x2 unitary matrix [[u00, u01], [u10, u11]]
794    ///
795    /// # Errors
796    /// Returns [`QisError::IndexOutOfBounds`] if `qubit` is out of bounds.
797    ///
798    /// # Example
799    /// ```rust,ignore
800    /// use cqlib_core::qis::Statevector;
801    /// use num_complex::Complex64;
802    ///
803    /// let mut sv = Statevector::new(1);
804    /// // Apply a custom rotation
805    /// let matrix = [
806    ///     [Complex64::new(0.0, 0.0), Complex64::new(1.0, 0.0)],
807    ///     [Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)],
808    /// ];
809    /// sv.apply_single_qubit_gate(0, matrix); // Equivalent to X gate
810    /// ```
811    pub fn apply_single_qubit_gate(
812        &mut self,
813        qubit: usize,
814        matrix: [[Complex64; 2]; 2],
815    ) -> Result<(), QisError> {
816        self.validate_qubit(qubit)?;
817        let dist = 1 << qubit;
818
819        // Preload matrix elements to registers, reducing memory access in loop
820        let u00 = matrix[0][0];
821        let u01 = matrix[0][1];
822        let u10 = matrix[1][0];
823        let u11 = matrix[1][1];
824
825        // Dispatch: AVX2 SIMD on x86_64 (2 Complex64/cycle), scalar elsewhere.
826        // The SIMD path uses `_mm256_addsub_pd` to compute (ac−bd, ad+bc) for
827        // two complex pairs per 256-bit register, bypassing LLVM's inability
828        // to auto-vectorize interleaved re/im complex multiplication.
829        let kernel = |chunk: &mut [Complex64]| {
830            let (lower, upper) = chunk.split_at_mut(dist);
831            sqg_kernel(lower, upper, u00, u01, u10, u11);
832        };
833
834        with_maybe_par!(self.num_qubits, self.data, 2 * dist, kernel);
835        Ok(())
836    }
837
838    /// Apply a general two-qubit gate.
839    ///
840    /// # Arguments
841    /// * `q0` - First qubit index (logical)
842    /// * `q1` - Second qubit index (logical)
843    /// * `matrix` - 4x4 unitary matrix in the standard computational basis
844    ///   ordered as |00⟩, |01⟩, |10⟩, |11⟩ where the bits are (q0, q1).
845    ///   That is, matrix[i][j] is the amplitude of |i⟩ when input is |j⟩,
846    ///   with i = q0*2 + q1 (binary: q0 is MSB, q1 is LSB).
847    ///
848    /// # Example - CNOT with q0 as control and q1 as target:
849    /// The CNOT gate flips the target when control is |1⟩:
850    /// |00⟩ -> |00⟩, |01⟩ -> |01⟩, |10⟩ -> |11⟩, |11⟩ -> |10⟩
851    ///
852    /// This corresponds to the matrix (rows/cols ordered as |00⟩, |01⟩, |10⟩, |11⟩):
853    /// ```text
854    /// [[1, 0, 0, 0],
855    ///  [0, 1, 0, 0],
856    ///  [0, 0, 0, 1],
857    ///  [0, 0, 1, 0]]
858    /// ```
859    pub fn apply_two_qubit_gate(
860        &mut self,
861        q0: usize,
862        q1: usize,
863        matrix: [[Complex64; 4]; 4],
864    ) -> Result<(), QisError> {
865        self.validate_two_qubits(q0, q1)?;
866
867        // 1. Identify physical high/low bit positions in memory
868        // q_max determines the outer parallel chunk size
869        // q_min determines the inner serial stride
870        let (q_min, q_max) = if q0 < q1 { (q0, q1) } else { (q1, q0) };
871        let dist_max = 1 << q_max;
872        let dist_min = 1 << q_min;
873
874        // 2. Preload matrix elements to registers, avoiding array access in hot loop
875        // Naming: m_row_col corresponds to logical state |q0 q1>
876        let m00 = matrix[0][0];
877        let m01 = matrix[0][1];
878        let m02 = matrix[0][2];
879        let m03 = matrix[0][3];
880        let m10 = matrix[1][0];
881        let m11 = matrix[1][1];
882        let m12 = matrix[1][2];
883        let m13 = matrix[1][3];
884        let m20 = matrix[2][0];
885        let m21 = matrix[2][1];
886        let m22 = matrix[2][2];
887        let m23 = matrix[2][3];
888        let m30 = matrix[3][0];
889        let m31 = matrix[3][1];
890        let m32 = matrix[3][2];
891        let m33 = matrix[3][3];
892
893        // 3. Choose serial or parallel based on qubit count
894        // Split statevector into chunks by max_qubit
895        // Each chunk contains 2 * dist_max elements
896        // Layout: [ ...dist_max (q_max=0)... | ...dist_max (q_max=1)... ]
897        let kernel = |chunk: &mut [Complex64]| {
898            // Split chunk into q_max=0 and q_max=1 parts
899            let (part0, part1) = chunk.split_at_mut(dist_max);
900
901            // 4. Inner serial traversal
902            // Further split by q_min within each part
903            // Use zip to traverse both parts simultaneously for optimal bounds check elision
904            part0
905                .chunks_exact_mut(2 * dist_min)
906                .zip(part1.chunks_exact_mut(2 * dist_min))
907                .for_each(|(sub_chunk0, sub_chunk1)| {
908                    // sub_chunk0: q_max=0 region
909                    // sub_chunk1: q_max=1 region
910
911                    // Further split by q_min
912                    let (v00_slice, v01_slice) = sub_chunk0.split_at_mut(dist_min);
913                    let (v10_slice, v11_slice) = sub_chunk1.split_at_mut(dist_min);
914
915                    // Now we have four slices corresponding to physical bit states:
916                    // v00_slice: q_max=0, q_min=0
917                    // v01_slice: q_max=0, q_min=1
918                    // v10_slice: q_max=1, q_min=0
919                    // v11_slice: q_max=1, q_min=1
920
921                    // 5. Core computation loop
922                    // Use iterator zip to avoid index calculations, enabling SIMD
923                    for (((c00, c01), c10), c11) in v00_slice
924                        .iter_mut()
925                        .zip(v01_slice.iter_mut())
926                        .zip(v10_slice.iter_mut())
927                        .zip(v11_slice.iter_mut())
928                    {
929                        let a = *c00; // Phys: 00
930                        let b = *c01; // Phys: 01
931                        let c = *c10; // Phys: 10
932                        let d = *c11; // Phys: 11
933
934                        // 6. Logical mapping: determine input vector positions based on q0 vs q1
935                        // logical_amp_00 corresponds to |q0=0, q1=0>
936                        // logical_amp_01 corresponds to |q0=0, q1=1> ...
937                        let (in0, in1, in2, in3) = if q0 < q1 {
938                            // q0=min, q1=max
939                            // Phys 00 (min=0, max=0) -> Log 00
940                            // Phys 01 (min=1, max=0) -> Log 10 (q0=1, q1=0)
941                            // Phys 10 (min=0, max=1) -> Log 01 (q0=0, q1=1)
942                            // Phys 11 (min=1, max=1) -> Log 11
943                            (a, c, b, d)
944                        } else {
945                            // q0=max, q1=min
946                            // Phys 00 (min=0, max=0) -> Log 00
947                            // Phys 01 (min=1, max=0) -> Log 01 (q0=0, q1=1)
948                            // Phys 10 (min=0, max=1) -> Log 10 (q0=1, q1=0)
949                            // Phys 11 (min=1, max=1) -> Log 11
950                            (a, b, c, d)
951                        };
952
953                        // Matrix multiplication
954                        let out0 = m00 * in0 + m01 * in1 + m02 * in2 + m03 * in3;
955                        let out1 = m10 * in0 + m11 * in1 + m12 * in2 + m13 * in3;
956                        let out2 = m20 * in0 + m21 * in1 + m22 * in2 + m23 * in3;
957                        let out3 = m30 * in0 + m31 * in1 + m32 * in2 + m33 * in3;
958
959                        // Write results (inverse mapping)
960                        if q0 < q1 {
961                            *c00 = out0;
962                            *c10 = out1; // Map Log 01 to Phys 10 (Log 01: q0=0, q1=1 => min=0, max=1 => Phys 10)
963                            *c01 = out2; // Map Log 10 to Phys 01 (q0=1, q1=0 => min=1, max=0 => Phys 01)
964                            *c11 = out3;
965                        } else {
966                            *c00 = out0;
967                            *c01 = out1;
968                            *c10 = out2;
969                            *c11 = out3;
970                        }
971                    }
972                });
973        };
974        with_maybe_par!(self.num_qubits, self.data, 2 * dist_max, kernel);
975        Ok(())
976    }
977
978    /// Applies an arbitrary n-qubit unitary gate.
979    ///
980    /// General-purpose method for applying any 2^n × 2^n unitary matrix.
981    /// Uses block-iteration with bit-manipulation for efficient index calculation.
982    /// Supports parallel execution for large statevectors.
983    ///
984    /// For standard gates, prefer specialized methods which are more optimized.
985    ///
986    /// # Arguments
987    /// * `qubits` - Slice of qubit indices the gate acts on (logical order)
988    /// * `matrix` - The unitary matrix as a 2^n × 2^n ndarray
989    ///
990    /// # Errors
991    /// Returns an error if:
992    /// - Qubit indices are out of bounds
993    /// - Qubit indices contain duplicates
994    /// - Matrix dimensions don't match 2^n × 2^n
995    ///
996    /// # Example
997    /// ```rust,ignore
998    /// use cqlib_core::qis::Statevector;
999    /// use ndarray::Array2;
1000    /// use num_complex::Complex64;
1001    ///
1002    /// let mut sv = Statevector::new(2);
1003    /// // Create SWAP gate matrix
1004    /// let swap = Array2::from_shape_vec(
1005    ///     (4, 4),
1006    ///     vec![
1007    ///         Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0),
1008    ///         Complex64::new(0.0, 0.0), Complex64::new(0.0, 0.0),
1009    ///         Complex64::new(0.0, 0.0), Complex64::new(0.0, 0.0),
1010    ///         Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0),
1011    ///         Complex64::new(0.0, 0.0), Complex64::new(1.0, 0.0),
1012    ///         Complex64::new(0.0, 0.0), Complex64::new(0.0, 0.0),
1013    ///         Complex64::new(0.0, 0.0), Complex64::new(0.0, 0.0),
1014    ///         Complex64::new(0.0, 0.0), Complex64::new(1.0, 0.0),
1015    ///     ]
1016    /// ).unwrap();
1017    /// sv.apply_unitary_gate(&[0, 1], &swap);
1018    /// ```
1019    pub fn apply_unitary_gate(
1020        &mut self,
1021        qubits: &[usize],
1022        matrix: &ndarray::Array2<Complex64>,
1023    ) -> Result<(), QisError> {
1024        let num_target_qubits = qubits.len();
1025        let gate_dim = 1 << num_target_qubits;
1026
1027        // Validate inputs
1028        if num_target_qubits == 0 || num_target_qubits > self.num_qubits {
1029            return Err(QisError::InvalidParameterValue(format!(
1030                "Invalid number of target qubits: {} (must be 1 to {})",
1031                num_target_qubits, self.num_qubits
1032            )));
1033        }
1034
1035        for (i, &q) in qubits.iter().enumerate() {
1036            self.validate_qubit(q)?;
1037            for &other in &qubits[..i] {
1038                if q == other {
1039                    return Err(QisError::InvalidParameterValue(format!(
1040                        "Duplicate qubit index {} in apply_unitary",
1041                        q
1042                    )));
1043                }
1044            }
1045        }
1046
1047        if matrix.shape() != [gate_dim, gate_dim] {
1048            return Err(QisError::InvalidParameterValue(format!(
1049                "Matrix dimensions {}x{} don't match expected {}x{} for {} qubits",
1050                matrix.nrows(),
1051                matrix.ncols(),
1052                gate_dim,
1053                gate_dim,
1054                num_target_qubits
1055            )));
1056        }
1057
1058        // Precompute gate offset mapping: gate_index -> physical_offset
1059        let mut gate_offsets: Vec<usize> = vec![0; gate_dim];
1060        #[allow(clippy::needless_range_loop)]
1061        for gate_idx in 0..gate_dim {
1062            let mut offset = 0;
1063            for (i, &qubit) in qubits.iter().enumerate() {
1064                // Big-endian mapping: qubits[0] corresponds to the MSB of the gate index,
1065                // qubits[last] corresponds to the LSB. This aligns with standard tensor
1066                // product order where the first qubit in the list is the most significant.
1067                let bit_pos = num_target_qubits - 1 - i;
1068                if (gate_idx >> bit_pos) & 1 == 1 {
1069                    offset |= 1 << qubit;
1070                }
1071            }
1072            gate_offsets[gate_idx] = offset;
1073        }
1074
1075        // Sort qubits for efficient index calculation
1076        let mut sorted_qubits: Vec<usize> = qubits.to_vec();
1077        sorted_qubits.sort_unstable();
1078
1079        // Calculate chunk size based on max qubit
1080        let max_qubit = *sorted_qubits.last().unwrap();
1081        let chunk_size = 1 << (max_qubit + 1);
1082
1083        // Create matrix view for cache efficiency
1084        let matrix_rows: Vec<Vec<Complex64>> = (0..gate_dim)
1085            .map(|r| (0..gate_dim).map(|c| matrix[[r, c]]).collect())
1086            .collect();
1087
1088        // Process chunks
1089        let kernel = |chunk: &mut [Complex64]| {
1090            let mut input_buf = vec![Complex64::default(); gate_dim];
1091            let mut output_buf = vec![Complex64::default(); gate_dim];
1092
1093            // Number of blocks in this chunk
1094            let num_blocks = chunk.len() >> num_target_qubits;
1095
1096            for block_idx in 0..num_blocks {
1097                // Expand compressed index to physical address
1098                let mut base = block_idx;
1099                for &q in &sorted_qubits {
1100                    let mask = (1 << q) - 1;
1101                    let left = (base & !mask) << 1;
1102                    let right = base & mask;
1103                    base = left | right;
1104                }
1105
1106                // Load input amplitudes
1107                for (gate_idx, &offset) in gate_offsets.iter().enumerate() {
1108                    input_buf[gate_idx] = chunk[base + offset];
1109                }
1110
1111                // Apply matrix multiplication
1112                for row in 0..gate_dim {
1113                    let mut sum = Complex64::default();
1114                    for (matrix_value, input_value) in matrix_rows[row].iter().zip(input_buf.iter())
1115                    {
1116                        sum += matrix_value * input_value;
1117                    }
1118                    output_buf[row] = sum;
1119                }
1120
1121                // Store output amplitudes
1122                for (gate_idx, &offset) in gate_offsets.iter().enumerate() {
1123                    chunk[base + offset] = output_buf[gate_idx];
1124                }
1125            }
1126        };
1127
1128        with_maybe_par!(self.num_qubits, self.data, chunk_size, kernel);
1129        Ok(())
1130    }
1131
1132    /// Applies a native Pauli-string rotation in place.
1133    ///
1134    /// The operation is
1135    ///
1136    /// `exp(-i * theta / 2 * P)`
1137    ///
1138    /// where `P` is a Hermitian [`PauliString`]. This fused kernel avoids
1139    /// decomposing a Pauli exponential into basis changes, a CNOT ladder, and
1140    /// an RZ gate. It is intended for high-frequency variational workloads.
1141    ///
1142    /// # Errors
1143    ///
1144    /// Returns an error if the Pauli string has a different number of qubits or
1145    /// carries an imaginary global phase (and is therefore not Hermitian).
1146    pub fn apply_pauli_rotation(
1147        &mut self,
1148        pauli: &PauliString,
1149        theta: f64,
1150    ) -> Result<(), QisError> {
1151        if pauli.num_qubits != self.num_qubits {
1152            return Err(QisError::QubitMismatch {
1153                expected: self.num_qubits,
1154                actual: pauli.num_qubits,
1155            });
1156        }
1157        if matches!(pauli.phase, Phase::I | Phase::MinusI) {
1158            return Err(QisError::NotHermitian);
1159        }
1160        if theta.abs() < 1e-15 {
1161            return Ok(());
1162        }
1163
1164        let x_mask = pauli.x_mask();
1165        let z_mask = pauli.z_mask();
1166        let base_factor = pauli.phase.to_complex() * pauli.y_phase();
1167        let half = theta * 0.5;
1168        let cosine = half.cos();
1169        let minus_i_sine = Complex64::new(0.0, -half.sin());
1170
1171        #[inline(always)]
1172        fn source_phase(index: usize, z_mask: usize, base: Complex64) -> Complex64 {
1173            if (index & z_mask).count_ones() & 1 == 0 {
1174                base
1175            } else {
1176                -base
1177            }
1178        }
1179
1180        if x_mask == 0 {
1181            if self.num_qubits < PARALLEL_THRESHOLD {
1182                self.data
1183                    .iter_mut()
1184                    .enumerate()
1185                    .for_each(|(index, amplitude)| {
1186                        let phase = source_phase(index, z_mask, base_factor);
1187                        *amplitude *= Complex64::new(cosine, 0.0) + minus_i_sine * phase;
1188                    });
1189            } else {
1190                self.data
1191                    .par_iter_mut()
1192                    .enumerate()
1193                    .for_each(|(index, amplitude)| {
1194                        let phase = source_phase(index, z_mask, base_factor);
1195                        *amplitude *= Complex64::new(cosine, 0.0) + minus_i_sine * phase;
1196                    });
1197            }
1198            return Ok(());
1199        }
1200
1201        // Split on the highest flipped bit. Every Pauli pair then contains one
1202        // amplitude in the lower half and one in the upper half of a chunk, so
1203        // disjoint pairs can be updated safely without an auxiliary statevector.
1204        let highest_bit = usize::BITS as usize - 1 - x_mask.leading_zeros() as usize;
1205        let dist = 1usize << highest_bit;
1206        let chunk_size = 2 * dist;
1207        let low_flip_mask = x_mask ^ dist;
1208
1209        let apply_chunk = |chunk_index: usize, chunk: &mut [Complex64]| {
1210            let base_index = chunk_index * chunk_size;
1211            for lower_index in 0..dist {
1212                let upper_index = dist + (lower_index ^ low_flip_mask);
1213                let source_lower = base_index + lower_index;
1214                let source_upper = base_index + upper_index;
1215                let lower_phase = source_phase(source_lower, z_mask, base_factor);
1216                let upper_phase = source_phase(source_upper, z_mask, base_factor);
1217                let lower = chunk[lower_index];
1218                let upper = chunk[upper_index];
1219                chunk[lower_index] = cosine * lower + minus_i_sine * upper_phase * upper;
1220                chunk[upper_index] = cosine * upper + minus_i_sine * lower_phase * lower;
1221            }
1222        };
1223
1224        if self.num_qubits < PARALLEL_THRESHOLD {
1225            self.data
1226                .chunks_mut(chunk_size)
1227                .enumerate()
1228                .for_each(|(chunk_index, chunk)| apply_chunk(chunk_index, chunk));
1229        } else {
1230            self.data
1231                .par_chunks_mut(chunk_size)
1232                .enumerate()
1233                .for_each(|(chunk_index, chunk)| apply_chunk(chunk_index, chunk));
1234        }
1235        Ok(())
1236    }
1237
1238    /// Apply Pauli-X gate: bit flip
1239    /// Matrix: [[0, 1], [1, 0]]
1240    /// Cost: 0 muls, 0 adds, just memory swap.
1241    pub fn apply_x(&mut self, qubit: usize) -> Result<(), QisError> {
1242        self.validate_qubit(qubit)?;
1243        let dist = 1 << qubit;
1244        let kernel = |chunk: &mut [Complex64]| {
1245            let (lower, upper) = chunk.split_at_mut(dist);
1246            lower.iter_mut().zip(upper.iter_mut()).for_each(|(a, b)| {
1247                std::mem::swap(a, b);
1248            });
1249        };
1250        with_maybe_par!(self.num_qubits, self.data, 2 * dist, kernel);
1251        Ok(())
1252    }
1253
1254    /// Apply Pauli-Y gate
1255    /// Matrix: [[0, -i], [i, 0]]
1256    /// Cost: Manual component swapping (faster than complex mul).
1257    pub fn apply_y(&mut self, qubit: usize) -> Result<(), QisError> {
1258        self.validate_qubit(qubit)?;
1259        let dist = 1 << qubit;
1260        let kernel = |chunk: &mut [Complex64]| {
1261            let (lower, upper) = chunk.split_at_mut(dist);
1262            lower
1263                .iter_mut()
1264                .zip(upper.iter_mut())
1265                .for_each(|(alpha, beta)| {
1266                    let a = *alpha;
1267                    let b = *beta;
1268                    *alpha = Complex64::new(b.im, -b.re);
1269                    *beta = Complex64::new(-a.im, a.re);
1270                });
1271        };
1272        with_maybe_par!(self.num_qubits, self.data, 2 * dist, kernel);
1273        Ok(())
1274    }
1275
1276    /// Apply Pauli-Z gate: phase flip
1277    /// Matrix: [[1, 0], [0, -1]]
1278    /// Cost: Only processes half the array. 1 negation.
1279    pub fn apply_z(&mut self, qubit: usize) -> Result<(), QisError> {
1280        self.validate_qubit(qubit)?;
1281        let dist = 1 << qubit;
1282        let kernel = |chunk: &mut [Complex64]| {
1283            let (_lower, upper) = chunk.split_at_mut(dist);
1284            upper.iter_mut().for_each(|val| {
1285                *val = -*val;
1286            });
1287        };
1288        with_maybe_par!(self.num_qubits, self.data, 2 * dist, kernel);
1289        Ok(())
1290    }
1291
1292    /// Apply Hadamard gate
1293    /// Matrix: 1/sqrt(2) * [[1, 1], [1, -1]]
1294    /// Creates superposition: H|0⟩ = (|0⟩ + |1⟩)/√2, H|1⟩ = (|0⟩ - |1⟩)/√2
1295    pub fn apply_h(&mut self, qubit: usize) -> Result<(), QisError> {
1296        self.validate_qubit(qubit)?;
1297        let dist = 1 << qubit;
1298        let k = FRAC_1_SQRT_2;
1299
1300        let kernel = |chunk: &mut [Complex64]| {
1301            let (lower, upper) = chunk.split_at_mut(dist);
1302            lower
1303                .iter_mut()
1304                .zip(upper.iter_mut())
1305                .for_each(|(alpha, beta)| {
1306                    let a = *alpha;
1307                    let b = *beta;
1308                    *alpha = k * (a + b);
1309                    *beta = k * (a - b);
1310                });
1311        };
1312        with_maybe_par!(self.num_qubits, self.data, 2 * dist, kernel);
1313        Ok(())
1314    }
1315
1316    /// Apply U gate (generic single-qubit rotation)
1317    /// Matrix: [[cos(θ/2), -e^(iλ)sin(θ/2)], [e^(iφ)sin(θ/2), e^(i(φ+λ))cos(θ/2)]]
1318    ///
1319    /// This is the most general single-qubit unitary (up to global phase).
1320    /// Parameters:
1321    ///   theta: rotation angle
1322    ///   phi: phase of the rotation axis
1323    ///   lambda: second phase parameter
1324    pub fn apply_u(
1325        &mut self,
1326        qubit: usize,
1327        theta: f64,
1328        phi: f64,
1329        lambda: f64,
1330    ) -> Result<(), QisError> {
1331        self.validate_qubit(qubit)?;
1332
1333        // Fast path: if theta is 0, U simplifies to a Z-rotation-like operation
1334        if theta.abs() < 1e-15 {
1335            // U(0, φ, λ) = e^(i(φ+λ)/2) * [[1, 0], [0, e^(i(λ+φ))]]
1336            // This is essentially a phase gate plus global phase
1337            return self.apply_phase(qubit, lambda + phi);
1338        }
1339
1340        let dist = 1 << qubit;
1341
1342        let half_theta = theta * 0.5;
1343        let c = half_theta.cos();
1344        let s = half_theta.sin();
1345
1346        // Compute phase factors
1347        let phase_phi = Complex64::from_polar(1.0, phi); // e^(iφ)
1348        let phase_lambda = Complex64::from_polar(1.0, lambda); // e^(iλ)
1349        let phase_phi_lambda = Complex64::from_polar(1.0, phi + lambda); // e^(i(φ+λ))
1350
1351        // Matrix elements:
1352        // u00 = cos(θ/2)
1353        // u01 = -e^(iλ) * sin(θ/2)
1354        // u10 = e^(iφ) * sin(θ/2)
1355        // u11 = e^(i(φ+λ)) * cos(θ/2)
1356        let u00 = Complex64::new(c, 0.0);
1357        let u01 = -phase_lambda * s;
1358        let u10 = phase_phi * s;
1359        let u11 = phase_phi_lambda * c;
1360
1361        let kernel = |chunk: &mut [Complex64]| {
1362            let (lower, upper) = chunk.split_at_mut(dist);
1363            lower
1364                .iter_mut()
1365                .zip(upper.iter_mut())
1366                .for_each(|(alpha, beta)| {
1367                    let a = *alpha;
1368                    let b = *beta;
1369                    *alpha = u00 * a + u01 * b;
1370                    *beta = u10 * a + u11 * b;
1371                });
1372        };
1373        with_maybe_par!(self.num_qubits, self.data, 2 * dist, kernel);
1374        Ok(())
1375    }
1376
1377    /// Apply S gate (Phase gate, Z^1/2)
1378    /// Matrix: [[1, 0], [0, i]]
1379    pub fn apply_s(&mut self, qubit: usize) -> Result<(), QisError> {
1380        self.validate_qubit(qubit)?;
1381        let dist = 1 << qubit;
1382        let kernel = |chunk: &mut [Complex64]| {
1383            let (_lower, upper) = chunk.split_at_mut(dist);
1384            upper.iter_mut().for_each(|val| {
1385                *val = Complex64::new(-val.im, val.re);
1386            });
1387        };
1388        with_maybe_par!(self.num_qubits, self.data, 2 * dist, kernel);
1389        Ok(())
1390    }
1391
1392    /// Apply S† (S-dagger) gate
1393    /// Matrix: [[1, 0], [0, -i]]
1394    pub fn apply_sdg(&mut self, qubit: usize) -> Result<(), QisError> {
1395        self.validate_qubit(qubit)?;
1396        let dist = 1 << qubit;
1397        let kernel = |chunk: &mut [Complex64]| {
1398            let (_lower, upper) = chunk.split_at_mut(dist);
1399            upper.iter_mut().for_each(|val| {
1400                *val = Complex64::new(val.im, -val.re);
1401            });
1402        };
1403        with_maybe_par!(self.num_qubits, self.data, 2 * dist, kernel);
1404        Ok(())
1405    }
1406
1407    /// Apply T gate (Z^1/4)
1408    /// Matrix: [[1, 0], [0, e^(i*pi/4)]]
1409    pub fn apply_t(&mut self, qubit: usize) -> Result<(), QisError> {
1410        self.validate_qubit(qubit)?;
1411        let dist = 1 << qubit;
1412        let phase = Complex64::new(FRAC_1_SQRT_2, FRAC_1_SQRT_2);
1413        let kernel = |chunk: &mut [Complex64]| {
1414            let (_lower, upper) = chunk.split_at_mut(dist);
1415            upper.iter_mut().for_each(|val| {
1416                *val *= phase;
1417            });
1418        };
1419        with_maybe_par!(self.num_qubits, self.data, 2 * dist, kernel);
1420        Ok(())
1421    }
1422
1423    /// Apply T† (T-dagger) gate
1424    /// Matrix: [[1, 0], [0, e^(-i*pi/4)]]
1425    pub fn apply_tdg(&mut self, qubit: usize) -> Result<(), QisError> {
1426        self.validate_qubit(qubit)?;
1427        let dist = 1 << qubit;
1428        let phase = Complex64::new(FRAC_1_SQRT_2, -FRAC_1_SQRT_2);
1429        let kernel = |chunk: &mut [Complex64]| {
1430            let (_lower, upper) = chunk.split_at_mut(dist);
1431            upper.iter_mut().for_each(|val| {
1432                *val *= phase;
1433            });
1434        };
1435        with_maybe_par!(self.num_qubits, self.data, 2 * dist, kernel);
1436        Ok(())
1437    }
1438
1439    /// Apply P (Phase) gate
1440    /// Matrix: [[1, 0], [0, e^(i*theta)]]
1441    pub fn apply_phase(&mut self, qubit: usize, theta: f64) -> Result<(), QisError> {
1442        self.validate_qubit(qubit)?;
1443        if theta.abs() < 1e-15 {
1444            return Ok(());
1445        }
1446
1447        let dist = 1 << qubit;
1448        let phase = Complex64::from_polar(1.0, theta);
1449        let kernel = |chunk: &mut [Complex64]| {
1450            let (_lower, upper) = chunk.split_at_mut(dist);
1451            upper.iter_mut().for_each(|val| {
1452                *val *= phase;
1453            });
1454        };
1455        with_maybe_par!(self.num_qubits, self.data, 2 * dist, kernel);
1456        Ok(())
1457    }
1458
1459    /// Apply Rx(theta) gate
1460    /// Matrix: [[cos(t/2), -i*sin(t/2)], [-i*sin(t/2), cos(t/2)]]
1461    pub fn apply_rx(&mut self, qubit: usize, theta: f64) -> Result<(), QisError> {
1462        self.validate_qubit(qubit)?;
1463        if theta.abs() < 1e-15 {
1464            return Ok(());
1465        }
1466
1467        let dist = 1 << qubit;
1468        let half_theta = theta * 0.5;
1469        let c = half_theta.cos();
1470        let s = half_theta.sin();
1471
1472        let kernel = |chunk: &mut [Complex64]| {
1473            let (lower, upper) = chunk.split_at_mut(dist);
1474            lower
1475                .iter_mut()
1476                .zip(upper.iter_mut())
1477                .for_each(|(alpha, beta)| {
1478                    let a = *alpha;
1479                    let b = *beta;
1480                    let bs_im = b.im * s;
1481                    let bs_re = b.re * s;
1482                    let as_im = a.im * s;
1483                    let as_re = a.re * s;
1484
1485                    *alpha = Complex64::new(
1486                        a.re * c + bs_im, // Real part
1487                        a.im * c - bs_re, // Imag part
1488                    );
1489
1490                    *beta = Complex64::new(
1491                        b.re * c + as_im, // Real part
1492                        b.im * c - as_re, // Imag part
1493                    );
1494                });
1495        };
1496        with_maybe_par!(self.num_qubits, self.data, 2 * dist, kernel);
1497        Ok(())
1498    }
1499
1500    /// Apply Ry(theta) gate
1501    /// Matrix: [[cos(t/2), -sin(t/2)], [sin(t/2), cos(t/2)]]
1502    pub fn apply_ry(&mut self, qubit: usize, theta: f64) -> Result<(), QisError> {
1503        self.validate_qubit(qubit)?;
1504        if theta.abs() < 1e-15 {
1505            return Ok(());
1506        }
1507
1508        let dist = 1 << qubit;
1509
1510        let half_theta = theta * 0.5;
1511        let c = half_theta.cos();
1512        let s = half_theta.sin();
1513
1514        let kernel = |chunk: &mut [Complex64]| {
1515            let (lower, upper) = chunk.split_at_mut(dist);
1516            lower
1517                .iter_mut()
1518                .zip(upper.iter_mut())
1519                .for_each(|(alpha, beta)| {
1520                    let a = *alpha;
1521                    let b = *beta;
1522                    *alpha = a * c - b * s;
1523                    *beta = a * s + b * c;
1524                });
1525        };
1526        with_maybe_par!(self.num_qubits, self.data, 2 * dist, kernel);
1527        Ok(())
1528    }
1529
1530    /// Apply Rz(theta) gate
1531    /// Matrix: [[e^(-i*theta/2), 0], [0, e^(i*theta/2)]]
1532    pub fn apply_rz(&mut self, qubit: usize, theta: f64) -> Result<(), QisError> {
1533        self.validate_qubit(qubit)?;
1534        if theta.abs() < 1e-15 {
1535            return Ok(());
1536        }
1537
1538        let dist = 1 << qubit;
1539
1540        // Precompute phase factors
1541        let half_theta = theta * 0.5;
1542        let phase_0 = Complex64::from_polar(1.0, -half_theta); // e^(-i*t/2)
1543        let phase_1 = Complex64::from_polar(1.0, half_theta); // e^(i*t/2)
1544
1545        let kernel = |chunk: &mut [Complex64]| {
1546            let (lower, upper) = chunk.split_at_mut(dist);
1547            lower.iter_mut().zip(upper.iter_mut()).for_each(|(a, b)| {
1548                *a *= phase_0;
1549                *b *= phase_1;
1550            });
1551        };
1552        with_maybe_par!(self.num_qubits, self.data, 2 * dist, kernel);
1553        Ok(())
1554    }
1555
1556    /// Applies X2P gate: Rx(π/2), also known as √X.
1557    ///
1558    /// Matrix: 1/√2 * [[1, -i], [-i, 1]]
1559    ///
1560    /// # Optimization
1561    /// Uses constant factors only, no trigonometric functions.
1562    pub fn apply_x2p(&mut self, qubit: usize) -> Result<(), QisError> {
1563        self.validate_qubit(qubit)?;
1564        let dist = 1 << qubit;
1565        let k = FRAC_1_SQRT_2;
1566        let kernel = |chunk: &mut [Complex64]| {
1567            let (lower, upper) = chunk.split_at_mut(dist);
1568            lower
1569                .iter_mut()
1570                .zip(upper.iter_mut())
1571                .for_each(|(alpha, beta)| {
1572                    let a = *alpha;
1573                    let b = *beta;
1574                    *alpha = Complex64::new(k * (a.re + b.im), k * (a.im - b.re));
1575                    *beta = Complex64::new(k * (b.re + a.im), k * (b.im - a.re));
1576                });
1577        };
1578        with_maybe_par!(self.num_qubits, self.data, 2 * dist, kernel);
1579        Ok(())
1580    }
1581
1582    /// Applies X2M gate: Rx(-π/2), inverse of √X.
1583    ///
1584    /// Matrix: 1/√2 * [[1, i], [i, 1]]
1585    pub fn apply_x2m(&mut self, qubit: usize) -> Result<(), QisError> {
1586        self.validate_qubit(qubit)?;
1587        let dist = 1 << qubit;
1588        let k = FRAC_1_SQRT_2;
1589        let kernel = |chunk: &mut [Complex64]| {
1590            let (lower, upper) = chunk.split_at_mut(dist);
1591            lower
1592                .iter_mut()
1593                .zip(upper.iter_mut())
1594                .for_each(|(alpha, beta)| {
1595                    let a = *alpha;
1596                    let b = *beta;
1597                    *alpha = Complex64::new(k * (a.re - b.im), k * (a.im + b.re));
1598                    *beta = Complex64::new(k * (b.re - a.im), k * (b.im + a.re));
1599                });
1600        };
1601        with_maybe_par!(self.num_qubits, self.data, 2 * dist, kernel);
1602        Ok(())
1603    }
1604
1605    /// Applies Y2P gate: Ry(π/2).
1606    ///
1607    /// Matrix: 1/√2 * [[1, -1], [1, 1]]
1608    pub fn apply_y2p(&mut self, qubit: usize) -> Result<(), QisError> {
1609        self.validate_qubit(qubit)?;
1610        let dist = 1 << qubit;
1611        let k = FRAC_1_SQRT_2;
1612        let kernel = |chunk: &mut [Complex64]| {
1613            let (lower, upper) = chunk.split_at_mut(dist);
1614            lower
1615                .iter_mut()
1616                .zip(upper.iter_mut())
1617                .for_each(|(alpha, beta)| {
1618                    let a = *alpha;
1619                    let b = *beta;
1620                    *alpha = k * (a - b);
1621                    *beta = k * (a + b);
1622                });
1623        };
1624        with_maybe_par!(self.num_qubits, self.data, 2 * dist, kernel);
1625        Ok(())
1626    }
1627
1628    /// Applies Y2M gate: Ry(-π/2).
1629    ///
1630    /// Matrix: 1/√2 * [[1, 1], [-1, 1]]
1631    pub fn apply_y2m(&mut self, qubit: usize) -> Result<(), QisError> {
1632        self.validate_qubit(qubit)?;
1633        let dist = 1 << qubit;
1634        let k = FRAC_1_SQRT_2;
1635        let kernel = |chunk: &mut [Complex64]| {
1636            let (lower, upper) = chunk.split_at_mut(dist);
1637            lower
1638                .iter_mut()
1639                .zip(upper.iter_mut())
1640                .for_each(|(alpha, beta)| {
1641                    let a = *alpha;
1642                    let b = *beta;
1643                    *alpha = k * (a + b);
1644                    *beta = k * (b - a);
1645                });
1646        };
1647        with_maybe_par!(self.num_qubits, self.data, 2 * dist, kernel);
1648        Ok(())
1649    }
1650
1651    /// Applies XY2P gate: Rz(θ - π/2) Ry(π/2) Rz(π/2 - θ).
1652    ///
1653    /// Matrix: 1/√2 * [[1, -i·e^(-iθ)], [-i·e^(iθ), 1]]
1654    ///
1655    /// # Arguments
1656    /// * `qubit` - Target qubit index
1657    /// * `theta` - Rotation angle
1658    pub fn apply_xy2p(&mut self, qubit: usize, theta: f64) -> Result<(), QisError> {
1659        self.validate_qubit(qubit)?;
1660        let dist = 1 << qubit;
1661        let k = FRAC_1_SQRT_2;
1662
1663        // Precompute coefficients, fuse k to reduce multiplications in loop
1664        // e^(i*theta)
1665        let phase = Complex64::from_polar(1.0, theta);
1666        let phase_conj = phase.conj(); // e^(-i*theta)
1667        let neg_i = Complex64::new(0.0, -1.0);
1668
1669        // off_diag_01 = k * (-i * e^(-i*theta))
1670        let coef_01 = neg_i * phase_conj * k;
1671
1672        // off_diag_10 = k * (-i * e^(i*theta))
1673        let coef_10 = neg_i * phase * k;
1674
1675        // Explicitly convert to Complex64 for multiplication with complex numbers
1676        let k_complex = Complex64::new(k, 0.0);
1677        let kernel = |chunk: &mut [Complex64]| {
1678            let (lower, upper) = chunk.split_at_mut(dist);
1679            lower
1680                .iter_mut()
1681                .zip(upper.iter_mut())
1682                .for_each(|(alpha, beta)| {
1683                    let a = *alpha;
1684                    let b = *beta;
1685                    *alpha = k_complex * a + coef_01 * b;
1686                    *beta = coef_10 * a + k_complex * b;
1687                });
1688        };
1689        with_maybe_par!(self.num_qubits, self.data, 2 * dist, kernel);
1690        Ok(())
1691    }
1692
1693    /// Applies XY2M gate: Rz(-θ + π/2) Ry(-π/2) Rz(-π/2 + θ).
1694    ///
1695    /// Matrix: 1/√2 * [[1, i·e^(-iθ)], [i·e^(iθ), 1]]
1696    ///
1697    /// # Arguments
1698    /// * `qubit` - Target qubit index
1699    /// * `theta` - Rotation angle
1700    pub fn apply_xy2m(&mut self, qubit: usize, theta: f64) -> Result<(), QisError> {
1701        self.validate_qubit(qubit)?;
1702        let dist = 1 << qubit;
1703        let k = FRAC_1_SQRT_2;
1704        let phase = Complex64::from_polar(1.0, theta);
1705        let phase_conj = phase.conj();
1706        let pos_i = Complex64::new(0.0, 1.0);
1707        let coef_01 = pos_i * phase_conj * k;
1708        let coef_10 = pos_i * phase * k;
1709        let k_complex = Complex64::new(k, 0.0);
1710        let kernel = |chunk: &mut [Complex64]| {
1711            let (lower, upper) = chunk.split_at_mut(dist);
1712            lower
1713                .iter_mut()
1714                .zip(upper.iter_mut())
1715                .for_each(|(alpha, beta)| {
1716                    let a = *alpha;
1717                    let b = *beta;
1718                    *alpha = k_complex * a + coef_01 * b;
1719                    *beta = coef_10 * a + k_complex * b;
1720                });
1721        };
1722        with_maybe_par!(self.num_qubits, self.data, 2 * dist, kernel);
1723        Ok(())
1724    }
1725
1726    /// Applies XY gate.
1727    ///
1728    /// Matrix: [[0, -i·e^(-iθ)], [-i·e^(iθ), 0]]
1729    ///
1730    /// This is a single-qubit gate distinct from XY2P/XY2M.
1731    ///
1732    /// # Arguments
1733    /// * `qubit` - Target qubit index
1734    /// * `theta` - Rotation angle
1735    pub fn apply_xy(&mut self, qubit: usize, theta: f64) -> Result<(), QisError> {
1736        self.validate_qubit(qubit)?;
1737
1738        let dist = 1 << qubit;
1739        let phase = Complex64::from_polar(1.0, theta);
1740        let phase_conj = phase.conj();
1741        let neg_i = Complex64::new(0.0, -1.0);
1742
1743        // u01 = -i * e^(-i*theta)
1744        let u01 = neg_i * phase_conj;
1745        // u10 = -i * e^(i*theta)
1746        let u10 = neg_i * phase;
1747
1748        let kernel = |chunk: &mut [Complex64]| {
1749            let (lower, upper) = chunk.split_at_mut(dist);
1750            lower
1751                .iter_mut()
1752                .zip(upper.iter_mut())
1753                .for_each(|(alpha, beta)| {
1754                    let a = *alpha;
1755                    let b = *beta;
1756                    *alpha = u01 * b;
1757                    *beta = u10 * a;
1758                });
1759        };
1760        with_maybe_par!(self.num_qubits, self.data, 2 * dist, kernel);
1761        Ok(())
1762    }
1763
1764    /// Apply general rotation R(theta, phi).
1765    ///
1766    /// Rotates around axis cos(phi)X + sin(phi)Y by angle theta.
1767    /// Matrix: [[cos(t/2), -i*e^(-i*phi)*sin(t/2)], [-i*e^(i*phi)*sin(t/2), cos(t/2)]]
1768    ///
1769    /// # Arguments
1770    /// * `qubit` - Target qubit index
1771    /// * `theta` - Rotation angle
1772    /// * `phi` - Rotation axis angle in XY plane
1773    pub fn apply_rxy(&mut self, qubit: usize, theta: f64, phi: f64) -> Result<(), QisError> {
1774        self.validate_qubit(qubit)?;
1775
1776        // Fast path: treat as Identity if theta is negligible
1777        if theta.abs() < 1e-15 {
1778            return Ok(());
1779        }
1780
1781        // Precompute all coefficients
1782        let dist = 1 << qubit;
1783        let half_theta = theta * 0.5;
1784        let c = half_theta.cos();
1785        let s = half_theta.sin();
1786
1787        // Precompute phi trig functions
1788        let (sin_phi, cos_phi) = phi.sin_cos();
1789
1790        // Construct off-diagonal coefficients
1791        // u01 = -i * (cos_phi - i*sin_phi) * s
1792        //     = (-i*cos_phi - sin_phi) * s
1793        //     = s * (-sin_phi - i*cos_phi)
1794        let u01 = Complex64::new(-s * sin_phi, -s * cos_phi);
1795
1796        // u10 = -i * (cos_phi + i*sin_phi) * s
1797        //     = (-i*cos_phi + sin_phi) * s
1798        //     = s * (sin_phi - i*cos_phi)
1799        let u10 = Complex64::new(s * sin_phi, -s * cos_phi);
1800
1801        // Apply (serial or parallel based on qubit count)
1802        let kernel = |chunk: &mut [Complex64]| {
1803            let (lower, upper) = chunk.split_at_mut(dist);
1804            lower
1805                .iter_mut()
1806                .zip(upper.iter_mut())
1807                .for_each(|(alpha, beta)| {
1808                    let a = *alpha;
1809                    let b = *beta;
1810
1811                    // Core computation:
1812                    // alpha' = c * a + u01 * b
1813                    // beta'  = u10 * a + c * b
1814
1815                    // num_complex supports Complex * f64, faster than Complex(c, 0)
1816                    *alpha = a * c + b * u01;
1817                    *beta = a * u10 + b * c;
1818                });
1819        };
1820        with_maybe_par!(self.num_qubits, self.data, 2 * dist, kernel);
1821        Ok(())
1822    }
1823
1824    /// Applies SWAP gate to exchange two qubits.
1825    ///
1826    /// Matrix: [[1,0,0,0], [0,0,1,0], [0,1,0,0], [0,0,0,1]]
1827    ///
1828    /// # Arguments
1829    /// * `q0` - First qubit index
1830    /// * `q1` - Second qubit index
1831    ///
1832    /// # Errors
1833    /// Returns [`QisError::IndexOutOfBounds`] if either qubit is out of bounds,
1834    /// or [`QisError::QubitMismatch`] if `q0 == q1`.
1835    ///
1836    /// # Optimization
1837    /// Uses memory swap without floating-point operations.
1838    pub fn apply_swap(&mut self, q0: usize, q1: usize) -> Result<(), QisError> {
1839        self.validate_two_qubits(q0, q1)?;
1840
1841        // 1. Determine physical memory strides
1842        let (q_min, q_max) = if q0 < q1 { (q0, q1) } else { (q1, q0) };
1843        let dist_max = 1 << q_max;
1844        let dist_min = 1 << q_min;
1845
1846        // 2. Choose serial or parallel based on qubit count
1847        let kernel = |chunk: &mut [Complex64]| {
1848            let (part0, part1) = chunk.split_at_mut(dist_max);
1849
1850            part0
1851                .chunks_exact_mut(2 * dist_min)
1852                .zip(part1.chunks_exact_mut(2 * dist_min))
1853                .for_each(|(sub_chunk0, sub_chunk1)| {
1854                    let (_, v01_slice) = sub_chunk0.split_at_mut(dist_min);
1855                    let (v10_slice, _) = sub_chunk1.split_at_mut(dist_min);
1856
1857                    // v01_slice: physical (max=0, min=1) -> logical |01> or |10>
1858                    // v10_slice: physical (max=1, min=0) -> logical |10> or |01>
1859                    // SWAP always exchanges these two memory blocks
1860                    v01_slice
1861                        .iter_mut()
1862                        .zip(v10_slice.iter_mut())
1863                        .for_each(|(a, b)| {
1864                            // Direct memory swap, no floating-point overhead
1865                            std::mem::swap(a, b);
1866                        });
1867                });
1868        };
1869        with_maybe_par!(self.num_qubits, self.data, 2 * dist_max, kernel);
1870        Ok(())
1871    }
1872
1873    /// Applies CNOT (CX) gate: controlled-X operation.
1874    ///
1875    /// Flips the target qubit when the control qubit is |1⟩.
1876    /// Matrix: [[1,0,0,0], [0,1,0,0], [0,0,0,1], [0,0,1,0]]
1877    ///
1878    /// # Arguments
1879    /// * `control` - Control qubit index
1880    /// * `target` - Target qubit index
1881    ///
1882    /// # Errors
1883    /// Returns [`QisError::IndexOutOfBounds`] if either qubit is out of bounds,
1884    /// or [`QisError::QubitMismatch`] if `control == target`.
1885    pub fn apply_cx(&mut self, control: usize, target: usize) -> Result<(), QisError> {
1886        self.validate_two_qubits(control, target)?;
1887
1888        let (q_min, q_max) = if control < target {
1889            (control, target)
1890        } else {
1891            (target, control)
1892        };
1893        let dist_max = 1 << q_max;
1894        let dist_min = 1 << q_min;
1895
1896        let kernel = |chunk: &mut [Complex64]| {
1897            let (part0, part1) = chunk.split_at_mut(dist_max);
1898
1899            part0
1900                .chunks_exact_mut(2 * dist_min)
1901                .zip(part1.chunks_exact_mut(2 * dist_min))
1902                .for_each(|(sub_chunk0, sub_chunk1)| {
1903                    // Get references to four blocks; which to operate on depends on control/target
1904                    let (_v00, v01) = sub_chunk0.split_at_mut(dist_min);
1905                    let (v10, v11) = sub_chunk1.split_at_mut(dist_min);
1906
1907                    if control < target {
1908                        // Case A: Control is low (q_min), Target is high (q_max)
1909                        // Control=1 physical positions:
1910                        //   1. max=0, min=1 (v01) -> Control=1, Target=0
1911                        //   2. max=1, min=1 (v11) -> Control=1, Target=1
1912                        // Action: swap v01 and v11 (flip Target)
1913                        v01.iter_mut()
1914                            .zip(v11.iter_mut())
1915                            .for_each(|(a, b)| std::mem::swap(a, b));
1916                    } else {
1917                        // Case B: Control is high (q_max), Target is low (q_min)
1918                        // Control=1 physical positions:
1919                        //   1. max=1, min=0 (v10) -> Control=1, Target=0
1920                        //   2. max=1, min=1 (v11) -> Control=1, Target=1
1921                        // Action: swap v10 and v11 (flip Target)
1922                        v10.iter_mut()
1923                            .zip(v11.iter_mut())
1924                            .for_each(|(a, b)| std::mem::swap(a, b));
1925                    }
1926                });
1927        };
1928        with_maybe_par!(self.num_qubits, self.data, 2 * dist_max, kernel);
1929        Ok(())
1930    }
1931
1932    /// Applies controlled-Y (CY) gate.
1933    ///
1934    /// Applies Y gate to target when control qubit is |1⟩.
1935    /// Y matrix: [[0, -i], [i, 0]]
1936    ///
1937    /// # Arguments
1938    /// * `control` - Control qubit index
1939    /// * `target` - Target qubit index
1940    ///
1941    /// # Errors
1942    /// Returns [`QisError::IndexOutOfBounds`] if either qubit is out of bounds,
1943    /// or [`QisError::QubitMismatch`] if `control == target`.
1944    pub fn apply_cy(&mut self, control: usize, target: usize) -> Result<(), QisError> {
1945        self.validate_two_qubits(control, target)?;
1946
1947        // 1. Determine physical memory strides
1948        let (q_min, q_max) = if control < target {
1949            (control, target)
1950        } else {
1951            (target, control)
1952        };
1953        let dist_max = 1 << q_max;
1954        let dist_min = 1 << q_min;
1955
1956        // 2. Choose serial or parallel based on qubit count
1957        let kernel = |chunk: &mut [Complex64]| {
1958            let (part0, part1) = chunk.split_at_mut(dist_max);
1959
1960            part0
1961                .chunks_exact_mut(2 * dist_min)
1962                .zip(part1.chunks_exact_mut(2 * dist_min))
1963                .for_each(|(sub_chunk0, sub_chunk1)| {
1964                    // Locate memory blocks where control=1
1965                    // block_t0: amplitude where Control=1, Target=0
1966                    // block_t1: amplitude where Control=1, Target=1
1967
1968                    let (block_t0, block_t1) = if control < target {
1969                        // Case A: Control is low (min), Target is high (max)
1970                        // part0: q_max=0 (Target=0), part1: q_max=1 (Target=1)
1971                        // Need Control=1 states (q_min=1)
1972                        // |10⟩ (q_min=1, q_max=0) and |11⟩ (q_min=1, q_max=1)
1973                        // Corresponds to sub_chunk0[dist_min..] and sub_chunk1[dist_min..]
1974                        let (_, c1_t0) = sub_chunk0.split_at_mut(dist_min);
1975                        let (_, c1_t1) = sub_chunk1.split_at_mut(dist_min);
1976                        (c1_t0, c1_t1)
1977                    } else {
1978                        // Case B: Control is high (max), Target is low (min)
1979                        // part0 is Control=0 (skip), part1 is Control=1
1980                        // Within part1, lower half is Target=0, upper half is Target=1
1981                        let (t0, t1) = sub_chunk1.split_at_mut(dist_min);
1982                        (t0, t1)
1983                    };
1984
1985                    // 3. Apply Y gate transformation
1986                    block_t0
1987                        .iter_mut()
1988                        .zip(block_t1.iter_mut())
1989                        .for_each(|(alpha, beta)| {
1990                            let a = *alpha; // Target=0
1991                            let b = *beta; // Target=1
1992
1993                            // Y matrix: [[0, -i], [i, 0]]
1994                            // new_a = -i * b
1995                            // new_b =  i * a
1996
1997                            // -i * (x + it) = y - ix
1998                            *alpha = Complex64::new(b.im, -b.re);
1999
2000                            // i * (x + it) = -y + ix
2001                            *beta = Complex64::new(-a.im, a.re);
2002                        });
2003                });
2004        };
2005        with_maybe_par!(self.num_qubits, self.data, 2 * dist_max, kernel);
2006        Ok(())
2007    }
2008
2009    /// Applies controlled-Z (CZ) gate.
2010    ///
2011    /// Applies phase flip (-1) to |11⟩ component.
2012    /// Leaves |00⟩, |01⟩, |10⟩ unchanged.
2013    ///
2014    /// # Arguments
2015    /// * `q0` - First qubit index (acts as control)
2016    /// * `q1` - Second qubit index (acts as control)
2017    ///
2018    /// # Errors
2019    /// Returns [`QisError::IndexOutOfBounds`] if either qubit is out of bounds,
2020    /// or [`QisError::QubitMismatch`] if `q0 == q1`.
2021    pub fn apply_cz(&mut self, q0: usize, q1: usize) -> Result<(), QisError> {
2022        self.validate_two_qubits(q0, q1)?;
2023
2024        let (q_min, q_max) = if q0 < q1 { (q0, q1) } else { (q1, q0) };
2025        let dist_max = 1 << q_max;
2026        let dist_min = 1 << q_min;
2027
2028        let kernel = |chunk: &mut [Complex64]| {
2029            let (_, part1) = chunk.split_at_mut(dist_max); // Only care about max=1 part
2030
2031            part1.chunks_exact_mut(2 * dist_min).for_each(|sub_chunk1| {
2032                let (_, v11) = sub_chunk1.split_at_mut(dist_min); // Only care about min=1 part
2033
2034                // v11 now corresponds to max=1, min=1, i.e., |11⟩ state
2035                // Apply sign flip only to this part
2036                for val in v11.iter_mut() {
2037                    *val = -*val;
2038                }
2039            });
2040        };
2041        with_maybe_par!(self.num_qubits, self.data, 2 * dist_max, kernel);
2042        Ok(())
2043    }
2044
2045    /// Apply Toffoli gate (CCX - Controlled-Controlled-X)
2046    /// Two control qubits and one target qubit.
2047    /// Flips the target if and only if both controls are |1⟩.
2048    ///
2049    /// # Arguments
2050    /// * `c0` - First control qubit index
2051    /// * `c1` - Second control qubit index
2052    /// * `target` - Target qubit index
2053    pub fn apply_ccx(&mut self, c0: usize, c1: usize, target: usize) -> Result<(), QisError> {
2054        self.validate_qubits(&[c0, c1, target])?;
2055
2056        // 1. Sort: determine physical min, mid, max positions and their roles
2057        // kind: 0 = Control, 1 = Target
2058        let mut qubits = [(c0, 0), (c1, 0), (target, 1)];
2059        qubits.sort_by_key(|(q, _)| *q);
2060
2061        let (q0, k0) = qubits[0]; // Min position
2062        let (q1, k1) = qubits[1]; // Mid position
2063        let (q2, k2) = qubits[2]; // Max position
2064
2065        let d0 = 1 << q0;
2066        let d1 = 1 << q1;
2067        let d2 = 1 << q2;
2068
2069        match (k0, k1, k2) {
2070            // Case A: Target is at max position -> (Control, Control, Target)
2071            (0, 0, 1) => {
2072                let kernel = |chunk: &mut [Complex64]| {
2073                    let (part_t0, part_t1) = chunk.split_at_mut(d2);
2074
2075                    // Iterate over Mid layer
2076                    part_t0
2077                        .chunks_exact_mut(2 * d1)
2078                        .zip(part_t1.chunks_exact_mut(2 * d1))
2079                        .for_each(|(sub_t0, sub_t1)| {
2080                            // Take Mid=1 part (control is active)
2081                            let (_, high_m_t0) = sub_t0.split_at_mut(d1);
2082                            let (_, high_m_t1) = sub_t1.split_at_mut(d1);
2083
2084                            // Iterate over Min layer (using chunks to handle non-contiguous gaps)
2085                            high_m_t0
2086                                .chunks_exact_mut(2 * d0)
2087                                .zip(high_m_t1.chunks_exact_mut(2 * d0))
2088                                .for_each(|(leaf_t0, leaf_t1)| {
2089                                    // Take Min=1 part (control is active)
2090                                    let (_, valid_t0) = leaf_t0.split_at_mut(d0);
2091                                    let (_, valid_t1) = leaf_t1.split_at_mut(d0);
2092
2093                                    // Now Min=1, Mid=1, execute Target flip
2094                                    valid_t0
2095                                        .iter_mut()
2096                                        .zip(valid_t1.iter_mut())
2097                                        .for_each(|(a, b)| std::mem::swap(a, b));
2098                                });
2099                        });
2100                };
2101                with_maybe_par!(self.num_qubits, self.data, 2 * d2, kernel);
2102            }
2103
2104            // Case B: Target is at mid position -> (Control, Target, Control)
2105            // Min=Control0, Mid=Target, Max=Control1
2106            // Need to swap amplitudes where Control0=1, Control1=1, Target=0 with Target=1
2107            //
2108            // Memory layout note:
2109            // We use zip to pair elements from low_t0 (Target=0) and high_t1 (Target=1).
2110            // Although low_t0 and high_t1 are not contiguous in memory, they are "symmetric"
2111            // in logical structure:
2112            // - low_t0 contains Min=0 and Min=1 parts
2113            // - high_t1 also contains Min=0 and Min=1 parts
2114            // Since both slices come from the same d1 split, their internal structures match.
2115            // Therefore, using zip on low_t0.chunks_exact_mut(2*d0) and
2116            // high_t1.chunks_exact_mut(2*d0) correctly pairs elements at the same Min index.
2117            (0, 1, 0) => {
2118                let kernel = |chunk: &mut [Complex64]| {
2119                    // Max (Control1) is a control, take Max=1 part
2120                    let (_, part_c1) = chunk.split_at_mut(d2);
2121
2122                    part_c1.chunks_exact_mut(2 * d1).for_each(|sub| {
2123                        // Mid is Target, split into Target=0 and Target=1
2124                        let (low_t0, high_t1) = sub.split_at_mut(d1);
2125
2126                        // Iterate over Min (Control0) layer
2127                        // Use memory structure symmetry to zip pair Target=0 and Target=1 blocks
2128                        low_t0
2129                            .chunks_exact_mut(2 * d0)
2130                            .zip(high_t1.chunks_exact_mut(2 * d0))
2131                            .for_each(|(leaf_t0, leaf_t1)| {
2132                                // Min is Control0, take Min=1 part (both controls are 1)
2133                                let (_, valid_t0) = leaf_t0.split_at_mut(d0);
2134                                let (_, valid_t1) = leaf_t1.split_at_mut(d0);
2135
2136                                // Execute SWAP: exchange Target=0 and Target=1 amplitudes
2137                                valid_t0
2138                                    .iter_mut()
2139                                    .zip(valid_t1.iter_mut())
2140                                    .for_each(|(a, b)| std::mem::swap(a, b));
2141                            });
2142                    });
2143                };
2144                with_maybe_par!(self.num_qubits, self.data, 2 * d2, kernel);
2145            }
2146
2147            // Case C: Target is at min position -> (Target, Control, Control)
2148            (1, 0, 0) => {
2149                let kernel = |chunk: &mut [Complex64]| {
2150                    let (_, part_max1) = chunk.split_at_mut(d2); // Max=1
2151
2152                    part_max1.chunks_exact_mut(2 * d1).for_each(|sub| {
2153                        let (_, part_mid1) = sub.split_at_mut(d1); // Mid=1
2154
2155                        part_mid1.chunks_exact_mut(2 * d0).for_each(|leaf| {
2156                            let (target0, target1) = leaf.split_at_mut(d0); // Min=0 vs Min=1
2157
2158                            // No further split needed since Min is Target
2159                            target0
2160                                .iter_mut()
2161                                .zip(target1.iter_mut())
2162                                .for_each(|(a, b)| std::mem::swap(a, b));
2163                        });
2164                    });
2165                };
2166                with_maybe_par!(self.num_qubits, self.data, 2 * d2, kernel);
2167            }
2168
2169            _ => unreachable!("Should cover all permutations"),
2170        }
2171        Ok(())
2172    }
2173
2174    /// Applies RXX gate (Ising XX interaction).
2175    ///
2176    /// Matrix: exp(-i·θ/2 · X⊗X) = cos(θ/2)I - i·sin(θ/2)·X⊗X
2177    ///
2178    /// # Arguments
2179    /// * `q0` - First qubit index
2180    /// * `q1` - Second qubit index
2181    /// * `theta` - Interaction angle
2182    pub fn apply_rxx(&mut self, q0: usize, q1: usize, theta: f64) -> Result<(), QisError> {
2183        self.validate_two_qubits(q0, q1)?;
2184        if theta.abs() < 1e-15 {
2185            return Ok(());
2186        }
2187
2188        let (q_min, q_max) = if q0 < q1 { (q0, q1) } else { (q1, q0) };
2189        let d_max = 1 << q_max;
2190        let d_min = 1 << q_min;
2191
2192        let half_theta = theta * 0.5;
2193        let c = half_theta.cos();
2194        let s = half_theta.sin();
2195
2196        let kernel = |chunk: &mut [Complex64]| {
2197            let (part0, part1) = chunk.split_at_mut(d_max);
2198            part0
2199                .chunks_exact_mut(2 * d_min)
2200                .zip(part1.chunks_exact_mut(2 * d_min))
2201                .for_each(|(sub0, sub1)| {
2202                    let (v00, v01) = sub0.split_at_mut(d_min);
2203                    let (v10, v11) = sub1.split_at_mut(d_min);
2204                    for (((a, b), c_amp), d) in v00
2205                        .iter_mut()
2206                        .zip(v01.iter_mut())
2207                        .zip(v10.iter_mut())
2208                        .zip(v11.iter_mut())
2209                    {
2210                        let a_in = *a;
2211                        let b_in = *b;
2212                        let c_in = *c_amp;
2213                        let d_in = *d;
2214                        *a = c * a_in + Complex64::new(0.0, -s) * d_in;
2215                        *b = c * b_in + Complex64::new(0.0, -s) * c_in;
2216                        *c_amp = Complex64::new(0.0, -s) * b_in + c * c_in;
2217                        *d = Complex64::new(0.0, -s) * a_in + c * d_in;
2218                    }
2219                });
2220        };
2221        with_maybe_par!(self.num_qubits, self.data, 2 * d_max, kernel);
2222        Ok(())
2223    }
2224
2225    /// Applies RYY gate (Ising YY interaction).
2226    ///
2227    /// Matrix: exp(-i·θ/2 · Y⊗Y) = cos(θ/2)I - i·sin(θ/2)·Y⊗Y
2228    ///
2229    /// # Arguments
2230    /// * `q0` - First qubit index
2231    /// * `q1` - Second qubit index
2232    /// * `theta` - Interaction angle
2233    pub fn apply_ryy(&mut self, q0: usize, q1: usize, theta: f64) -> Result<(), QisError> {
2234        self.validate_two_qubits(q0, q1)?;
2235        if theta.abs() < 1e-15 {
2236            return Ok(());
2237        }
2238
2239        let (q_min, q_max) = if q0 < q1 { (q0, q1) } else { (q1, q0) };
2240        let d_max = 1 << q_max;
2241        let d_min = 1 << q_min;
2242
2243        let half_theta = theta * 0.5;
2244        let c = half_theta.cos();
2245        let s = half_theta.sin();
2246
2247        let kernel = |chunk: &mut [Complex64]| {
2248            let (part0, part1) = chunk.split_at_mut(d_max);
2249            part0
2250                .chunks_exact_mut(2 * d_min)
2251                .zip(part1.chunks_exact_mut(2 * d_min))
2252                .for_each(|(sub0, sub1)| {
2253                    let (v00, v01) = sub0.split_at_mut(d_min);
2254                    let (v10, v11) = sub1.split_at_mut(d_min);
2255                    for (((a, b), c_amp), d) in v00
2256                        .iter_mut()
2257                        .zip(v01.iter_mut())
2258                        .zip(v10.iter_mut())
2259                        .zip(v11.iter_mut())
2260                    {
2261                        let a_in = *a;
2262                        let b_in = *b;
2263                        let c_in = *c_amp;
2264                        let d_in = *d;
2265                        *a = c * a_in + Complex64::new(0.0, s) * d_in;
2266                        *b = c * b_in + Complex64::new(0.0, -s) * c_in;
2267                        *c_amp = Complex64::new(0.0, -s) * b_in + c * c_in;
2268                        *d = Complex64::new(0.0, s) * a_in + c * d_in;
2269                    }
2270                });
2271        };
2272        with_maybe_par!(self.num_qubits, self.data, 2 * d_max, kernel);
2273        Ok(())
2274    }
2275
2276    /// Applies RZZ gate (Ising ZZ interaction).
2277    ///
2278    /// Matrix: exp(-i·θ/2 · Z⊗Z) - applies phase based on qubit parity
2279    ///
2280    /// # Arguments
2281    /// * `q0` - First qubit index
2282    /// * `q1` - Second qubit index
2283    /// * `theta` - Interaction angle
2284    pub fn apply_rzz(&mut self, q0: usize, q1: usize, theta: f64) -> Result<(), QisError> {
2285        self.validate_two_qubits(q0, q1)?;
2286        if theta.abs() < 1e-15 {
2287            return Ok(());
2288        }
2289
2290        let (q_min, q_max) = if q0 < q1 { (q0, q1) } else { (q1, q0) };
2291        let d_max = 1 << q_max;
2292        let d_min = 1 << q_min;
2293
2294        let half_theta = theta * 0.5;
2295        let phase_even = Complex64::from_polar(1.0, -half_theta);
2296        let phase_odd = Complex64::from_polar(1.0, half_theta);
2297
2298        let kernel = |chunk: &mut [Complex64]| {
2299            let (part0, part1) = chunk.split_at_mut(d_max);
2300            part0
2301                .chunks_exact_mut(2 * d_min)
2302                .zip(part1.chunks_exact_mut(2 * d_min))
2303                .for_each(|(sub0, sub1)| {
2304                    let (v00, v01) = sub0.split_at_mut(d_min);
2305                    let (v10, v11) = sub1.split_at_mut(d_min);
2306                    v00.iter_mut().for_each(|a| *a *= phase_even);
2307                    v11.iter_mut().for_each(|d| *d *= phase_even);
2308                    v01.iter_mut().for_each(|b| *b *= phase_odd);
2309                    v10.iter_mut().for_each(|c| *c *= phase_odd);
2310                });
2311        };
2312        with_maybe_par!(self.num_qubits, self.data, 2 * d_max, kernel);
2313        Ok(())
2314    }
2315
2316    /// Applies RZX gate (Ising ZX interaction).
2317    ///
2318    /// Matrix: exp(-i·θ/2 · Z⊗X)
2319    ///
2320    /// # Arguments
2321    /// * `q0` - First qubit index (Z rotation)
2322    /// * `q1` - Second qubit index (X rotation)
2323    /// * `theta` - Interaction angle
2324    pub fn apply_rzx(&mut self, q0: usize, q1: usize, theta: f64) -> Result<(), QisError> {
2325        self.validate_two_qubits(q0, q1)?;
2326        if theta.abs() < 1e-15 {
2327            return Ok(());
2328        }
2329
2330        let (q_min, q_max) = if q0 < q1 { (q0, q1) } else { (q1, q0) };
2331        let d_max = 1 << q_max;
2332        let d_min = 1 << q_min;
2333
2334        let half_theta = theta * 0.5;
2335        let c = half_theta.cos();
2336        let s = half_theta.sin();
2337
2338        let kernel = |chunk: &mut [Complex64]| {
2339            let (part0, part1) = chunk.split_at_mut(d_max);
2340            part0
2341                .chunks_exact_mut(2 * d_min)
2342                .zip(part1.chunks_exact_mut(2 * d_min))
2343                .for_each(|(sub0, sub1)| {
2344                    let (v00, v01) = sub0.split_at_mut(d_min);
2345                    let (v10, v11) = sub1.split_at_mut(d_min);
2346                    for (((a, b), c_amp), d) in v00
2347                        .iter_mut()
2348                        .zip(v01.iter_mut())
2349                        .zip(v10.iter_mut())
2350                        .zip(v11.iter_mut())
2351                    {
2352                        let a_in = *a;
2353                        let b_in = *b;
2354                        let c_in = *c_amp;
2355                        let d_in = *d;
2356                        if q0 < q1 {
2357                            *a = c * a_in + Complex64::new(0.0, -s) * c_in;
2358                            *b = c * b_in + Complex64::new(0.0, s) * d_in;
2359                            *c_amp = Complex64::new(0.0, -s) * a_in + c * c_in;
2360                            *d = Complex64::new(0.0, s) * b_in + c * d_in;
2361                        } else {
2362                            *a = c * a_in + Complex64::new(0.0, -s) * b_in;
2363                            *b = Complex64::new(0.0, -s) * a_in + c * b_in;
2364                            *c_amp = c * c_in + Complex64::new(0.0, s) * d_in;
2365                            *d = Complex64::new(0.0, s) * c_in + c * d_in;
2366                        }
2367                    }
2368                });
2369        };
2370        with_maybe_par!(self.num_qubits, self.data, 2 * d_max, kernel);
2371        Ok(())
2372    }
2373
2374    /// Applies controlled-RX gate.
2375    ///
2376    /// Applies RX(θ) to target when control is |1⟩.
2377    ///
2378    /// # Arguments
2379    /// * `control` - Control qubit index
2380    /// * `target` - Target qubit index
2381    /// * `theta` - Rotation angle
2382    pub fn apply_crx(&mut self, control: usize, target: usize, theta: f64) -> Result<(), QisError> {
2383        self.validate_two_qubits(control, target)?;
2384        if theta.abs() < 1e-15 {
2385            return Ok(());
2386        }
2387
2388        let (q_min, q_max) = if control < target {
2389            (control, target)
2390        } else {
2391            (target, control)
2392        };
2393        let d_max = 1 << q_max;
2394        let d_min = 1 << q_min;
2395
2396        let half_theta = theta * 0.5;
2397        let c = half_theta.cos();
2398        let s = half_theta.sin();
2399
2400        // CRX matrix on target (when control=1): [[c, -i*s], [-i*s, c]]
2401
2402        let kernel = |chunk: &mut [Complex64]| {
2403            let (part0, part1) = chunk.split_at_mut(d_max);
2404
2405            if control < target {
2406                // Control is q_min, target is q_max
2407                // Need to pair |Target=0, Control=1⟩ (part0, sub upper) with |Target=1, Control=1⟩ (part1, sub upper)
2408                part0
2409                    .chunks_exact_mut(2 * d_min)
2410                    .zip(part1.chunks_exact_mut(2 * d_min))
2411                    .for_each(|(sub0, sub1)| {
2412                        // sub0: target=0, sub1: target=1
2413                        // In each sub, lower half is control=0, upper half is control=1
2414                        let (_, target0_c1) = sub0.split_at_mut(d_min);
2415                        let (_, target1_c1) = sub1.split_at_mut(d_min);
2416
2417                        // Apply RX rotation to target when control=1
2418                        for (a, b) in target0_c1.iter_mut().zip(target1_c1.iter_mut()) {
2419                            let a_in = *a;
2420                            let b_in = *b;
2421                            // RX: [[c, -i*s], [-i*s, c]]
2422                            // -i*s*(x+iy) = s*y - i*s*x
2423                            *a = Complex64::new(
2424                                c * a_in.re + s * b_in.im,
2425                                c * a_in.im - s * b_in.re,
2426                            );
2427                            *b = Complex64::new(
2428                                s * a_in.im + c * b_in.re,
2429                                -s * a_in.re + c * b_in.im,
2430                            );
2431                        }
2432                    });
2433            } else {
2434                // Control is q_max, target is q_min
2435                // Process pairs where control=1
2436                part0
2437                    .chunks_exact_mut(2 * d_min)
2438                    .zip(part1.chunks_exact_mut(2 * d_min))
2439                    .for_each(|(_, sub1)| {
2440                        // sub0: control=0, sub1: control=1
2441                        // In sub1, split by target
2442                        let (target0_ctl1, target1_ctl1) = sub1.split_at_mut(d_min);
2443
2444                        // Apply RX rotation to target when control=1
2445                        for (a, b) in target0_ctl1.iter_mut().zip(target1_ctl1.iter_mut()) {
2446                            let a_in = *a;
2447                            let b_in = *b;
2448                            // RX: [[c, -i*s], [-i*s, c]]
2449                            *a = Complex64::new(
2450                                c * a_in.re + s * b_in.im,
2451                                c * a_in.im - s * b_in.re,
2452                            );
2453                            *b = Complex64::new(
2454                                s * a_in.im + c * b_in.re,
2455                                -s * a_in.re + c * b_in.im,
2456                            );
2457                        }
2458                    });
2459            }
2460        };
2461        with_maybe_par!(self.num_qubits, self.data, 2 * d_max, kernel);
2462        Ok(())
2463    }
2464
2465    /// Applies controlled-RY gate.
2466    ///
2467    /// Applies RY(θ) to target when control is |1⟩.
2468    ///
2469    /// # Arguments
2470    /// * `control` - Control qubit index
2471    /// * `target` - Target qubit index
2472    /// * `theta` - Rotation angle
2473    pub fn apply_cry(&mut self, control: usize, target: usize, theta: f64) -> Result<(), QisError> {
2474        self.validate_two_qubits(control, target)?;
2475        if theta.abs() < 1e-15 {
2476            return Ok(());
2477        }
2478
2479        let (q_min, q_max) = if control < target {
2480            (control, target)
2481        } else {
2482            (target, control)
2483        };
2484        let d_max = 1 << q_max;
2485        let d_min = 1 << q_min;
2486
2487        let half_theta = theta * 0.5;
2488        let c = half_theta.cos();
2489        let s = half_theta.sin();
2490
2491        let kernel = |chunk: &mut [Complex64]| {
2492            let (part0, part1) = chunk.split_at_mut(d_max);
2493
2494            if control < target {
2495                // Control is q_min, target is q_max
2496                // Need to pair |Target=0, Control=1> (part0, sub upper) with |Target=1, Control=1> (part1, sub upper)
2497                part0
2498                    .chunks_exact_mut(2 * d_min)
2499                    .zip(part1.chunks_exact_mut(2 * d_min))
2500                    .for_each(|(sub0, sub1)| {
2501                        // sub0: target=0, sub1: target=1
2502                        // In each sub, lower half is control=0, upper half is control=1
2503                        let (_, target0_c1) = sub0.split_at_mut(d_min);
2504                        let (_, target1_c1) = sub1.split_at_mut(d_min);
2505
2506                        // Apply RY rotation to target when control=1
2507                        for (a, b) in target0_c1.iter_mut().zip(target1_c1.iter_mut()) {
2508                            let a_in = *a;
2509                            let b_in = *b;
2510                            *a = c * a_in - s * b_in;
2511                            *b = s * a_in + c * b_in;
2512                        }
2513                    });
2514            } else {
2515                // Control is q_max, target is q_min
2516                // Process pairs where control=1
2517                part0
2518                    .chunks_exact_mut(2 * d_min)
2519                    .zip(part1.chunks_exact_mut(2 * d_min))
2520                    .for_each(|(_, sub1)| {
2521                        // sub0: control=0, sub1: control=1
2522                        // In sub1, split by target
2523                        let (target0_ctl1, target1_ctl1) = sub1.split_at_mut(d_min);
2524
2525                        // Apply RY rotation to target when control=1
2526                        for (a, b) in target0_ctl1.iter_mut().zip(target1_ctl1.iter_mut()) {
2527                            let a_in = *a;
2528                            let b_in = *b;
2529                            *a = c * a_in - s * b_in;
2530                            *b = s * a_in + c * b_in;
2531                        }
2532                    });
2533            }
2534        };
2535        with_maybe_par!(self.num_qubits, self.data, 2 * d_max, kernel);
2536        Ok(())
2537    }
2538
2539    /// Applies controlled-RZ gate.
2540    ///
2541    /// Applies RZ(θ) to target when control is |1⟩.
2542    ///
2543    /// # Arguments
2544    /// * `control` - Control qubit index
2545    /// * `target` - Target qubit index
2546    /// * `theta` - Rotation angle
2547    pub fn apply_crz(&mut self, control: usize, target: usize, theta: f64) -> Result<(), QisError> {
2548        self.validate_two_qubits(control, target)?;
2549        if theta.abs() < 1e-15 {
2550            return Ok(());
2551        }
2552
2553        let (q_min, q_max) = if control < target {
2554            (control, target)
2555        } else {
2556            (target, control)
2557        };
2558        let d_max = 1 << q_max;
2559        let d_min = 1 << q_min;
2560
2561        let half_theta = theta * 0.5;
2562        let phase_0 = Complex64::from_polar(1.0, -half_theta);
2563        let phase_1 = Complex64::from_polar(1.0, half_theta);
2564
2565        let kernel = |chunk: &mut [Complex64]| {
2566            let (part0, part1) = chunk.split_at_mut(d_max);
2567
2568            if control < target {
2569                // Control is q_min, target is q_max
2570                // Need to pair |Target=0, Control=1> (part0, sub upper) with |Target=1, Control=1> (part1, sub upper)
2571                part0
2572                    .chunks_exact_mut(2 * d_min)
2573                    .zip(part1.chunks_exact_mut(2 * d_min))
2574                    .for_each(|(sub0, sub1)| {
2575                        // sub0: target=0, sub1: target=1
2576                        // In each sub, lower half is control=0, upper half is control=1
2577                        let (_, target0_c1) = sub0.split_at_mut(d_min);
2578                        let (_, target1_c1) = sub1.split_at_mut(d_min);
2579
2580                        // Apply RZ rotation to target when control=1
2581                        for (a, b) in target0_c1.iter_mut().zip(target1_c1.iter_mut()) {
2582                            *a *= phase_0;
2583                            *b *= phase_1;
2584                        }
2585                    });
2586            } else {
2587                // Control is q_max, target is q_min
2588                // Process pairs where control=1
2589                part0
2590                    .chunks_exact_mut(2 * d_min)
2591                    .zip(part1.chunks_exact_mut(2 * d_min))
2592                    .for_each(|(_, sub1)| {
2593                        // sub0: control=0, sub1: control=1
2594                        // In sub1, split by target
2595                        let (target0_ctl1, target1_ctl1) = sub1.split_at_mut(d_min);
2596
2597                        // When control=1, apply RZ to target
2598                        for (a, b) in target0_ctl1.iter_mut().zip(target1_ctl1.iter_mut()) {
2599                            *a *= phase_0;
2600                            *b *= phase_1;
2601                        }
2602                    });
2603            }
2604        };
2605        with_maybe_par!(self.num_qubits, self.data, 2 * d_max, kernel);
2606        Ok(())
2607    }
2608
2609    /// Applies Fermionic Simulation (fSim) gate.
2610    ///
2611    /// Native gate for superconducting qubits. Combines iSWAP and controlled-phase.
2612    ///
2613    /// Matrix: [[1, 0, 0, 0], [0, c, -i·s, 0], [0, -i·s, c, 0], [0, 0, 0, e^(-iφ)]]
2614    /// where c = cos(θ), s = sin(θ)
2615    ///
2616    /// # Arguments
2617    /// * `q0` - First qubit index
2618    /// * `q1` - Second qubit index
2619    /// * `theta` - iSWAP angle
2620    /// * `phi` - Controlled-phase angle
2621    pub fn apply_fsim(
2622        &mut self,
2623        q0: usize,
2624        q1: usize,
2625        theta: f64,
2626        phi: f64,
2627    ) -> Result<(), QisError> {
2628        self.validate_two_qubits(q0, q1)?;
2629
2630        let (q_min, q_max) = if q0 < q1 { (q0, q1) } else { (q1, q0) };
2631        let d_max = 1 << q_max;
2632        let d_min = 1 << q_min;
2633
2634        let c = theta.cos();
2635        let s = theta.sin();
2636        let phase_11 = Complex64::from_polar(1.0, -phi);
2637
2638        let kernel = |chunk: &mut [Complex64]| {
2639            let (part0, part1) = chunk.split_at_mut(d_max);
2640            part0
2641                .chunks_exact_mut(2 * d_min)
2642                .zip(part1.chunks_exact_mut(2 * d_min))
2643                .for_each(|(sub0, sub1)| {
2644                    let (_, v01) = sub0.split_at_mut(d_min);
2645                    let (v10, v11) = sub1.split_at_mut(d_min);
2646
2647                    // |11⟩ gets phase
2648                    v11.iter_mut().for_each(|d| *d *= phase_11);
2649
2650                    // |01⟩ and |10⟩ get mixed
2651                    for (b, c_amp) in v01.iter_mut().zip(v10.iter_mut()) {
2652                        let b_in = *b;
2653                        let c_in = *c_amp;
2654                        *b = c * b_in + Complex64::new(0.0, -s) * c_in;
2655                        *c_amp = Complex64::new(0.0, -s) * b_in + c * c_in;
2656                    }
2657                });
2658        };
2659        with_maybe_par!(self.num_qubits, self.data, 2 * d_max, kernel);
2660        Ok(())
2661    }
2662
2663    /// Applies global phase.
2664    ///
2665    /// Multiplies the entire statevector by e^(iφ).
2666    /// Physically undetectable but mathematically relevant for circuit composition.
2667    ///
2668    /// # Arguments
2669    /// * `phi` - Phase angle
2670    pub fn apply_gphase(&mut self, phi: f64) -> Result<(), QisError> {
2671        if phi.abs() < 1e-15 {
2672            return Ok(());
2673        }
2674        let phase = Complex64::from_polar(1.0, phi);
2675        if self.num_qubits < PARALLEL_THRESHOLD {
2676            self.data.iter_mut().for_each(|a| *a *= phase);
2677        } else {
2678            self.data.par_iter_mut().for_each(|a| *a *= phase);
2679        }
2680        Ok(())
2681    }
2682
2683    /// Computes the expectation value of a Hamiltonian observable.
2684    ///
2685    /// Calculates ⟨ψ|H|ψ⟩ for the current statevector |ψ⟩ and a given Hamiltonian H.
2686    /// The Hamiltonian is represented as a sum of Pauli strings with coefficients.
2687    ///
2688    /// # Arguments
2689    /// * `h` - The Hamiltonian observable.
2690    ///
2691    /// # Returns
2692    /// The expectation value as a real number (f64), or a [`QisError`] if the
2693    /// qubit counts do not match.
2694    ///
2695    /// # Errors
2696    /// Returns [`QisError::CircuitError`] if the observable acts on a different
2697    /// number of qubits than the statevector.
2698    ///
2699    /// # Example
2700    /// ```rust
2701    /// use cqlib_core::qis::{Statevector, Hamiltonian};
2702    /// use cqlib_core::qis::pauli::{Pauli, PauliString};
2703    /// use num_complex::Complex64;
2704    ///
2705    /// let mut sv = Statevector::new(1);
2706    /// sv.apply_x(0);
2707    ///
2708    /// // Create Hamiltonian H = Z (Pauli-Z observable)
2709    /// let mut ps = PauliString::new(1);
2710    /// ps.set_pauli(0, Pauli::Z);
2711    /// let h = Hamiltonian::from_pauli(ps);
2712    ///
2713    /// let exp = sv.expectation(&h).unwrap();
2714    /// // For |1⟩ state, ⟨Z⟩ = -1
2715    /// ```
2716    pub fn expectation(&self, h: &dyn Observable) -> Result<f64, QisError> {
2717        h.expectation_statevector(self)
2718    }
2719
2720    /// Measures qubit `qubit` in the Z basis, collapsing the statevector.
2721    ///
2722    /// Returns `false` for outcome |0⟩ and `true` for outcome |1⟩.
2723    /// After measurement the statevector is normalised in the post-measurement subspace.
2724    ///
2725    /// This is a destructive operation — clone the statevector first if you need
2726    /// to preserve the pre-measurement state.
2727    pub fn measure(&mut self, qubit: usize) -> Result<bool, QisError> {
2728        let mut rng = rand::rng();
2729        self.measure_with_rng(qubit, &mut rng)
2730    }
2731
2732    /// Measures qubit `qubit` using a caller-provided random number generator.
2733    ///
2734    /// This is useful for reproducible simulations. For one-off non-deterministic
2735    /// measurements, use [`measure`](Self::measure).
2736    pub fn measure_with_rng(&mut self, qubit: usize, rng: &mut impl Rng) -> Result<bool, QisError> {
2737        self.validate_qubit(qubit)?;
2738        let n = self.num_qubits;
2739        let size = 1 << n;
2740        let mask = 1 << qubit;
2741
2742        // Compute probability of outcome |1⟩.
2743        let p1: f64 = (0..size)
2744            .filter(|i| i & mask != 0)
2745            .map(|i| self.data[i].norm_sqr())
2746            .sum();
2747
2748        let outcome = rng.random::<f64>() < p1;
2749
2750        // Collapse and renormalise.
2751        let norm = if outcome {
2752            p1.sqrt()
2753        } else {
2754            (1.0 - p1).sqrt()
2755        };
2756        for i in 0..size {
2757            let keep = if outcome {
2758                (i & mask) != 0
2759            } else {
2760                (i & mask) == 0
2761            };
2762            if keep {
2763                self.data[i] /= norm;
2764            } else {
2765                self.data[i] = Complex64::new(0.0, 0.0);
2766            }
2767        }
2768        Ok(outcome)
2769    }
2770
2771    /// Resets `qubit` to the |0⟩ state.
2772    ///
2773    /// Reset is modeled as a Z-basis measurement followed by an X correction
2774    /// when the measured outcome is |1⟩. This is destructive, matching circuit
2775    /// reset semantics.
2776    pub fn reset(&mut self, qubit: usize) -> Result<(), QisError> {
2777        if self.measure(qubit)? {
2778            self.apply_x(qubit)?;
2779        }
2780        Ok(())
2781    }
2782
2783    /// Copies amplitude data from `source` into `self` without allocating.
2784    ///
2785    /// Both statevectors must have the same `num_qubits`. Used by
2786    /// [`sample_shots`](Self::sample_shots) to reuse each thread's working copy.
2787    fn reset_from(&mut self, source: &Statevector) {
2788        debug_assert_eq!(self.num_qubits, source.num_qubits);
2789        self.qubit_map.clone_from(&source.qubit_map);
2790        self.data
2791            .as_mut_slice()
2792            .copy_from_slice(source.data.as_slice());
2793    }
2794
2795    /// Measures all qubits sequentially, returning a bit-packed [`Outcome`].
2796    ///
2797    /// Equivalent to calling `measure(q)` for each qubit `q` in order `0..num_qubits`.
2798    /// The statevector is fully collapsed after this call.
2799    /// Use [`Outcome::is_one(q)`](crate::device::Outcome::is_one) to read qubit `q`'s result.
2800    pub fn measure_all(&mut self) -> Outcome {
2801        self.measure_all_with_rng(&mut rand::rng())
2802    }
2803
2804    /// Measures all qubits in storage order using the supplied random stream.
2805    pub fn measure_all_with_rng(&mut self, rng: &mut impl Rng) -> Outcome {
2806        let num_chunks = self.num_qubits.div_ceil(64);
2807        let mut chunks = SmallVec::from_elem(0u64, num_chunks);
2808        for q in 0..self.num_qubits {
2809            if self.measure_with_rng(q, rng).unwrap() {
2810                chunks[q / 64] |= 1u64 << (q % 64);
2811            }
2812        }
2813        Outcome::new(chunks)
2814    }
2815
2816    /// Samples `shots` independent measurement outcomes in parallel.
2817    ///
2818    /// Each Rayon worker thread reuses a single pre-allocated clone of the
2819    /// statevector (via [`reset_from`](Self::reset_from)), avoiding per-shot
2820    /// heap allocation. Returns a [`Vec<Outcome>`] of bit-packed results.
2821    ///
2822    /// # Example
2823    /// ```rust
2824    /// use cqlib_core::qis::Statevector;
2825    ///
2826    /// let mut sv = Statevector::new(2);
2827    /// sv.apply_h(0).unwrap();
2828    /// sv.apply_cx(0, 1).unwrap(); // |Φ⁺⟩ Bell state
2829    ///
2830    /// let shots = sv.sample_shots(200);
2831    /// // All outcomes must be |00⟩ or |11⟩
2832    /// assert!(shots.iter().all(|v| v.is_one(0) == v.is_one(1)));
2833    /// ```
2834    pub fn sample_shots(&self, shots: usize) -> Vec<Outcome> {
2835        self.sample_shots_with_seed(shots, None)
2836    }
2837
2838    /// Samples without changing this state. An explicit seed assigns a random
2839    /// stream to each shot before parallel execution, independent of scheduling.
2840    pub fn sample_shots_with_seed(&self, shots: usize, seed: Option<u64>) -> Vec<Outcome> {
2841        if let Some(seed) = seed {
2842            let mut rng = SmallRng::seed_from_u64(seed);
2843            let seeds: Vec<u64> = (0..shots).map(|_| rng.random()).collect();
2844            return seeds
2845                .into_par_iter()
2846                .map_with(self.clone(), |work, seed| {
2847                    work.reset_from(self);
2848                    work.measure_all_with_rng(&mut SmallRng::seed_from_u64(seed))
2849                })
2850                .collect();
2851        }
2852        (0..shots)
2853            .into_par_iter()
2854            .map_with(self.clone(), |work, _| {
2855                work.reset_from(self);
2856                work.measure_all()
2857            })
2858            .collect()
2859    }
2860
2861    /// Samples the state using a circuit [`Measurement`] as the output contract.
2862    ///
2863    /// `measurement.qubits()[i]` becomes bit `i` in each [`Outcome`]. The returned
2864    /// [`ExecutionResult`] reuses the device result shape, with counts projected
2865    /// to only the qubits named by the measurement.
2866    pub fn sample(
2867        &self,
2868        measurement: &Measurement,
2869        shots: usize,
2870    ) -> Result<ExecutionResult, QisError> {
2871        self.sample_with_seed(measurement, shots, None)
2872    }
2873
2874    /// Projects seeded full-state samples onto the requested measurement.
2875    pub fn sample_with_seed(
2876        &self,
2877        measurement: &Measurement,
2878        shots: usize,
2879        seed: Option<u64>,
2880    ) -> Result<ExecutionResult, QisError> {
2881        let projection = super::resolve_measurement(measurement, &self.qubit_map, self.num_qubits)?;
2882
2883        let mut counts = HashMap::new();
2884        for full in self.sample_shots_with_seed(shots, seed) {
2885            *counts.entry(projection.project(&full)).or_insert(0usize) += 1;
2886        }
2887
2888        Ok(ExecutionResult::from_counts(
2889            "statevector-sample".to_string(),
2890            measurement.qubits().to_vec(),
2891            shots,
2892            measurement.width(),
2893            Some("statevector".to_string()),
2894            counts,
2895        ))
2896    }
2897
2898    /// Returns the marginal probability distribution selected by a circuit [`Measurement`].
2899    ///
2900    /// This method does not execute circuit IR. It treats the [`Measurement`] as
2901    /// a self-contained output contract and aggregates the full statevector
2902    /// computational-basis probabilities over unmeasured qubits.
2903    pub fn probs(&self, measurement: &Measurement) -> Result<HashMap<Outcome, f64>, QisError> {
2904        let projection = super::resolve_measurement(measurement, &self.qubit_map, self.num_qubits)?;
2905
2906        let mut marginal = HashMap::new();
2907        for (basis, prob) in self.probabilities().into_iter().enumerate() {
2908            if prob == 0.0 {
2909                continue;
2910            }
2911            *marginal
2912                .entry(projection.project_basis(basis))
2913                .or_insert(0.0) += prob;
2914        }
2915        Ok(marginal)
2916    }
2917}
2918
2919#[cfg(test)]
2920#[path = "./statevector_test.rs"]
2921mod statevector_test;