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