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