Skip to main content

cqlib_core/qis/state/
density_matrix.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 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//! Density Matrix quantum simulation.
15//!
16//! Provides a high-performance density matrix simulator capable of simulating mixed
17//! quantum states and quantum channels (e.g., via Kraus operators).
18//!
19//! # Architecture
20//! The simulator utilizes a $2N$-qubit isomorphism (a variant of the Choi-Jamiołkowski isomorphism)
21//! to map the $2^N \times 2^N$ density matrix into a statevector of size $4^N$:
22//! - **Data Representation**: The density matrix is flattened into a 1D vector of length $4^N$.
23//! - **Ket Side (Left Multiply)**: Applying a unitary $U$ acts on the upper half of the conceptual $2N$ qubits (indices $N$ to $2N-1$).
24//! - **Bra Side (Right Multiply)**: Applying $U^\dagger$ acts on the lower half of the conceptual $2N$ qubits (indices 0 to $N-1$), applying the conjugate matrix $U^*$.
25//!
26//! This design allows the simulator to directly reuse highly-optimized statevector and bitwise memory kernels,
27//! achieving exceptional parallel performance and cache locality.
28
29use crate::circuit::Measurement;
30use crate::circuit::circuit_impl::Circuit;
31use crate::circuit::circuit_param::CircuitParam;
32use crate::circuit::error::CircuitError;
33use crate::circuit::gate::StandardGate;
34use crate::circuit::gate::directive::Directive;
35use crate::circuit::gate::instruction::Instruction;
36use crate::device::{ExecutionResult, Outcome};
37use crate::qis::Observable;
38use crate::qis::error::QisError;
39use num_complex::Complex64;
40use rand::{Rng, SeedableRng, rngs::SmallRng};
41use rayon::prelude::*;
42use smallvec::{SmallVec, smallvec};
43use std::collections::{HashMap, HashSet};
44use std::f64::consts::{FRAC_1_SQRT_2, FRAC_PI_2, FRAC_PI_4};
45
46const PARALLEL_THRESHOLD: usize = 7;
47
48macro_rules! with_maybe_par {
49    ($num_qubits:expr, $data:expr, $chunk_size:expr, $body:expr) => {{
50        if $num_qubits < PARALLEL_THRESHOLD {
51            $data.chunks_exact_mut($chunk_size).for_each($body);
52        } else {
53            $data.par_chunks_exact_mut($chunk_size).for_each($body);
54        }
55    }};
56}
57
58#[inline(always)]
59fn interleave_bits(keep: &[usize], k_val: usize, trace: &[usize], t_val: usize) -> usize {
60    let mut res = 0;
61    for (idx, &q) in keep.iter().enumerate() {
62        if (k_val >> idx) & 1 == 1 {
63            res |= 1 << q;
64        }
65    }
66    for (idx, &q) in trace.iter().enumerate() {
67        if (t_val >> idx) & 1 == 1 {
68            res |= 1 << q;
69        }
70    }
71    res
72}
73
74/// Quantum density matrix representing mixed or pure quantum states.
75///
76/// A density matrix describes the statistical state of an N-qubit quantum system.
77/// Unlike a statevector which can only represent pure states, a density matrix
78/// can represent mixed states (ensembles of pure states).
79///
80/// # Memory Layout
81/// The `data` vector uses a contiguous memory layout representing a flattened
82/// $2^N \times 2^N$ matrix. To optimize simulation performance, the simulator
83/// employs a $2N$-qubit isomorphism:
84/// - The matrix is treated as a statevector of $2N$ qubits.
85/// - The "ket" side (Left U) acts on the upper $N$ qubits (indices $N$ to $2N-1$).
86/// - The "bra" side (Right $U^\dagger$) acts on the lower $N$ qubits (indices 0 to $N-1$).
87///
88/// # Example
89/// ```rust
90/// use cqlib_core::qis::DensityMatrix;
91///
92/// // Create a 1-qubit density matrix in state |0><0|
93/// let mut dm = DensityMatrix::new(1);
94/// assert_eq!(dm.num_qubits, 1);
95///
96/// // Apply Hadamard gate -> |+><+|
97/// dm.apply_h(0);
98///
99/// // Probabilities should be 0.5 for both |0> and |1>
100/// let probs = dm.probabilities();
101/// assert!((probs[0] - 0.5).abs() < 1e-10);
102/// assert!((probs[1] - 0.5).abs() < 1e-10);
103/// ```
104#[derive(Debug, Clone)]
105pub struct DensityMatrix {
106    /// Flattened matrix elements. Length is $4^N$.
107    data: Vec<Complex64>,
108    /// Circuit qubit identifiers mapped to storage positions.
109    pub(super) qubit_map: super::QubitMap,
110    /// Number of qubits in the system ($N$).
111    pub num_qubits: usize,
112}
113
114impl std::ops::AddAssign for DensityMatrix {
115    fn add_assign(&mut self, rhs: Self) {
116        assert_eq!(self.num_qubits, rhs.num_qubits);
117        if self.num_qubits < PARALLEL_THRESHOLD {
118            self.data
119                .iter_mut()
120                .zip(rhs.data)
121                .for_each(|(a, b)| *a += b);
122        } else {
123            self.data
124                .par_iter_mut()
125                .zip(rhs.data.into_par_iter())
126                .for_each(|(a, b)| *a += b);
127        }
128    }
129}
130
131impl DensityMatrix {
132    /// Returns the flattened row-major density matrix data.
133    pub fn data(&self) -> &[Complex64] {
134        &self.data
135    }
136
137    /// Validates that a single qubit index is within bounds.
138    #[inline]
139    fn validate_qubit(&self, qubit: usize) -> Result<(), QisError> {
140        if self.num_qubits == 0 {
141            return Err(QisError::InvalidParameterValue(
142                "no valid qubit indices in a zero-qubit system".to_string(),
143            ));
144        }
145        if qubit >= self.num_qubits {
146            return Err(QisError::IndexOutOfBounds {
147                index: qubit,
148                max: self.num_qubits.saturating_sub(1),
149            });
150        }
151        Ok(())
152    }
153
154    /// Validates that two qubit indices are within bounds and distinct.
155    #[inline]
156    fn validate_two_qubits(&self, q0: usize, q1: usize) -> Result<(), QisError> {
157        if self.num_qubits == 0 {
158            return Err(QisError::InvalidParameterValue(
159                "no valid qubit indices in a zero-qubit system".to_string(),
160            ));
161        }
162        if q0 >= self.num_qubits {
163            return Err(QisError::IndexOutOfBounds {
164                index: q0,
165                max: self.num_qubits.saturating_sub(1),
166            });
167        }
168        if q1 >= self.num_qubits {
169            return Err(QisError::IndexOutOfBounds {
170                index: q1,
171                max: self.num_qubits.saturating_sub(1),
172            });
173        }
174        if q0 == q1 {
175            return Err(QisError::InvalidParameterValue(format!(
176                "Duplicate qubit index {q0} in gate operation"
177            )));
178        }
179        Ok(())
180    }
181
182    /// Validates that all qubit indices in a slice are within bounds and distinct.
183    #[inline]
184    fn validate_qubits(&self, qubits: &[usize]) -> Result<(), QisError> {
185        if self.num_qubits == 0 {
186            return Err(QisError::InvalidParameterValue(
187                "no valid qubit indices in a zero-qubit system".to_string(),
188            ));
189        }
190        for &q in qubits {
191            if q >= self.num_qubits {
192                return Err(QisError::IndexOutOfBounds {
193                    index: q,
194                    max: self.num_qubits.saturating_sub(1),
195                });
196            }
197        }
198        for (i, &q) in qubits.iter().enumerate() {
199            if qubits[..i].contains(&q) {
200                return Err(QisError::InvalidParameterValue(format!(
201                    "Duplicate qubit index {q} in gate operation"
202                )));
203            }
204        }
205        Ok(())
206    }
207
208    /// Creates a new density matrix initialized to the pure state $|0\dots 0\rangle\langle 0\dots 0|$.
209    ///
210    /// # Arguments
211    /// * `num_qubits` - Number of qubits in the system.
212    ///
213    /// # Panics
214    ///
215    /// Panics if the dimension is unrepresentable or allocation fails.
216    /// Use [`Self::try_new`] to receive an error instead.
217    pub fn new(num_qubits: usize) -> Self {
218        Self::try_new(num_qubits).expect("cannot allocate density matrix")
219    }
220
221    /// Creates the ground state, returning an error for an unrepresentable
222    /// dimension or a failed allocation.
223    ///
224    /// # Errors
225    ///
226    /// Returns [`QisError::InvalidParameterValue`] for an unrepresentable
227    /// dimension, or [`QisError::UnsupportedOperation`] if allocation fails.
228    pub fn try_new(num_qubits: usize) -> Result<Self, QisError> {
229        let size = super::checked_state_len(num_qubits, true)?;
230        let mut data = Vec::new();
231        data.try_reserve_exact(size).map_err(|_| {
232            QisError::UnsupportedOperation("density matrix allocation failed".into())
233        })?;
234        data.resize(size, Complex64::new(0.0, 0.0));
235        data[0] = Complex64::new(1.0, 0.0);
236        Ok(Self {
237            data,
238            num_qubits,
239            qubit_map: None,
240        })
241    }
242
243    /// Creates the maximally mixed state $I / 2^N$.
244    ///
245    /// The returned density matrix has equal probability on every
246    /// computational-basis state and zero off-diagonal entries. A zero-qubit
247    /// system is represented by the one-dimensional density matrix `[1]`.
248    ///
249    /// Like every dense density-matrix constructor, this allocates $O(4^N)$
250    /// complex values.
251    ///
252    /// # Panics
253    ///
254    /// Panics if the dimension is unrepresentable or allocation fails.
255    /// Use [`Self::try_maximally_mixed`] to receive an error instead.
256    pub fn maximally_mixed(num_qubits: usize) -> Self {
257        Self::try_maximally_mixed(num_qubits).expect("cannot allocate density matrix")
258    }
259
260    /// Fallible variant of [`Self::maximally_mixed`].
261    ///
262    /// # Errors
263    ///
264    /// Returns [`QisError::InvalidParameterValue`] for an unrepresentable
265    /// dimension, or [`QisError::UnsupportedOperation`] if allocation fails.
266    pub fn try_maximally_mixed(num_qubits: usize) -> Result<Self, QisError> {
267        let mut state = Self::try_new(num_qubits)?;
268        let dim = 1usize << num_qubits;
269        let probability = Complex64::new(1.0 / dim as f64, 0.0);
270        for index in 0..dim {
271            state.data[index * dim + index] = probability;
272        }
273        Ok(state)
274    }
275
276    /// Creates a density matrix from an initial statevector (pure state).
277    ///
278    /// Internally computes the outer product $\rho = |\psi\rangle\langle\psi|$.
279    ///
280    /// # Arguments
281    /// * `num_qubits` - Number of qubits in the system.
282    /// * `initial_state` - Vector of $2^N$ complex amplitudes representing a normalized pure state.
283    ///
284    /// # Errors
285    /// Returns [`QisError::InvalidStateDimension`] if the `initial_state` length is
286    /// incorrect, or [`QisError::NotNormalized`] if it is not normalized.
287    pub fn from_state(num_qubits: usize, initial_state: Vec<Complex64>) -> Result<Self, QisError> {
288        super::checked_state_len(num_qubits, true)?;
289        let dim = 1usize << num_qubits;
290        if initial_state.len() != dim {
291            return Err(QisError::InvalidStateDimension(initial_state.len()));
292        }
293        let norm: f64 = initial_state.iter().map(|c| c.norm_sqr()).sum();
294        if !norm.is_finite() || (norm - 1.0).abs() >= 1e-10 {
295            return Err(QisError::NotNormalized);
296        }
297
298        let mut data = Self::try_new(num_qubits)?.data;
299
300        let kernel = |(i, chunk): (usize, &mut [Complex64])| {
301            let alpha_i = initial_state[i];
302            for j in 0..dim {
303                chunk[j] = alpha_i * initial_state[j].conj();
304            }
305        };
306
307        if num_qubits < PARALLEL_THRESHOLD {
308            data.chunks_exact_mut(dim).enumerate().for_each(kernel);
309        } else {
310            data.par_chunks_exact_mut(dim).enumerate().for_each(kernel);
311        }
312
313        Ok(Self {
314            data,
315            num_qubits,
316            qubit_map: None,
317        })
318    }
319
320    /// Creates a density matrix directly from a flattened $2^N \times 2^N$ matrix.
321    ///
322    /// Validates all physical constraints: Hermiticity, positive semidefiniteness, and unit trace.
323    ///
324    /// # Arguments
325    /// * `num_qubits` - Number of qubits in the system.
326    /// * `dm_state` - Vector of $4^N$ complex values representing the density matrix.
327    ///
328    /// # Errors
329    /// Returns `QisError::InvalidStateDimension` if the matrix length is incorrect.
330    /// Returns `QisError::NotHermitian` if the matrix is not Hermitian.
331    /// Returns `QisError::NotPositiveSemidefinite` if the matrix has negative eigenvalues.
332    /// Returns `QisError::NotNormalized` if the trace is not equal to 1.
333    pub fn from_density_matrix_state(
334        num_qubits: usize,
335        dm_state: Vec<Complex64>,
336    ) -> Result<Self, QisError> {
337        let size = super::checked_state_len(num_qubits, true)?;
338        if dm_state.len() != size {
339            return Err(QisError::InvalidStateDimension(dm_state.len()));
340        }
341
342        let dm = Self {
343            data: dm_state,
344            num_qubits,
345            qubit_map: None,
346        };
347        dm.validate_physical(1e-10)?;
348        Ok(dm)
349    }
350
351    /// Computes the measurement probability distribution over all computational basis states.
352    ///
353    /// Extracts the diagonal elements of the density matrix, which represent
354    /// the probabilities $P(|i\rangle) = \rho_{ii}$.
355    ///
356    /// # Returns
357    /// Vector of probabilities summing to 1.0.
358    pub fn probabilities(&self) -> Vec<f64> {
359        let dim = 1 << self.num_qubits;
360        if self.num_qubits < PARALLEL_THRESHOLD {
361            (0..dim).map(|i| self.data[i * dim + i].re).collect()
362        } else {
363            (0..dim)
364                .into_par_iter()
365                .map(|i| self.data[i * dim + i].re)
366                .collect()
367        }
368    }
369
370    /// Constructs a density matrix by simulating a quantum circuit.
371    ///
372    /// Executes the circuit gates sequentially to evolve the initial $|0\dots 0\rangle\langle 0\dots 0|$ state.
373    ///
374    /// # Arguments
375    /// * `circuit` - The quantum circuit to simulate.
376    ///
377    /// # Returns
378    /// * `Ok(DensityMatrix)` - The resulting density matrix after execution.
379    /// * `Err(QisError)` - If the circuit contains unsupported operations.
380    pub fn from_circuit(circuit: &Circuit) -> Result<Self, QisError> {
381        let mut dm = DensityMatrix::try_new(circuit.num_qubits())?;
382        dm.apply_circuit(circuit)?;
383        Ok(dm)
384    }
385
386    /// Applies a quantum circuit to this density matrix in-place.
387    ///
388    /// Once bound to circuit qubit IDs, subsequent circuits must have the same
389    /// IDs in the same order. The mapping is committed only on success; gates
390    /// completed before an error are not rolled back.
391    ///
392    /// As in `from_circuit`, terminal measurements are ignored. A measured qubit
393    /// used by a later gate or Reset is rejected before the state is changed.
394    /// Independent gates, repeated measurements, Barrier, Delay, and Store are allowed.
395    ///
396    /// The circuit is first decomposed into basic gates via [`Circuit::decompose`],
397    /// then each operation is applied sequentially to the current density matrix.
398    ///
399    /// # Arguments
400    /// * `circuit` - The circuit to apply. Must have the same number of qubits
401    ///   as this density matrix.
402    ///
403    /// # Returns
404    /// * `Ok(())` - If all operations were applied successfully
405    /// * `Err(QisError)` - If the circuit cannot be simulated
406    ///
407    /// # Errors
408    /// - [`QisError::InvalidStateDimension`] if `circuit.num_qubits() != self.num_qubits`
409    /// - [`QisError::UnsupportedOperation`] if the circuit contains classical control flow
410    /// - [`QisError::CircuitError`] if a gate lacks a matrix representation, contains
411    ///   unresolved symbolic parameters, or a referenced qubit is not found
412    ///
413    /// # Supported Instructions
414    /// - Standard single and multi-qubit gates
415    /// - Controlled gates (CX, CY, CZ, CRX, CRY, CRZ)
416    /// - Multi-controlled gates (CCX / Toffoli)
417    /// - Unitary gates with an explicit matrix representation
418    /// - `Reset`
419    /// - Barriers and delays (ignored)
420    /// - Measurement output declarations from `Circuit::measure*` (ignored)
421    pub fn apply_circuit(&mut self, circuit: &Circuit) -> Result<(), QisError> {
422        if self.num_qubits != circuit.num_qubits() {
423            return Err(QisError::InvalidStateDimension(circuit.num_qubits()));
424        }
425        if circuit
426            .operations()
427            .iter()
428            .any(|op| matches!(op.instruction, Instruction::ClassicalControl(_)))
429        {
430            return Err(QisError::UnsupportedOperation(
431                "classical control flow is not supported in density matrix simulation".to_string(),
432            ));
433        }
434        let circuit = circuit.decompose()?;
435        super::validate_terminal_measurements(&circuit)?;
436        let dm = self;
437
438        let qubit_map = super::prepare_qubit_map(&circuit.qubits(), &dm.qubit_map)?;
439
440        let parameter_values: Vec<Option<f64>> = circuit
441            .parameters()
442            .iter()
443            .map(|p| p.evaluate(&None).ok())
444            .collect();
445
446        for op in circuit.operations() {
447            let params: Vec<f64> = op
448                .params
449                .iter()
450                .map(|p| match p {
451                    CircuitParam::Fixed(v) => Ok(*v),
452                    CircuitParam::Index(idx) => parameter_values
453                        .get(*idx as usize)
454                        .copied()
455                        .flatten()
456                        .ok_or(QisError::CircuitError(CircuitError::SymbolicParameterError)),
457                })
458                .collect::<Result<Vec<_>, QisError>>()?;
459
460            let qubit_indices: Result<Vec<usize>, QisError> =
461                op.qubits
462                    .iter()
463                    .map(|q| {
464                        qubit_map.get(q).copied().ok_or_else(|| {
465                            QisError::CircuitError(CircuitError::QubitNotFound(q.id()))
466                        })
467                    })
468                    .collect();
469            let qubit_indices = qubit_indices?;
470
471            match &op.instruction {
472                Instruction::Standard(gate) => {
473                    dm.apply_standard_gate(*gate, &qubit_indices, &params)?;
474                }
475                Instruction::McGate(mc_gate) => {
476                    let num_controls = mc_gate.num_ctrl_qubits();
477                    let base_gate = mc_gate.base_gate();
478
479                    match (num_controls, *base_gate) {
480                        (0, StandardGate::GPhase) => {
481                            dm.apply_standard_gate(*base_gate, &qubit_indices, &params)?
482                        }
483                        (1, StandardGate::X) => dm.apply_cx(qubit_indices[0], qubit_indices[1])?,
484                        (1, StandardGate::Y) => dm.apply_cy(qubit_indices[0], qubit_indices[1])?,
485                        (1, StandardGate::Z) => dm.apply_cz(qubit_indices[0], qubit_indices[1])?,
486                        (1, StandardGate::RX) => {
487                            dm.apply_crx(qubit_indices[0], qubit_indices[1], params[0])?
488                        }
489                        (1, StandardGate::RY) => {
490                            dm.apply_cry(qubit_indices[0], qubit_indices[1], params[0])?
491                        }
492                        (1, StandardGate::RZ) => {
493                            dm.apply_crz(qubit_indices[0], qubit_indices[1], params[0])?
494                        }
495                        (2, StandardGate::X) => {
496                            dm.apply_ccx(qubit_indices[0], qubit_indices[1], qubit_indices[2])?
497                        }
498                        _ => {
499                            let matrix = mc_gate.matrix(&params).map_err(|_| {
500                                QisError::CircuitError(CircuitError::NoMatrixRepresentation)
501                            })?;
502                            dm.apply_unitary_gate(&qubit_indices, &matrix)?;
503                        }
504                    }
505                }
506                Instruction::UnitaryGate(u_gate) => {
507                    if let Some(matrix) = u_gate.matrix() {
508                        dm.apply_unitary_gate(&qubit_indices, matrix)?;
509                    } else {
510                        return Err(QisError::CircuitError(CircuitError::NoMatrixRepresentation));
511                    }
512                }
513                Instruction::CircuitGate(_) => {
514                    return Err(QisError::CircuitError(CircuitError::InvalidOperation(
515                        "CircuitGate should have been decomposed".to_string(),
516                    )));
517                }
518                Instruction::Directive(directive) => match directive {
519                    Directive::Barrier => {}
520                    Directive::Measure => {
521                        return Err(QisError::UnsupportedOperation(
522                            "legacy Measure directive is not supported by density matrix circuit evolution; use Circuit::measure as an output declaration".to_string(),
523                        ));
524                    }
525                    Directive::Reset => {
526                        let qubit = qubit_indices[0];
527                        dm.reset(qubit)?;
528                    }
529                },
530                Instruction::Delay => continue,
531                Instruction::ClassicalControl(_) => {
532                    return Err(QisError::UnsupportedOperation(
533                        "classical control flow is not supported in density matrix simulation"
534                            .to_string(),
535                    ));
536                }
537                Instruction::ClassicalData(_) => {}
538            }
539        }
540        dm.qubit_map = Some(qubit_map);
541        Ok(())
542    }
543
544    pub fn apply_standard_gate(
545        &mut self,
546        gate: StandardGate,
547        qubits: &[usize],
548        params: &[f64],
549    ) -> Result<(), QisError> {
550        if qubits.len() != gate.num_qubits() {
551            return Err(QisError::InvalidParameterValue(format!(
552                "Gate {:?} requires {} qubits, got {}",
553                gate,
554                gate.num_qubits(),
555                qubits.len()
556            )));
557        }
558        if params.len() != gate.num_params() {
559            return Err(QisError::InvalidParameterValue(format!(
560                "Gate {:?} requires {} parameters, got {}",
561                gate,
562                gate.num_params(),
563                params.len()
564            )));
565        }
566        match gate {
567            StandardGate::I => {}
568            StandardGate::X => self.apply_x(qubits[0])?,
569            StandardGate::Y => self.apply_y(qubits[0])?,
570            StandardGate::Z => self.apply_z(qubits[0])?,
571            StandardGate::H => self.apply_h(qubits[0])?,
572            StandardGate::S => self.apply_s(qubits[0])?,
573            StandardGate::SDG => self.apply_sdg(qubits[0])?,
574            StandardGate::T => self.apply_t(qubits[0])?,
575            StandardGate::TDG => self.apply_tdg(qubits[0])?,
576            StandardGate::RX => self.apply_rx(qubits[0], params[0])?,
577            StandardGate::RY => self.apply_ry(qubits[0], params[0])?,
578            StandardGate::RZ => self.apply_rz(qubits[0], params[0])?,
579            StandardGate::Phase => self.apply_phase(qubits[0], params[0])?,
580            StandardGate::X2P => self.apply_x2p(qubits[0])?,
581            StandardGate::X2M => self.apply_x2m(qubits[0])?,
582            StandardGate::Y2P => self.apply_y2p(qubits[0])?,
583            StandardGate::Y2M => self.apply_y2m(qubits[0])?,
584            StandardGate::RXY => self.apply_rxy(qubits[0], params[0], params[1])?,
585            StandardGate::XY => self.apply_xy(qubits[0], params[0])?,
586            StandardGate::XY2P => self.apply_xy2p(qubits[0], params[0])?,
587            StandardGate::XY2M => self.apply_xy2m(qubits[0], params[0])?,
588            StandardGate::U => self.apply_u(qubits[0], params[0], params[1], params[2])?,
589            StandardGate::GPhase => self.apply_gphase(params[0]),
590
591            StandardGate::CX => self.apply_cx(qubits[0], qubits[1])?,
592            StandardGate::CY => self.apply_cy(qubits[0], qubits[1])?,
593            StandardGate::CZ => self.apply_cz(qubits[0], qubits[1])?,
594            StandardGate::SWAP => self.apply_swap(qubits[0], qubits[1])?,
595            StandardGate::RXX => self.apply_rxx(qubits[0], qubits[1], params[0])?,
596            StandardGate::RYY => self.apply_ryy(qubits[0], qubits[1], params[0])?,
597            StandardGate::RZZ => self.apply_rzz(qubits[0], qubits[1], params[0])?,
598            StandardGate::RZX => self.apply_rzx(qubits[0], qubits[1], params[0])?,
599
600            StandardGate::CRX => self.apply_crx(qubits[0], qubits[1], params[0])?,
601            StandardGate::CRY => self.apply_cry(qubits[0], qubits[1], params[0])?,
602            StandardGate::CRZ => self.apply_crz(qubits[0], qubits[1], params[0])?,
603
604            StandardGate::CCX => self.apply_ccx(qubits[0], qubits[1], qubits[2])?,
605
606            StandardGate::FSIM => self.apply_fsim(qubits[0], qubits[1], params[0], params[1])?,
607        }
608        Ok(())
609    }
610
611    /// Creates a new density matrix filled entirely with zeros.
612    ///
613    /// This is not a valid physical state (trace = 0) but is useful as an accumulator
614    /// during operations like Kraus channel application.
615    ///
616    /// # Arguments
617    /// * `num_qubits` - Number of qubits in the system.
618    ///
619    /// # Panics
620    ///
621    /// Panics if the dimension is unrepresentable or allocation fails.
622    /// Use [`Self::try_zeros`] to receive an error instead.
623    pub fn zeros(num_qubits: usize) -> Self {
624        Self::try_zeros(num_qubits).expect("cannot allocate density matrix")
625    }
626
627    /// Fallible variant of [`Self::zeros`].
628    ///
629    /// # Errors
630    ///
631    /// Returns [`QisError::InvalidParameterValue`] for an unrepresentable
632    /// dimension, or [`QisError::UnsupportedOperation`] if allocation fails.
633    pub fn try_zeros(num_qubits: usize) -> Result<Self, QisError> {
634        let mut state = Self::try_new(num_qubits)?;
635        state.data[0] = Complex64::new(0.0, 0.0);
636        Ok(state)
637    }
638
639    /// Computes the trace of the density matrix.
640    ///
641    /// For any valid physical state, the trace must equal 1.0.
642    ///
643    /// # Returns
644    /// The trace (sum of diagonal elements) as a complex number.
645    pub fn trace(&self) -> Complex64 {
646        let n = self.num_qubits;
647        let dim = 1 << n;
648        if n < PARALLEL_THRESHOLD {
649            (0..dim).map(|i| self.data[i * dim + i]).sum()
650        } else {
651            (0..dim)
652                .into_par_iter()
653                .map(|i| self.data[i * dim + i])
654                .sum()
655        }
656    }
657
658    /// Checks if the density matrix is Hermitian (self-adjoint) within a tolerance.
659    ///
660    /// A valid density matrix must satisfy ρ = ρ†, i.e., ρ_ij = ρ_ji*.
661    ///
662    /// # Arguments
663    /// * `tol` - Tolerance for floating-point comparison (e.g., 1e-10).
664    ///
665    /// # Returns
666    /// `true` if the matrix is Hermitian within the specified tolerance.
667    pub fn is_hermitian(&self, tol: f64) -> bool {
668        if self
669            .data
670            .iter()
671            .any(|v| !v.re.is_finite() || !v.im.is_finite())
672        {
673            return false;
674        }
675        let dim = 1 << self.num_qubits;
676        for i in 0..dim {
677            for j in 0..i {
678                let ij_idx = i * dim + j;
679                let ji_idx = j * dim + i;
680                let ij = self.data[ij_idx];
681                let ji_conj = self.data[ji_idx].conj();
682                if (ij.re - ji_conj.re).abs() > tol || (ij.im - ji_conj.im).abs() > tol {
683                    return false;
684                }
685            }
686        }
687        // Check diagonal is real
688        for i in 0..dim {
689            if self.data[i * dim + i].im.abs() > tol {
690                return false;
691            }
692        }
693        true
694    }
695
696    /// Checks if the density matrix is positive semidefinite (approximately).
697    ///
698    /// Uses faer's self-adjoint eigenvalue decomposition to check that
699    /// all eigenvalues satisfy λᵢ ≥ -tol.
700    ///
701    /// For Hermitian matrices the eigenvalue check is definitive.
702    /// For non-Hermitian inputs the decomposition may fail or produce
703    /// meaningless results, in which case `false` is returned.
704    ///
705    /// # Edge cases
706    /// - Matrices containing NaN or Inf return `false` without panicking.
707    /// - Negative eigenvalues smaller in magnitude than `tol` are treated
708    ///   as numerical noise and pass the check.
709    ///
710    /// # Arguments
711    /// * `tol` - Tolerance for floating-point comparison.
712    ///
713    /// # Returns
714    /// `true` if all eigenvalues are ≥ -tol, `false` otherwise.
715    pub fn is_positive_semidefinite_approx(&self, tol: f64) -> bool {
716        let dim = 1 << self.num_qubits;
717
718        // Reject NaN/Inf immediately.
719        if self
720            .data
721            .iter()
722            .any(|c| c.re.is_nan() || c.re.is_infinite() || c.im.is_nan() || c.im.is_infinite())
723        {
724            return false;
725        }
726
727        if !self.is_hermitian(tol) {
728            return false;
729        }
730
731        // Quick rejection: negative diagonal (necessary condition).
732        for i in 0..dim {
733            if self.data[i * dim + i].re < -tol {
734                return false;
735            }
736        }
737
738        // Build faer matrix and compute self-adjoint eigenvalues.
739        let mat = faer::Mat::from_fn(dim, dim, |row, col| {
740            let c = self.data[row * dim + col];
741            faer::c64::new(c.re, c.im)
742        });
743
744        match mat.self_adjoint_eigenvalues(faer::Side::Lower) {
745            Ok(eigenvalues) => eigenvalues.iter().all(|&lambda| lambda >= -tol),
746            Err(_) => false,
747        }
748    }
749
750    /// Validates all physical constraints of the density matrix.
751    ///
752    /// Checks:
753    /// 1. Hermiticity: ρ = ρ†
754    /// 2. Positive semidefiniteness: All eigenvalues >= 0
755    /// 3. Unit trace: Tr(ρ) = 1
756    ///
757    /// # Arguments
758    /// * `tol` - Tolerance for floating-point comparisons (e.g., 1e-10).
759    ///
760    /// # Returns
761    /// * `Ok(())` if all constraints are satisfied.
762    /// * `Err(QisError::NotHermitian)` if not Hermitian.
763    /// * `Err(QisError::NotPositiveSemidefinite)` if not positive semidefinite.
764    /// * `Err(QisError::NotNormalized)` if trace is not 1.
765    pub fn validate_physical(&self, tol: f64) -> Result<(), QisError> {
766        // Check Hermiticity
767        if !self.is_hermitian(tol) {
768            return Err(QisError::NotHermitian);
769        }
770
771        // Check positive semidefiniteness
772        if !self.is_positive_semidefinite_approx(tol) {
773            return Err(QisError::NotPositiveSemidefinite);
774        }
775
776        // Check unit trace
777        let tr = self.trace();
778        if (tr.re - 1.0).abs() >= tol || tr.im.abs() >= tol {
779            return Err(QisError::NotNormalized);
780        }
781
782        Ok(())
783    }
784
785    fn apply_x_kernel(&mut self, q: usize, off: usize) {
786        let d = 1 << (q + off);
787        let kernel = |chunk: &mut [Complex64]| {
788            let (l, u) = chunk.split_at_mut(d);
789            l.iter_mut()
790                .zip(u.iter_mut())
791                .for_each(|(a, b)| std::mem::swap(a, b));
792        };
793        with_maybe_par!(self.num_qubits, self.data, 2 * d, kernel);
794    }
795
796    fn apply_y_kernel(&mut self, q: usize, off: usize, conj: bool) {
797        let d = 1 << (q + off);
798        let kernel = |chunk: &mut [Complex64]| {
799            let (l, u) = chunk.split_at_mut(d);
800            l.iter_mut().zip(u.iter_mut()).for_each(|(alpha, beta)| {
801                let a = *alpha;
802                let b = *beta;
803                if conj {
804                    *alpha = Complex64::new(-b.im, b.re);
805                    *beta = Complex64::new(a.im, -a.re);
806                } else {
807                    *alpha = Complex64::new(b.im, -b.re);
808                    *beta = Complex64::new(-a.im, a.re);
809                }
810            });
811        };
812        with_maybe_par!(self.num_qubits, self.data, 2 * d, kernel);
813    }
814
815    fn apply_z_kernel(&mut self, q: usize, off: usize) {
816        let d = 1 << (q + off);
817        let kernel = |chunk: &mut [Complex64]| {
818            u_slice(chunk, d).iter_mut().for_each(|v| *v = -*v);
819        };
820        with_maybe_par!(self.num_qubits, self.data, 2 * d, kernel);
821    }
822
823    fn apply_h_kernel(&mut self, q: usize, off: usize) {
824        let d = 1 << (q + off);
825        let k = FRAC_1_SQRT_2;
826        let kernel = |chunk: &mut [Complex64]| {
827            let (l, u) = chunk.split_at_mut(d);
828            l.iter_mut().zip(u.iter_mut()).for_each(|(a, b)| {
829                let (v0, v1) = (*a, *b);
830                *a = k * (v0 + v1);
831                *b = k * (v0 - v1);
832            });
833        };
834        with_maybe_par!(self.num_qubits, self.data, 2 * d, kernel);
835    }
836
837    fn apply_rx_kernel(&mut self, q: usize, off: usize, theta: f64) {
838        let d = 1 << (q + off);
839        let (c, s) = ((theta * 0.5).cos(), (theta * 0.5).sin());
840        let kernel = |chunk: &mut [Complex64]| {
841            let (l, u) = chunk.split_at_mut(d);
842            l.iter_mut().zip(u.iter_mut()).for_each(|(alpha, beta)| {
843                let (a, b) = (*alpha, *beta);
844                *alpha = Complex64::new(a.re * c + b.im * s, a.im * c - b.re * s);
845                *beta = Complex64::new(b.re * c + a.im * s, b.im * c - a.re * s);
846            });
847        };
848        with_maybe_par!(self.num_qubits, self.data, 2 * d, kernel);
849    }
850
851    fn apply_ry_kernel(&mut self, q: usize, off: usize, theta: f64) {
852        let d = 1 << (q + off);
853        let (c, s) = ((theta * 0.5).cos(), (theta * 0.5).sin());
854        let kernel = |chunk: &mut [Complex64]| {
855            let (l, u) = chunk.split_at_mut(d);
856            l.iter_mut().zip(u.iter_mut()).for_each(|(a, b)| {
857                let (v0, v1) = (*a, *b);
858                *a = v0 * c - v1 * s;
859                *b = v0 * s + v1 * c;
860            });
861        };
862        with_maybe_par!(self.num_qubits, self.data, 2 * d, kernel);
863    }
864
865    fn apply_u_kernel(&mut self, q: usize, off: usize, theta: f64, phi: f64, lam: f64, conj: bool) {
866        let d = 1 << (q + off);
867        let (c, s) = ((theta * 0.5).cos(), (theta * 0.5).sin());
868        let (p_phi, p_lam, p_tot) = if conj {
869            (
870                Complex64::from_polar(1.0, -phi),
871                Complex64::from_polar(1.0, -lam),
872                Complex64::from_polar(1.0, -(phi + lam)),
873            )
874        } else {
875            (
876                Complex64::from_polar(1.0, phi),
877                Complex64::from_polar(1.0, lam),
878                Complex64::from_polar(1.0, phi + lam),
879            )
880        };
881        let (u00, u01, u10, u11) = (Complex64::new(c, 0.0), -p_lam * s, p_phi * s, p_tot * c);
882        let kernel = |chunk: &mut [Complex64]| {
883            let (l, u) = chunk.split_at_mut(d);
884            l.iter_mut().zip(u.iter_mut()).for_each(|(a, b)| {
885                let (v0, v1) = (*a, *b);
886                *a = u00 * v0 + u01 * v1;
887                *b = u10 * v0 + u11 * v1;
888            });
889        };
890        with_maybe_par!(self.num_qubits, self.data, 2 * d, kernel);
891    }
892
893    fn apply_cx_kernel(&mut self, ctrl: usize, tgt: usize, off: usize) {
894        let (c, t) = (ctrl + off, tgt + off);
895        let (q_min, q_max) = if c < t { (c, t) } else { (t, c) };
896        let (d_min, d_max) = (1 << q_min, 1 << q_max);
897        let kernel = |chunk: &mut [Complex64]| {
898            let (p0, p1) = chunk.split_at_mut(d_max);
899            p0.chunks_exact_mut(2 * d_min)
900                .zip(p1.chunks_exact_mut(2 * d_min))
901                .for_each(|(s0, s1)| {
902                    if c < t {
903                        let (_, v01) = s0.split_at_mut(d_min);
904                        let (_, v11) = s1.split_at_mut(d_min);
905                        v01.iter_mut()
906                            .zip(v11.iter_mut())
907                            .for_each(|(a, b)| std::mem::swap(a, b));
908                    } else {
909                        let (v10, v11) = s1.split_at_mut(d_min);
910                        v10.iter_mut()
911                            .zip(v11.iter_mut())
912                            .for_each(|(a, b)| std::mem::swap(a, b));
913                    }
914                });
915        };
916        with_maybe_par!(self.num_qubits, self.data, 2 * d_max, kernel);
917    }
918
919    fn apply_cz_kernel(&mut self, q0: usize, q1: usize, off: usize) {
920        let (c0, c1) = (q0 + off, q1 + off);
921        let (q_min, q_max) = if c0 < c1 { (c0, c1) } else { (c1, c0) };
922        let (d_min, d_max) = (1 << q_min, 1 << q_max);
923        let kernel = |chunk: &mut [Complex64]| {
924            let (_, p1) = chunk.split_at_mut(d_max);
925            p1.chunks_exact_mut(2 * d_min).for_each(|s| {
926                u_slice(s, d_min).iter_mut().for_each(|v| *v = -*v);
927            });
928        };
929        with_maybe_par!(self.num_qubits, self.data, 2 * d_max, kernel);
930    }
931
932    /// Applies the Pauli-X (NOT) gate to the specified qubit.
933    ///
934    /// # Errors
935    /// Returns `QisError::IndexOutOfBounds` if qubit >= num_qubits.
936    pub fn apply_x(&mut self, qubit: usize) -> Result<(), QisError> {
937        self.validate_qubit(qubit)?;
938        self.apply_x_kernel(qubit, self.num_qubits);
939        self.apply_x_kernel(qubit, 0);
940        Ok(())
941    }
942    /// Applies the Pauli-Y gate to the specified qubit.
943    pub fn apply_y(&mut self, qubit: usize) -> Result<(), QisError> {
944        self.validate_qubit(qubit)?;
945        self.apply_y_kernel(qubit, self.num_qubits, false);
946        self.apply_y_kernel(qubit, 0, true);
947        Ok(())
948    }
949    /// Applies the Pauli-Z gate to the specified qubit.
950    pub fn apply_z(&mut self, qubit: usize) -> Result<(), QisError> {
951        self.validate_qubit(qubit)?;
952        self.apply_z_kernel(qubit, self.num_qubits);
953        self.apply_z_kernel(qubit, 0);
954        Ok(())
955    }
956    /// Applies the Hadamard gate to the specified qubit.
957    pub fn apply_h(&mut self, qubit: usize) -> Result<(), QisError> {
958        self.validate_qubit(qubit)?;
959        self.apply_h_kernel(qubit, self.num_qubits);
960        self.apply_h_kernel(qubit, 0);
961        Ok(())
962    }
963    /// Applies the generic single-qubit U gate with parameters `theta`, `phi`, and `lam`.
964    pub fn apply_u(
965        &mut self,
966        qubit: usize,
967        theta: f64,
968        phi: f64,
969        lambda: f64,
970    ) -> Result<(), QisError> {
971        self.validate_qubit(qubit)?;
972        self.apply_u_kernel(qubit, self.num_qubits, theta, phi, lambda, false);
973        self.apply_u_kernel(qubit, 0, theta, phi, lambda, true);
974        Ok(())
975    }
976    /// Applies the single-qubit rotation about the X-axis by angle `theta`.
977    pub fn apply_rx(&mut self, qubit: usize, theta: f64) -> Result<(), QisError> {
978        self.validate_qubit(qubit)?;
979        self.apply_rx_kernel(qubit, self.num_qubits, theta);
980        self.apply_rx_kernel(qubit, 0, -theta);
981        Ok(())
982    }
983    /// Applies the single-qubit rotation about the Y-axis by angle `theta`.
984    pub fn apply_ry(&mut self, qubit: usize, theta: f64) -> Result<(), QisError> {
985        self.validate_qubit(qubit)?;
986        self.apply_ry_kernel(qubit, self.num_qubits, theta);
987        self.apply_ry_kernel(qubit, 0, theta);
988        Ok(())
989    }
990    /// Applies the single-qubit rotation about the Z-axis by angle `theta`.
991    pub fn apply_rz(&mut self, qubit: usize, theta: f64) -> Result<(), QisError> {
992        self.apply_phase(qubit, theta)?; /* RZ is equivalent to Phase gate in density matrix */
993        Ok(())
994    }
995    /// Applies a phase shift of `theta` to the specified qubit.
996    pub fn apply_phase(&mut self, qubit: usize, theta: f64) -> Result<(), QisError> {
997        self.validate_qubit(qubit)?;
998        self.apply_p_kernel(qubit, self.num_qubits, theta);
999        self.apply_p_kernel(qubit, 0, -theta);
1000        Ok(())
1001    }
1002
1003    /// Applies the S (Phase) gate to the specified qubit.
1004    pub fn apply_s(&mut self, qubit: usize) -> Result<(), QisError> {
1005        self.apply_phase(qubit, FRAC_PI_2)?;
1006        Ok(())
1007    }
1008    /// Applies the inverse S (SDG) gate to the specified qubit.
1009    pub fn apply_sdg(&mut self, qubit: usize) -> Result<(), QisError> {
1010        self.apply_phase(qubit, -FRAC_PI_2)?;
1011        Ok(())
1012    }
1013    /// Applies the T (Pi/8) gate to the specified qubit.
1014    pub fn apply_t(&mut self, qubit: usize) -> Result<(), QisError> {
1015        self.apply_phase(qubit, FRAC_PI_4)?;
1016        Ok(())
1017    }
1018    /// Applies the inverse T (TDG) gate to the specified qubit.
1019    pub fn apply_tdg(&mut self, qubit: usize) -> Result<(), QisError> {
1020        self.apply_phase(qubit, -FRAC_PI_4)?;
1021        Ok(())
1022    }
1023    /// Applies a +Pi/2 rotation about the X-axis.
1024    pub fn apply_x2p(&mut self, qubit: usize) -> Result<(), QisError> {
1025        self.apply_rx(qubit, FRAC_PI_2)?;
1026        Ok(())
1027    }
1028    /// Applies a -Pi/2 rotation about the X-axis.
1029    pub fn apply_x2m(&mut self, qubit: usize) -> Result<(), QisError> {
1030        self.apply_rx(qubit, -FRAC_PI_2)?;
1031        Ok(())
1032    }
1033    /// Applies a +Pi/2 rotation about the Y-axis.
1034    pub fn apply_y2p(&mut self, qubit: usize) -> Result<(), QisError> {
1035        self.apply_ry(qubit, FRAC_PI_2)?;
1036        Ok(())
1037    }
1038    /// Applies a -Pi/2 rotation about the Y-axis.
1039    pub fn apply_y2m(&mut self, qubit: usize) -> Result<(), QisError> {
1040        self.apply_ry(qubit, -FRAC_PI_2)?;
1041        Ok(())
1042    }
1043    /// Applies the parameterized RXY rotation gate.
1044    pub fn apply_rxy(&mut self, qubit: usize, theta: f64, phi: f64) -> Result<(), QisError> {
1045        self.apply_u(qubit, theta, phi - FRAC_PI_2, FRAC_PI_2 - phi)?;
1046        Ok(())
1047    }
1048    /// Applies the XY2P gate.
1049    pub fn apply_xy2p(&mut self, qubit: usize, theta: f64) -> Result<(), QisError> {
1050        self.apply_rxy(qubit, FRAC_PI_2, theta)?;
1051        Ok(())
1052    }
1053    /// Applies the XY2M gate.
1054    pub fn apply_xy2m(&mut self, qubit: usize, theta: f64) -> Result<(), QisError> {
1055        self.apply_rxy(qubit, -FRAC_PI_2, theta)?;
1056        Ok(())
1057    }
1058    /// Applies the XY gate.
1059    ///
1060    /// Matrix: [[0, -i·e^(-iθ)], [-i·e^(iθ), 0]]
1061    pub fn apply_xy(&mut self, qubit: usize, theta: f64) -> Result<(), QisError> {
1062        self.validate_qubit(qubit)?;
1063        let mat = StandardGate::XY.matrix(&[theta]).unwrap();
1064        self.apply_single_qubit_gate(
1065            qubit,
1066            [[mat[[0, 0]], mat[[0, 1]]], [mat[[1, 0]], mat[[1, 1]]]],
1067        )?;
1068        Ok(())
1069    }
1070    /// Applies a global phase (has no observable effect on a density matrix).
1071    pub fn apply_gphase(&mut self, _phi: f64) { /* Global phase has no effect on density matrix */
1072    }
1073    fn apply_p_kernel(&mut self, q: usize, off: usize, t: f64) {
1074        let d = 1 << (q + off);
1075        let phase = Complex64::from_polar(1.0, t);
1076        let kernel = |chunk: &mut [Complex64]| {
1077            u_slice(chunk, d).iter_mut().for_each(|v| *v *= phase);
1078        };
1079        with_maybe_par!(self.num_qubits, self.data, 2 * d, kernel);
1080    }
1081
1082    /// Applies the Controlled-X (CNOT) gate.
1083    pub fn apply_cx(&mut self, control: usize, target: usize) -> Result<(), QisError> {
1084        self.validate_two_qubits(control, target)?;
1085        self.apply_cx_kernel(control, target, self.num_qubits);
1086        self.apply_cx_kernel(control, target, 0);
1087        Ok(())
1088    }
1089    /// Applies the Controlled-Z gate.
1090    pub fn apply_cz(&mut self, q0: usize, q1: usize) -> Result<(), QisError> {
1091        self.validate_two_qubits(q0, q1)?;
1092        self.apply_cz_kernel(q0, q1, self.num_qubits);
1093        self.apply_cz_kernel(q0, q1, 0);
1094        Ok(())
1095    }
1096
1097    /// Applies an arbitrary 2x2 unitary matrix to a single qubit.
1098    pub fn apply_single_qubit_gate(
1099        &mut self,
1100        qubit: usize,
1101        matrix: [[Complex64; 2]; 2],
1102    ) -> Result<(), QisError> {
1103        self.validate_qubit(qubit)?;
1104        let flat = vec![matrix[0][0], matrix[0][1], matrix[1][0], matrix[1][1]];
1105        self.apply_matrix_kernel(&[qubit], self.num_qubits, &flat, false);
1106        self.apply_matrix_kernel(&[qubit], 0, &flat, true);
1107        Ok(())
1108    }
1109
1110    /// Applies an arbitrary 4x4 unitary matrix to two qubits.
1111    pub fn apply_two_qubit_gate(
1112        &mut self,
1113        q0: usize,
1114        q1: usize,
1115        matrix: [[Complex64; 4]; 4],
1116    ) -> Result<(), QisError> {
1117        self.validate_two_qubits(q0, q1)?;
1118        let mut flat = Vec::with_capacity(16);
1119        for row in &matrix {
1120            for &val in row {
1121                flat.push(val);
1122            }
1123        }
1124        self.apply_matrix_kernel(&[q0, q1], self.num_qubits, &flat, false);
1125        self.apply_matrix_kernel(&[q0, q1], 0, &flat, true);
1126        Ok(())
1127    }
1128
1129    /// Applies the SWAP gate between two qubits.
1130    pub fn apply_swap(&mut self, q0: usize, q1: usize) -> Result<(), QisError> {
1131        self.validate_two_qubits(q0, q1)?;
1132        let mat = StandardGate::SWAP.matrix(&[]).unwrap();
1133        self.apply_unitary_gate(&[q0, q1], &mat)?;
1134        Ok(())
1135    }
1136
1137    /// Applies the Controlled-Y gate.
1138    pub fn apply_cy(&mut self, control: usize, target: usize) -> Result<(), QisError> {
1139        self.validate_two_qubits(control, target)?;
1140        let mat = StandardGate::CY.matrix(&[]).unwrap();
1141        self.apply_unitary_gate(&[control, target], &mat)?;
1142        Ok(())
1143    }
1144
1145    /// Applies the Toffoli (Controlled-Controlled-X) gate.
1146    pub fn apply_ccx(&mut self, c0: usize, c1: usize, target: usize) -> Result<(), QisError> {
1147        self.validate_qubits(&[c0, c1, target])?;
1148        let mat = StandardGate::CCX.matrix(&[]).unwrap();
1149        self.apply_unitary_gate(&[c0, c1, target], &mat)?;
1150        Ok(())
1151    }
1152
1153    /// Applies the parameterized RXX (Ising XX) gate.
1154    pub fn apply_rxx(&mut self, q0: usize, q1: usize, theta: f64) -> Result<(), QisError> {
1155        self.validate_two_qubits(q0, q1)?;
1156        let mat = StandardGate::RXX.matrix(&[theta]).unwrap();
1157        self.apply_unitary_gate(&[q0, q1], &mat)?;
1158        Ok(())
1159    }
1160
1161    /// Applies the parameterized RYY (Ising YY) gate.
1162    pub fn apply_ryy(&mut self, q0: usize, q1: usize, theta: f64) -> Result<(), QisError> {
1163        self.validate_two_qubits(q0, q1)?;
1164        let mat = StandardGate::RYY.matrix(&[theta]).unwrap();
1165        self.apply_unitary_gate(&[q0, q1], &mat)?;
1166        Ok(())
1167    }
1168
1169    /// Applies the parameterized RZZ (Ising ZZ) gate.
1170    pub fn apply_rzz(&mut self, q0: usize, q1: usize, theta: f64) -> Result<(), QisError> {
1171        self.validate_two_qubits(q0, q1)?;
1172        let mat = StandardGate::RZZ.matrix(&[theta]).unwrap();
1173        self.apply_unitary_gate(&[q0, q1], &mat)?;
1174        Ok(())
1175    }
1176
1177    /// Applies the parameterized RZX gate.
1178    pub fn apply_rzx(&mut self, q0: usize, q1: usize, theta: f64) -> Result<(), QisError> {
1179        self.validate_two_qubits(q0, q1)?;
1180        let mat = StandardGate::RZX.matrix(&[theta]).unwrap();
1181        self.apply_unitary_gate(&[q0, q1], &mat)?;
1182        Ok(())
1183    }
1184
1185    /// Applies the Controlled-RX gate.
1186    pub fn apply_crx(&mut self, control: usize, target: usize, theta: f64) -> Result<(), QisError> {
1187        self.validate_two_qubits(control, target)?;
1188        let mat = StandardGate::CRX.matrix(&[theta]).unwrap();
1189        self.apply_unitary_gate(&[control, target], &mat)?;
1190        Ok(())
1191    }
1192
1193    /// Applies the Controlled-RY gate.
1194    pub fn apply_cry(&mut self, control: usize, target: usize, theta: f64) -> Result<(), QisError> {
1195        self.validate_two_qubits(control, target)?;
1196        let mat = StandardGate::CRY.matrix(&[theta]).unwrap();
1197        self.apply_unitary_gate(&[control, target], &mat)?;
1198        Ok(())
1199    }
1200
1201    /// Applies the Controlled-RZ gate.
1202    pub fn apply_crz(&mut self, control: usize, target: usize, theta: f64) -> Result<(), QisError> {
1203        self.validate_two_qubits(control, target)?;
1204        let mat = StandardGate::CRZ.matrix(&[theta]).unwrap();
1205        self.apply_unitary_gate(&[control, target], &mat)?;
1206        Ok(())
1207    }
1208
1209    /// Applies the Fermionic Simulation (FSIM) gate.
1210    pub fn apply_fsim(
1211        &mut self,
1212        q0: usize,
1213        q1: usize,
1214        theta: f64,
1215        phi: f64,
1216    ) -> Result<(), QisError> {
1217        self.validate_two_qubits(q0, q1)?;
1218        let mat = StandardGate::FSIM.matrix(&[theta, phi]).unwrap();
1219        self.apply_unitary_gate(&[q0, q1], &mat)?;
1220        Ok(())
1221    }
1222
1223    /// Applies an arbitrary n-qubit unitary gate.
1224    ///
1225    /// The evolution is given by $\rho \to U \rho U^\dagger$.
1226    ///
1227    /// # Arguments
1228    /// * `qs` - Slice of qubit indices the gate acts on.
1229    /// * `mat` - The unitary matrix as a $2^n \times 2^n$ `ndarray`.
1230    pub fn apply_unitary_gate(
1231        &mut self,
1232        qubits: &[usize],
1233        matrix: &ndarray::Array2<Complex64>,
1234    ) -> Result<(), QisError> {
1235        let num_target_qubits = qubits.len();
1236        let gate_dim = 1 << num_target_qubits;
1237
1238        // Validate number of target qubits
1239        if num_target_qubits == 0 || num_target_qubits > self.num_qubits {
1240            return Err(QisError::InvalidParameterValue(format!(
1241                "Invalid number of target qubits: {} (must be 1 to {})",
1242                num_target_qubits, self.num_qubits
1243            )));
1244        }
1245
1246        // Validate qubits and check for duplicates
1247        for (i, &q) in qubits.iter().enumerate() {
1248            if q >= self.num_qubits {
1249                return Err(QisError::IndexOutOfBounds {
1250                    index: q,
1251                    max: self.num_qubits.saturating_sub(1),
1252                });
1253            }
1254            for &other in &qubits[..i] {
1255                if q == other {
1256                    return Err(QisError::InvalidParameterValue(format!(
1257                        "Duplicate qubit index {} in apply_unitary_gate",
1258                        q
1259                    )));
1260                }
1261            }
1262        }
1263
1264        // Validate matrix dimensions
1265        if matrix.shape() != [gate_dim, gate_dim] {
1266            return Err(QisError::InvalidParameterValue(format!(
1267                "Matrix dimensions {}x{} don't match expected {}x{} for {} qubits",
1268                matrix.nrows(),
1269                matrix.ncols(),
1270                gate_dim,
1271                gate_dim,
1272                num_target_qubits
1273            )));
1274        }
1275
1276        let flat: Vec<Complex64> = matrix.iter().cloned().collect();
1277        self.apply_matrix_kernel(qubits, self.num_qubits, &flat, false);
1278        self.apply_matrix_kernel(qubits, 0, &flat, true);
1279        Ok(())
1280    }
1281
1282    fn apply_matrix_kernel(&mut self, qs: &[usize], off: usize, mat: &[Complex64], conj: bool) {
1283        Self::apply_matrix_kernel_impl(&mut self.data, self.num_qubits, qs, off, mat, conj);
1284    }
1285
1286    fn apply_matrix_kernel_impl(
1287        data: &mut [Complex64],
1288        num_qubits: usize,
1289        qs: &[usize],
1290        off: usize,
1291        mat: &[Complex64],
1292        conj: bool,
1293    ) {
1294        if qs.is_empty() {
1295            return;
1296        }
1297        let n = qs.len();
1298        let dim = 1 << n;
1299        let mut offsets: SmallVec<[usize; 16]> = smallvec![0; dim];
1300        for i in 0..dim {
1301            for (j, &q) in qs.iter().enumerate() {
1302                if (i >> (n - 1 - j)) & 1 == 1 {
1303                    offsets[i] |= 1 << (q + off);
1304                }
1305            }
1306        }
1307        let mut sorted: SmallVec<[usize; 16]> = qs.iter().map(|&q| q + off).collect();
1308        sorted.sort_unstable();
1309        let chunk_size = 1 << (sorted.last().unwrap() + 1);
1310
1311        let kernel = |chunk: &mut [Complex64]| {
1312            let mut i_buf: SmallVec<[Complex64; 16]> = smallvec![Complex64::default(); dim];
1313            let mut o_buf: SmallVec<[Complex64; 16]> = smallvec![Complex64::default(); dim];
1314            for b_idx in 0..(chunk.len() >> n) {
1315                let mut base = b_idx;
1316                for &q in &sorted {
1317                    let mask = (1 << q) - 1;
1318                    base = ((base & !mask) << 1) | (base & mask);
1319                }
1320                for i in 0..dim {
1321                    i_buf[i] = chunk[base + offsets[i]];
1322                }
1323                for r in 0..dim {
1324                    let mut sum = Complex64::default();
1325                    for c in 0..dim {
1326                        let val = mat[r * dim + c];
1327                        let coeff = if conj { val.conj() } else { val };
1328                        sum += coeff * i_buf[c];
1329                    }
1330                    o_buf[r] = sum;
1331                }
1332                for i in 0..dim {
1333                    chunk[base + offsets[i]] = o_buf[i];
1334                }
1335            }
1336        };
1337        with_maybe_par!(num_qubits, data, chunk_size, kernel);
1338    }
1339
1340    /// Applies a general quantum channel specified by Kraus operators.
1341    ///
1342    /// The evolution of the density matrix is given by $\rho \to \sum_k K_k \rho K_k^\dagger$,
1343    /// where $\sum_k K_k^\dagger K_k = I$ for a trace-preserving channel.
1344    ///
1345    /// # Arguments
1346    /// * `ops` - A slice of Kraus operators, where each operator is represented as a flattened vector of `Complex64`.
1347    /// * `qs` - The target qubit indices the channel acts upon.
1348    pub fn apply_kraus(&mut self, ops: &[Vec<Complex64>], qs: &[usize]) -> Result<(), QisError> {
1349        if ops.is_empty() {
1350            return Err(QisError::InvalidParameterValue(
1351                "Kraus operator list must not be empty".to_string(),
1352            ));
1353        }
1354        if qs.is_empty() {
1355            return Err(QisError::InvalidParameterValue(
1356                "Kraus channel must target at least one qubit".to_string(),
1357            ));
1358        }
1359        self.validate_qubits(qs)?;
1360        let op_dim = 1usize << qs.len();
1361        let expected_len = op_dim * op_dim;
1362        for (idx, op) in ops.iter().enumerate() {
1363            if op.len() != expected_len {
1364                return Err(QisError::InvalidParameterValue(format!(
1365                    "Kraus operator {} has length {}, expected {} for {} target qubits",
1366                    idx,
1367                    op.len(),
1368                    expected_len,
1369                    qs.len()
1370                )));
1371            }
1372        }
1373        let source_data = self.data.clone();
1374
1375        for val in self.data.iter_mut() {
1376            *val = Complex64::default();
1377        }
1378
1379        let mut work_buffer = vec![Complex64::default(); self.data.len()];
1380
1381        for op in ops {
1382            work_buffer.copy_from_slice(&source_data);
1383
1384            Self::apply_matrix_kernel_impl(
1385                &mut work_buffer,
1386                self.num_qubits,
1387                qs,
1388                self.num_qubits,
1389                op,
1390                false,
1391            );
1392            Self::apply_matrix_kernel_impl(&mut work_buffer, self.num_qubits, qs, 0, op, true);
1393
1394            if self.num_qubits < PARALLEL_THRESHOLD {
1395                for (acc, val) in self.data.iter_mut().zip(&work_buffer) {
1396                    *acc += val;
1397                }
1398            } else {
1399                use rayon::prelude::*;
1400                self.data
1401                    .par_iter_mut()
1402                    .zip(work_buffer.par_iter())
1403                    .for_each(|(acc, val)| {
1404                        *acc += val;
1405                    });
1406            }
1407        }
1408
1409        #[cfg(debug_assertions)]
1410        {
1411            let tr = self.trace();
1412            debug_assert!((tr.re - 1.0).abs() < 1e-10, "Trace error: {}", tr);
1413        }
1414        Ok(())
1415    }
1416
1417    /// Computes the partial trace over a set of qubits.
1418    ///
1419    /// Reduces the N-qubit system to a smaller subsystem containing only the specified `keep` qubits
1420    /// by tracing out all other qubits.
1421    ///
1422    /// # Arguments
1423    /// * `keep` - A slice of qubit indices to keep in the resulting reduced density matrix.
1424    ///
1425    /// # Returns
1426    /// A new `DensityMatrix` representing the subsystem, with `num_qubits = keep.len()`.
1427    pub fn partial_trace(&self, keep: &[usize]) -> Result<Self, QisError> {
1428        for (idx, &q) in keep.iter().enumerate() {
1429            if q >= self.num_qubits {
1430                return Err(QisError::IndexOutOfBounds {
1431                    index: q,
1432                    max: self.num_qubits.saturating_sub(1),
1433                });
1434            }
1435            if keep[..idx].contains(&q) {
1436                return Err(QisError::InvalidParameterValue(format!(
1437                    "Duplicate qubit index {} in partial_trace",
1438                    q
1439                )));
1440            }
1441        }
1442        let mut s_keep = keep.to_vec();
1443        s_keep.sort_unstable();
1444        let all: HashSet<_> = (0..self.num_qubits).collect();
1445        let mut trace: Vec<_> = all
1446            .difference(&s_keep.iter().cloned().collect())
1447            .cloned()
1448            .collect();
1449        trace.sort_unstable();
1450        let (n_k, n_t) = (s_keep.len(), trace.len());
1451        let (dim, r_dim) = (1 << self.num_qubits, 1 << n_k);
1452        let mut res = Self::zeros(n_k);
1453        res.qubit_map = self.qubit_map.as_ref().map(|qubit_map| {
1454            std::sync::Arc::new(
1455                qubit_map
1456                    .iter()
1457                    .filter_map(|(&qubit, position)| {
1458                        s_keep
1459                            .binary_search(position)
1460                            .ok()
1461                            .map(|index| (qubit, index))
1462                    })
1463                    .collect(),
1464            )
1465        });
1466        res.data.par_iter_mut().enumerate().for_each(|(idx, val)| {
1467            let (i, j) = (idx / r_dim, idx % r_dim);
1468            let mut sum = Complex64::default();
1469            for t in 0..(1 << n_t) {
1470                sum += self.data[interleave_bits(&s_keep, i, &trace, t) * dim
1471                    + interleave_bits(&s_keep, j, &trace, t)];
1472            }
1473            *val = sum;
1474        });
1475        Ok(res)
1476    }
1477
1478    pub(crate) fn partial_transpose(&self, target_qubits: &[usize]) -> Result<Self, QisError> {
1479        for &q in target_qubits {
1480            self.validate_qubit(q)?;
1481        }
1482
1483        let n = self.num_qubits;
1484        let mut swap_mask = 0usize;
1485        for &q in target_qubits {
1486            swap_mask |= 1 << q;
1487        }
1488
1489        let mut res = Self::zeros(n);
1490        res.qubit_map.clone_from(&self.qubit_map);
1491        res.data.par_iter_mut().enumerate().for_each(|(idx, val)| {
1492            let lower_swap_bits = idx & swap_mask;
1493            let upper_swap_bits = (idx >> n) & swap_mask;
1494
1495            let mut src_idx = idx & !(swap_mask | (swap_mask << n));
1496            src_idx |= upper_swap_bits;
1497            src_idx |= lower_swap_bits << n;
1498
1499            *val = self.data[src_idx];
1500        });
1501
1502        Ok(res)
1503    }
1504
1505    /// Computes the expectation value of a Hamiltonian observable.
1506    ///
1507    /// Calculates Tr(ρ * H) for the current density matrix ρ and a given Hamiltonian H.
1508    /// The Hamiltonian is represented as a sum of Pauli strings with coefficients.
1509    ///
1510    /// # Arguments
1511    /// * `h` - The Hamiltonian observable.
1512    ///
1513    /// # Returns
1514    /// The expectation value as a real number (f64), or a [`QisError`] if the
1515    /// qubit counts do not match.
1516    ///
1517    /// # Errors
1518    /// Returns [`QisError::CircuitError`] if the observable acts on a different
1519    /// number of qubits than the density matrix.
1520    pub fn expectation(&self, h: &dyn Observable) -> Result<f64, QisError> {
1521        h.expectation_density_matrix(self)
1522    }
1523
1524    /// Copies density matrix data from `source` into `self` without allocating.
1525    ///
1526    /// Both matrices must have the same `num_qubits`. Used by
1527    /// [`sample_shots`](Self::sample_shots) to reuse each thread's working copy.
1528    pub(crate) fn reset_from(&mut self, source: &DensityMatrix) {
1529        debug_assert_eq!(self.num_qubits, source.num_qubits);
1530        self.qubit_map.clone_from(&source.qubit_map);
1531        self.data.copy_from_slice(&source.data);
1532    }
1533
1534    /// Measures `qubit` in the Z basis, collapsing the density matrix in-place.
1535    ///
1536    /// Returns `true` for outcome |1⟩ and `false` for outcome |0⟩.
1537    /// The density matrix is projected into the subspace consistent with the
1538    /// outcome and renormalized: `ρ' = Π_b ρ Π_b / Tr(Π_b ρ)`.
1539    pub fn measure(&mut self, qubit: usize) -> Result<bool, QisError> {
1540        self.measure_with_rng(qubit, &mut rand::rng())
1541    }
1542
1543    /// Measures one qubit using the supplied random stream.
1544    pub fn measure_with_rng(&mut self, qubit: usize, rng: &mut impl Rng) -> Result<bool, QisError> {
1545        self.validate_qubit(qubit)?;
1546        let dim = 1 << self.num_qubits;
1547        let mask = 1usize << qubit;
1548
1549        // P(|1⟩) = sum of diagonal elements where bit `qubit` is 1.
1550        let p1: f64 = (0..dim)
1551            .filter(|i| i & mask != 0)
1552            .map(|i| self.data[i * dim + i].re)
1553            .sum();
1554
1555        let outcome = rng.random::<f64>() < p1;
1556
1557        // Project and renormalise: zero rows/cols inconsistent with outcome.
1558        let norm = if outcome { p1 } else { 1.0 - p1 };
1559        for row in 0..dim {
1560            let row_match = if outcome {
1561                (row & mask) != 0
1562            } else {
1563                (row & mask) == 0
1564            };
1565            for col in 0..dim {
1566                let col_match = if outcome {
1567                    (col & mask) != 0
1568                } else {
1569                    (col & mask) == 0
1570                };
1571                if row_match && col_match {
1572                    self.data[row * dim + col] /= norm;
1573                } else {
1574                    self.data[row * dim + col] = Complex64::new(0.0, 0.0);
1575                }
1576            }
1577        }
1578
1579        Ok(outcome)
1580    }
1581
1582    /// Resets `qubit` to the |0⟩ state.
1583    ///
1584    /// Applies the deterministic reset channel with Kraus operators |0⟩⟨0|
1585    /// and |0⟩⟨1|. The outcome is discarded, preserving the reduced density
1586    /// matrix of the other qubits, which may be mixed for an entangled input.
1587    pub fn reset(&mut self, qubit: usize) -> Result<(), QisError> {
1588        self.validate_qubit(qubit)?;
1589        let dim = 1usize << self.num_qubits;
1590        let mask = 1usize << qubit;
1591        for row in (0..dim).filter(|row| row & mask == 0) {
1592            for col in (0..dim).filter(|col| col & mask == 0) {
1593                let i00 = row * dim + col;
1594                let i01 = row * dim + (col | mask);
1595                let i10 = (row | mask) * dim + col;
1596                let i11 = (row | mask) * dim + (col | mask);
1597                let sum = self.data[i00] + self.data[i11];
1598                self.data[i00] = sum;
1599                self.data[i01] = Complex64::new(0.0, 0.0);
1600                self.data[i10] = Complex64::new(0.0, 0.0);
1601                self.data[i11] = Complex64::new(0.0, 0.0);
1602            }
1603        }
1604        Ok(())
1605    }
1606
1607    /// Measures all qubits sequentially, returning a bit-packed [`Outcome`].
1608    ///
1609    /// Equivalent to calling `measure(q)` for each qubit `q` in order `0..num_qubits`.
1610    /// The density matrix is fully collapsed after this call.
1611    /// Use [`Outcome::is_one(q)`](crate::device::Outcome::is_one) to read qubit `q`'s result.
1612    pub fn measure_all(&mut self) -> Outcome {
1613        self.measure_all_with_rng(&mut rand::rng())
1614    }
1615
1616    /// Measures all qubits in storage order using the supplied random stream.
1617    pub fn measure_all_with_rng(&mut self, rng: &mut impl Rng) -> Outcome {
1618        let num_chunks = self.num_qubits.div_ceil(64);
1619        let mut chunks = SmallVec::from_elem(0u64, num_chunks);
1620        for q in 0..self.num_qubits {
1621            if self.measure_with_rng(q, rng).unwrap() {
1622                chunks[q / 64] |= 1u64 << (q % 64);
1623            }
1624        }
1625        Outcome::new(chunks)
1626    }
1627
1628    /// Samples `shots` independent measurement outcomes in parallel.
1629    ///
1630    /// Each Rayon worker thread reuses a single pre-allocated clone of the
1631    /// density matrix (via [`reset_from`](Self::reset_from)), avoiding per-shot
1632    /// heap allocation. Returns a [`Vec<Outcome>`] of bit-packed results.
1633    ///
1634    /// # Example
1635    /// ```rust
1636    /// use cqlib_core::qis::DensityMatrix;
1637    ///
1638    /// let mut dm = DensityMatrix::new(2);
1639    /// dm.apply_h(0).unwrap();
1640    /// dm.apply_cx(0, 1).unwrap(); // |Φ⁺⟩ Bell state
1641    ///
1642    /// let shots = dm.sample_shots(200);
1643    /// // All outcomes must be |00⟩ or |11⟩
1644    /// assert!(shots.iter().all(|v| v.is_one(0) == v.is_one(1)));
1645    /// ```
1646    pub fn sample_shots(&self, shots: usize) -> Vec<Outcome> {
1647        self.sample_shots_with_seed(shots, None)
1648    }
1649
1650    /// Samples without changing this state. An explicit seed assigns a random
1651    /// stream to each shot before parallel execution, independent of scheduling.
1652    pub fn sample_shots_with_seed(&self, shots: usize, seed: Option<u64>) -> Vec<Outcome> {
1653        if let Some(seed) = seed {
1654            let mut rng = SmallRng::seed_from_u64(seed);
1655            let seeds: Vec<u64> = (0..shots).map(|_| rng.random()).collect();
1656            return seeds
1657                .into_par_iter()
1658                .map_with(self.clone(), |work, seed| {
1659                    work.reset_from(self);
1660                    work.measure_all_with_rng(&mut SmallRng::seed_from_u64(seed))
1661                })
1662                .collect();
1663        }
1664        (0..shots)
1665            .into_par_iter()
1666            .map_with(self.clone(), |work, _| {
1667                work.reset_from(self);
1668                work.measure_all()
1669            })
1670            .collect()
1671    }
1672
1673    /// Samples the state using a circuit [`Measurement`] as the output contract.
1674    ///
1675    /// `measurement.qubits()[i]` becomes bit `i` in each [`Outcome`]. The returned
1676    /// [`ExecutionResult`] reuses the device result shape, with counts projected
1677    /// to only the qubits named by the measurement.
1678    pub fn sample(
1679        &self,
1680        measurement: &Measurement,
1681        shots: usize,
1682    ) -> Result<ExecutionResult, QisError> {
1683        self.sample_with_seed(measurement, shots, None)
1684    }
1685
1686    /// Projects seeded full-state samples onto the requested measurement.
1687    pub fn sample_with_seed(
1688        &self,
1689        measurement: &Measurement,
1690        shots: usize,
1691        seed: Option<u64>,
1692    ) -> Result<ExecutionResult, QisError> {
1693        let projection = super::resolve_measurement(measurement, &self.qubit_map, self.num_qubits)?;
1694
1695        let mut counts = HashMap::new();
1696        for full in self.sample_shots_with_seed(shots, seed) {
1697            *counts.entry(projection.project(&full)).or_insert(0usize) += 1;
1698        }
1699
1700        Ok(ExecutionResult::from_counts(
1701            "density-matrix-sample".to_string(),
1702            measurement.qubits().to_vec(),
1703            shots,
1704            measurement.width(),
1705            Some("density-matrix".to_string()),
1706            counts,
1707        ))
1708    }
1709
1710    /// Returns the marginal probability distribution selected by a circuit [`Measurement`].
1711    ///
1712    /// This method does not execute circuit IR. It treats the [`Measurement`] as
1713    /// a self-contained output contract and aggregates computational-basis
1714    /// probabilities over unmeasured qubits.
1715    pub fn probs(&self, measurement: &Measurement) -> Result<HashMap<Outcome, f64>, QisError> {
1716        let projection = super::resolve_measurement(measurement, &self.qubit_map, self.num_qubits)?;
1717
1718        let mut marginal = HashMap::new();
1719        for (basis, prob) in self.probabilities().into_iter().enumerate() {
1720            if prob == 0.0 {
1721                continue;
1722            }
1723            *marginal
1724                .entry(projection.project_basis(basis))
1725                .or_insert(0.0) += prob;
1726        }
1727        Ok(marginal)
1728    }
1729}
1730
1731// Helpers for slice splitting
1732#[inline(always)]
1733fn u_slice(s: &mut [Complex64], d: usize) -> &mut [Complex64] {
1734    s.split_at_mut(d).1
1735}
1736
1737#[cfg(test)]
1738#[path = "density_matrix_test.rs"]
1739mod density_matrix_test;