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