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