cpx_mat2by2/
lib.rs

1//! The `cpx-mat2by2` library provides structures for representing and manipulating
2//! quantum states and operators in a 2-dimensional Hilbert space (qubit).
3//! It leverages the `cpx-coords` crate for complex number arithmetic.
4
5use crate::BraKet::{Bra, Ket};
6use crate::ProNil::{Nilpotent, Projector};
7use core::cmp::{max, min};
8use core::hash::Hash;
9use core::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign};
10use cpx_coords::FloatExt;
11use cpx_coords::*;
12use std::collections::BTreeMap;
13
14/// Enum representing the side of a state vector.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub enum BraKet {
17    /// A bra vector ⟨ψ|.
18    Bra,
19    /// A ket vector |ψ⟩.
20    Ket,
21}
22
23/// Canonical normalized amplitudes representing a single-qubit pure state.
24///
25/// This form enforces uniqueness for hashing and equality by fixing the global phase:
26/// - `c0 ∈ ℝ≥0` (real and non-negative)
27/// - `|c0|² + |c1|² = 1` (normalization)
28/// - If `c0 == 0`, then `c1 == Cpx::One {}` (canonical phase)
29///
30/// This ensures a normalized state vector with a unique, phase-fixed representation.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32pub struct NormPair<T: FloatExt + Hash> {
33    /// Real, non-negative amplitude of the |0⟩ basis state.
34    pub c0: T,
35    /// Complex amplitude of the |1⟩ basis state.
36    pub c1: Cpx<T>,
37}
38
39/// A normalized single-qubit state in canonical form with orientation.
40///
41/// Wraps a `NormPair<T>` with a `BraKet` tag to indicate whether the state is a ket or bra.
42/// The state is normalized and represented uniquely up to global phase.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
44pub struct NormQubit<T: FloatExt + Hash> {
45    /// Indicates if the state is a ket or bra.
46    pub bra_ket: BraKet,
47    /// Canonical normalized amplitudes for the qubit state.
48    pub norm_pair: NormPair<T>,
49}
50
51/// A mixed single-qubit state formed by a probabilistic mixture of a normalized state and its orthogonal complement.
52///
53/// The state represents the mixture:
54/// `ρ = prob × |ψ⟩⟨ψ| + (1 - prob) × |ψ⊥⟩⟨ψ⊥|`
55///
56/// - `prob ∈ [0.5, 1.0]` is the probability weight on the primary state.
57/// - `norm_pair` defines the normalized pure state |ψ⟩ in canonical form.
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
59pub struct MixPair<T: FloatExt + Hash> {
60    /// Probability assigned to the pure state |ψ⟩ (must satisfy 0.5 ≤ prob ≤ 1.0).
61    pub prob: T,
62    /// Canonical normalized amplitudes defining the pure state |ψ⟩.
63    pub norm_pair: NormPair<T>,
64}
65
66/// A mixed single-qubit state with bra/ket orientation.
67///
68/// Wraps a `MixPair<T>` along with a `BraKet` tag to indicate state orientation of the `MixPair<T>`.
69/// Represents a probabilistic mixture of a normalized qubit and its orthogonal complement.
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
71pub struct MixQubit<T: FloatExt + Hash> {
72    /// Indicates if the `MixPair<T>` is in a ket or bra representation.
73    pub bra_ket: BraKet,
74    /// The probabilistic mixture of a pure state and its orthogonal complement.
75    pub mix_pair: MixPair<T>,
76}
77
78/// A separable multi-qubit product state defined over an index interval.
79///
80/// The state is interpreted as a tensor product of individual qubit states over a specified
81/// inclusive `interval`. Each mapped entry provides a qubit's canonical normalized state.
82/// Missing indices are filled in using the default `padding` value.
83#[derive(Debug, Clone, PartialEq, Eq, Hash)]
84pub struct IdxToNorm<T: FloatExt + Hash> {
85    /// Inclusive interval of qubit indices this product state spans: [start, end].
86    pub interval: (usize, usize),
87    /// Default qubit state used for indices in the interval not explicitly mapped.
88    pub padding: NormPair<T>,
89    /// Mapping from selected qubit indices to the normalized single-qubit states.
90    pub idx_to_norm: BTreeMap<usize, NormPair<T>>,
91}
92
93/// A separable multi-qubit product state with bra/ket orientation.
94///
95/// Encapsulates a product of single-qubit states using a canonical index-to-state mapping (`IdxToNorm<T>`),
96/// along with a `BraKet` tag indicating whether the state is a ket or bra.
97/// Assumes qubits are independent and ordered by index.
98#[derive(Debug, Clone, PartialEq, Eq, Hash)]
99pub struct SepQubits<T: FloatExt + Hash> {
100    /// Indicates if the full state is a ket or bra.
101    pub bra_ket: BraKet,
102    /// Mapping from indices to single-qubit normalized states, with padding.
103    pub idx_to_norm: IdxToNorm<T>,
104}
105
106/// A separable multi-qubit mixed state, where each qubit is independently in a mixed state.
107///
108/// Each qubit is represented by a `MixPair<T>`, describing a probabilistic mixture of a normalized
109/// pure state and its orthogonal complement. The `idx_to_mix` map assigns such states to selected
110/// qubit indices, while `padding` supplies a default value for indices not explicitly mapped.
111///
112/// Assumptions:
113/// - No entanglement between qubits (fully separable state).
114/// - Local mixing occurs independently at each qubit site.
115/// - The state is defined over the inclusive `interval` of indices, with canonical ordering.
116/// - The `bra_ket` field indicates whether this is a bra or ket state vector.
117#[derive(Debug, Clone, PartialEq, Eq, Hash)]
118pub struct IdxToMixQubits<T: FloatExt + Hash> {
119    /// Indicates if the overall `MixPair<T>`s are in a ket or bra representation.
120    pub bra_ket: BraKet,
121    /// Inclusive interval of qubit indices this product state spans: [start, end].
122    pub interval: (usize, usize),
123    /// Default mixed state used for indices in the interval not explicitly mapped.
124    pub padding: MixPair<T>,
125    /// Mapping from selected qubit indices to independent mixed single-qubit states.
126    pub idx_to_mix: BTreeMap<usize, MixPair<T>>,
127}
128
129/// Enum representing a type of rank-1 operator: either a projector or a nilpotent.
130///
131/// - `Projector` corresponds to `|ψ⟩⟨ψ|`, a Hermitian, idempotent rank-1 operator.
132/// - `Nilpotent` corresponds to either `|ψ⟩⟨ψ⊥|` or `|ψ⊥⟩⟨ψ|`.
133#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
134pub enum ProNil {
135    /// A Hermitian rank-1 projector: `|ψ⟩⟨ψ|`.
136    Projector,
137    /// A rank-1 nilpotent operator: `|ψ⟩⟨ψ⊥|` or `|ψ⊥⟩⟨ψ|`.
138    Nilpotent,
139}
140
141/// A rank-1 operator (projector or nilpotent) constructed from a normalized qubit state.
142///
143/// Combines a `NormPair<T>` with an operator type and orientation tag. The `BraKet` field determines
144/// the side (bra or ket) of the defining vector. For `Nilpotent`, the orthogonal complement is implied
145/// on the opposite side of the rank-1 outer product.
146///
147/// Examples:
148/// - `Projector` + `BraKet::Ket` with `|ψ⟩` → `|ψ⟩⟨ψ|`
149/// - `Nilpotent` + `BraKet::Bra` with `⟨ψ|` → `|ψ⊥⟩⟨ψ|`
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
151pub struct Rk1PN<T: FloatExt + Hash> {
152    /// Operator type: projector or nilpotent.
153    pub pro_nil: ProNil,
154    /// Indicates whether the vector is a bra or ket.
155    pub bra_ket: BraKet,
156    /// Canonical normalized qubit state defining the operator.
157    pub norm_pair: NormPair<T>,
158}
159
160/// A general rank-1 matrix of the form`M = scalar × |ket⟩⟨bra|`.
161///
162/// This representation explicitly separates the ket (column) and bra (row) components of the matrix.
163/// The underlying vectors are normalized and canonically phase-fixed via `NormPair<T>`.
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
165pub struct Rk1KB<T: FloatExt + Hash> {
166    /// The ket vector `|ψ⟩` defining the column part of the outer product.
167    pub ket: NormPair<T>,
168    /// The bra vector `⟨ϕ|` defining the row part of the outer product.
169    pub bra: NormPair<T>,
170}
171
172/// Enum representing a general rank-1 matrix operator.
173///
174/// This can be either a structured operator (`ProNil`) such as a projector or nilpotent,
175/// or a fully general rank-1 matrix formed from arbitrary normalized vectors.
176#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
177pub enum Rank1<T: FloatExt + Hash> {
178    /// A structured rank-1 operator: projector or nilpotent.
179    ProNil(Rk1PN<T>),
180    /// A general rank-1 matrix: `|ket⟩⟨bra|` with independent canonical vectors.
181    Other(Rk1KB<T>),
182}
183
184/// Struct representing a raw 2x2 complex matrix.
185#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
186pub struct RawMat<T: FloatExt + Hash> {
187    /// A raw 2x2 complex matrix.
188    pub mat: [[Cpx<T>; 2]; 2],
189}
190
191/// Represents a general 2×2 complex matrix using the Pauli (complex-quaternion) basis.
192///
193/// The matrix is expressed as a linear combination of Pauli matrices:
194/// `M = a₀·I + a₁·X + a₂·Y + a₃·Z`, where each `aᵢ` is a complex coefficient (`Cpx<T>`).
195///
196/// This representation is useful for compact encode general 2×2 complex operators using the Pauli basis.
197#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
198pub struct CpxQuaternion<T: FloatExt + Hash> {
199    /// Coefficients `[a₀, a₁, a₂, a₃]` corresponding to the Pauli basis `{I, X, Y, Z}`, in that order.
200    pub coefficients: [Cpx<T>; 4],
201}
202
203/// Represents a 2×2 Hermitian matrix using the Pauli (complex-quaternion) basis.
204///
205/// The matrix is expressed as a real linear combination of Pauli matrices:
206/// `M = a₀·I + a₁·X + a₂·Y + a₃·Z`, where each `aᵢ` is a real coefficient (`T`).
207///
208/// This form compactly encodes Hermitian 2×2 operators.
209#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
210pub struct RealQuaternion<T: FloatExt + Hash> {
211    /// Real coefficients `[a₀, a₁, a₂, a₃]` corresponding to the Pauli basis `{I, X, Y, Z}`, in that order.
212    pub coefficients: [T; 4],
213}
214
215/// Represents a 2×2 complex matrix with unit determinant, expressed in the Pauli (complex-quaternion) basis.
216///
217/// The matrix is written as a linear combination of Pauli matrices:
218/// `M = a₀·I + a₁·X + a₂·Y + a₃·Z`, where each `aᵢ` is a complex coefficient.
219///
220/// This struct stores only the non-identity coefficients `(a₁, a₂, a₃)`
221/// as fields `x`, `y`, and `z`, corresponding to the Pauli matrices `X`, `Y`, and `Z`.
222/// The identity coefficient `a₀` is computed implicitly to ensure `det(M) = 1`, using:
223///
224/// ```text
225/// det(M) = a₀² − a₁² − a₂² − a₃² = 1
226/// ⇒ a₀ = sqrt(1 + a₁² + a₂² + a₃²)
227/// ```
228///
229/// This compact form is well suited for representing special unitary matrices (SU(2)),
230/// rotations on the Bloch sphere, and Pauli-based gate decompositions.
231#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
232pub struct Det1CpxQuaternion<T: FloatExt + Hash> {
233    /// Coefficient of the Pauli X component.
234    pub x: Cpx<T>,
235
236    /// Coefficient of the Pauli Y component.
237    pub y: Cpx<T>,
238
239    /// Coefficient of the Pauli Z component.
240    pub z: Cpx<T>,
241}
242
243/// Represents a 2×2 Hermitian matrix with unit determinant, expressed in the Pauli (real-quaternion) basis.
244///
245/// As with the complex case, the matrix is expressed as:
246/// `M = a₀·I + a₁·X + a₂·Y + a₃·Z`, but each `aᵢ` is real, and the identity component
247/// `a₀` is computed implicitly to satisfy the determinant constraint:
248///
249/// ```text
250/// det(M) = a₀² − a₁² − a₂² − a₃² = 1
251/// ⇒ a₀ = sqrt(1 + a₁² + a₂² + a₃²)
252/// ```
253///
254/// This form is useful for encoding Hermitian, unit-determinant operators, such as traceless generators
255/// of real-valued quantum dynamics or normalized observables.
256#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
257pub struct Det1RealQuaternion<T: FloatExt + Hash> {
258    /// Coefficient of the Pauli X component.
259    pub x: T,
260
261    /// Coefficient of the Pauli Y component.
262    pub y: T,
263
264    /// Coefficient of the Pauli Z component.
265    pub z: T,
266}
267
268/// Represents a 2×2 complex matrix with unit determinant, expressed in the Pauli basis.
269///
270/// This enum distinguishes between:
271/// - `Hermitian`: real-valued Pauli coefficients (used for Hermitian unit-determinant matrices),
272/// - `Other`: complex-valued Pauli coefficients (for general SU(2) matrices).
273///
274/// Both variants implicitly encode the identity component `a₀` to satisfy `det(M) = 1`.
275#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
276pub enum Det1<T: FloatExt + Hash> {
277    /// A Hermitian matrix with real-valued Pauli components.
278    Hermitian(Det1RealQuaternion<T>),
279
280    /// A general complex matrix with potentially non-Hermitian structure.
281    Other(Det1CpxQuaternion<T>),
282}
283
284/// Represents a 2×2 complex matrix of rank 1 or 2, in a compact structured form.
285#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
286pub enum RankOneTwo<T: FloatExt + Hash> {
287    /// A rank-1 matrix of the form`M = |ket⟩⟨bra|`.
288    ///
289    /// This variant encodes a matrix as the outer product of a normalized ket and bra.
290    Rank1(Rank1<T>),
291
292    /// A full-rank 2×2 complex matrix with unit determinant.
293    ///
294    /// The matrix is expressed in the Pauli (quaternion) basis, and may be Hermitian or non-Hermitian.
295    Rank2(Det1<T>),
296}
297
298/// Represents a tensor product of local 2×2 complex operators acting on specific qubit indices.
299///
300/// This structure encodes a sparse tensor product of single-qubit operators. Each mapped entry
301/// specifies a `RankOneTwo<T>` acting on a particular qubit index. The full operator is
302/// constructed as an ordered tensor product over a specified interval of qubit indices. Indices
303/// not explicitly mapped use the provided `padding` operator.
304///
305/// This is useful for compactly representing structured operators such as Kraus operators.
306#[derive(Debug, Clone, PartialEq, Eq, Hash)]
307pub struct LocalOps<T: FloatExt + Hash> {
308    /// Inclusive interval of qubit indices spanned by the tensor product: `[start, end]`.
309    pub interval: (usize, usize),
310
311    /// Default `RankOneTwo<T>` used for unmapped qubit indices within the interval.
312    pub padding: RankOneTwo<T>,
313
314    /// Mapping from selected qubit indices to non-default local operators.
315    pub idx_to_mat: BTreeMap<usize, RankOneTwo<T>>,
316}
317
318/// Returns the inclusive interval `[min, max]` spanned by the keys of a BTreeMap.
319/// Falls back to `default` if the map is empty.
320pub fn key_interval_or<K: Copy + Ord>(map: &BTreeMap<K, impl Sized>, default: (K, K)) -> (K, K) {
321    match (map.keys().next().copied(), map.keys().next_back().copied()) {
322        (Some(min), Some(max)) => (min, max),
323        _ => default,
324    }
325}
326
327/// Swaps the values at keys `i` and `j` in a BTreeMap if either is present.
328/// No effect if both keys are absent.
329pub fn swap_btree_entries<K: Ord + Copy, V>(map: &mut BTreeMap<K, V>, i: K, j: K) {
330    if i == j {
331        return;
332    }
333
334    let i_val = map.remove(&i);
335    let j_val = map.remove(&j);
336
337    if let Some(val) = j_val {
338        map.insert(i, val);
339    }
340    if let Some(val) = i_val {
341        map.insert(j, val);
342    }
343}
344
345impl BraKet {
346    /// Returns the Hermitian conjugate (dagger) of the current `BraKet` variant.
347    ///
348    /// - `Bra` becomes `Ket`
349    /// - `Ket` becomes `Bra`
350    ///
351    /// This operation corresponds to taking the adjoint of a vector:
352    /// - `|ψ⟩† = ⟨ψ|`
353    /// - `⟨ψ|† = |ψ⟩`
354    pub fn dag(&self) -> Self {
355        match self {
356            Bra => Ket,
357            Ket => Bra,
358        }
359    }
360}
361
362impl<T: FloatExt + Hash> NormPair<T> {
363    /// Conjugates the norm_pair.
364    pub fn conj(&self) -> Self {
365        Self {
366            c0: self.c0,
367            c1: self.c1.conj(),
368        }
369    }
370    /// Returns an orthogonal normalized qubit state.
371    ///
372    /// Given a qubit `|ψ⟩ = c₀|0⟩ + c₁|1⟩`, this returns an orthogonal state
373    /// `|ψ⊥⟩ = c₁*|0⟩ − c₀*|1⟩`, normalized and up to a global phase.
374    pub fn orthogonal(&self) -> Self {
375        let (c1_mag, c1_phase) = self.c1.factor_out();
376        let new_c0 = c1_mag.re(); // This radius from c1 is always non-negative.
377
378        match new_c0 {
379            val if val == T::zero() => Self {
380                c0: T::zero(),
381                c1: Cpx::ONE,
382            },
383            val if val == T::one() => Self {
384                c0: T::one(),
385                c1: Cpx::ZERO,
386            },
387            _ => Self {
388                c0: new_c0,
389                c1: -c1_phase.conj() * self.c0,
390            },
391        }
392    }
393    /// Factors out a global amplitude and phase to produce a canonical form.
394    ///
395    /// This ensures a unique and hashable representation by:
396    /// - Forcing `c0 ∈ ℝ≥0` (non-negative real),
397    /// - Normalizing the pair to unit ℓ₂-norm,
398    /// - Extracting any global phase into a separate complex scalar.
399    ///
400    /// Returns:
401    /// - `None` if both amplitudes are zero.
402    /// - Otherwise, returns a triple `(canonical, sqrt_prob, phase)` such that: original = sqrt_prob × phase × canonical
403    ///   , where:
404    /// - `canonical` is a `NormPair` with unit norm and canonical phase,
405    /// - `sqrt_prob ∈ ℝ≥0` is the amplitude magnitude,
406    /// - `phase ∈ ℂ` is the global phase factor.
407    pub fn try_factor_out(&self) -> Option<(Self, T, Cpx<T>)> {
408        let zero = T::zero();
409        let one = T::one();
410
411        match (self.c0 == zero, matches!(self.c1, Cpx::Zero {})) {
412            (true, true) => None,
413            (true, false) => {
414                let (sqrt_prob, ph) = self.c1.factor_out();
415                Some((
416                    Self {
417                        c0: zero,
418                        c1: Cpx::ONE,
419                    },
420                    sqrt_prob.re(),
421                    ph,
422                ))
423            }
424            (false, true) => {
425                let c0_sign = if self.c0 > zero {
426                    Cpx::ONE
427                } else {
428                    Cpx::NEG_ONE
429                };
430                Some((
431                    Self {
432                        c0: one,
433                        c1: Cpx::ZERO,
434                    },
435                    self.c0.abs(),
436                    c0_sign,
437                ))
438            }
439            (false, false) => {
440                // Normalize and extract global phase from c0
441                let norm = self.c0.hypot(self.c1.rad());
442                let c0_sign = if self.c0 > zero { one } else { -one };
443                let c0_real = (self.c0 / norm).abs();
444                let c1_rel = self.c1 / norm * c0_sign;
445
446                Some((
447                    Self {
448                        c0: c0_real,
449                        c1: c1_rel,
450                    },
451                    norm,
452                    Cpx::Real { re: c0_sign },
453                ))
454            }
455        }
456    }
457}
458
459impl<T: FloatExt + Hash> NormQubit<T> {
460    /// Returns the Hermitian conjugate (dagger) of the qubit representation,
461    /// flipping `Bra` ↔ `Ket` and complex-conjugating the internal coefficients.
462    pub fn dag(&self) -> Self {
463        Self {
464            norm_pair: self.norm_pair.conj(),
465            bra_ket: self.bra_ket.dag(),
466        }
467    }
468    /// Converts the `NormQubit` to its bra representation.
469    /// If it is currently a ket, then return the Hermitian conjugate (dagger).
470    pub fn to_bra(&self) -> Self {
471        match self.bra_ket {
472            Bra => *self,
473            Ket => self.dag(),
474        }
475    }
476    /// Converts the `NormQubit` to its ket representation.
477    /// If it is currently a bra, then return the Hermitian conjugate (dagger).
478    pub fn to_ket(&self) -> Self {
479        match self.bra_ket {
480            Ket => *self,
481            Bra => self.dag(),
482        }
483    }
484    /// Returns a normalized qubit orthogonal to `self`.
485    pub fn orthogonal(&self) -> Self {
486        Self {
487            bra_ket: self.bra_ket,
488            norm_pair: self.norm_pair.orthogonal(),
489        }
490    }
491    /// Factors out a global amplitude and phase from the normalized qubit state.
492    ///
493    /// This provides a canonical decomposition of the form: original = sqrt_prob × phase × canonical, where:
494    /// - `canonical` is a `NormQubit` with unit ℓ₂-norm and canonical phase,
495    /// - `sqrt_prob ∈ ℝ≥0` is the size of the original state vector,
496    /// - `phase ∈ ℂ` is the global complex phase.
497    ///
498    /// The result ensures:
499    /// - `norm_pair.c0 ∈ ℝ≥0`
500    /// - `canonical` is uniquely determined up to this constraint
501    ///
502    /// Returns:
503    /// - `None` if both amplitudes are zero.
504    /// - `Some(( canonical, sqrt_prob, phase))` otherwise.
505    pub fn try_factor_out(&self) -> Option<(Self, T, Cpx<T>)> {
506        self.norm_pair
507            .try_factor_out()
508            .map(|(norm_pair, sqrt_prob, phase)| {
509                (
510                    Self {
511                        norm_pair,
512                        bra_ket: self.bra_ket,
513                    },
514                    sqrt_prob,
515                    phase,
516                )
517            })
518    }
519    /// Computes the inner product ⟨self|ket⟩ between two normalized qubits.
520    ///
521    /// `self` is interpreted as a bra, and `ket` as a ket.
522    /// Automatically converts representations if necessary.
523    pub fn inner(&self, ket: &Self) -> Cpx<T> {
524        let bra_pair = self.to_bra().norm_pair;
525        let ket_pair = ket.to_ket().norm_pair;
526        (bra_pair.c1 * ket_pair.c1) + (bra_pair.c0 * ket_pair.c0)
527    }
528    /// Computes the outer product `|self⟩⟨other|` between two normalized qubit states,
529    /// returning a canonical rank-1 matrix representation.
530    ///
531    /// This method interprets:
532    /// - `self` as a ket vector (`|ψ⟩`)
533    /// - `bra` as a bra vector (`⟨φ|`)
534    ///
535    /// It automatically converts the internal orientations (`BraKet`) of each input
536    /// to ensure proper `ket–bra` alignment. The resulting operator is classified as:
537    ///
538    /// - `Projector` if `⟨bra| == ⟨self|` (i.e., `|self⟩⟨self|`);
539    /// - `Nilpotent` if `⟨bra| == ⟨self⊥|` (i.e., `|self⟩⟨self⊥|`);
540    /// - `Rank1::Other` for a general outer product `|ket⟩⟨bra|`.
541    ///
542    /// This ensures canonical detection and representation of projector and nilpotent
543    /// operators when applicable.
544    pub fn outer(&self, bra: &Self) -> Rank1<T> {
545        let ket = self.to_ket().norm_pair;
546        let bra = bra.to_bra().norm_pair;
547        let conj_ket = ket.conj();
548
549        if conj_ket == bra {
550            Rank1::ProNil(Rk1PN {
551                pro_nil: Projector,
552                bra_ket: Ket,
553                norm_pair: ket,
554            })
555        } else if conj_ket.orthogonal() == bra {
556            Rank1::ProNil(Rk1PN {
557                pro_nil: Nilpotent,
558                bra_ket: Ket,
559                norm_pair: ket,
560            })
561        } else {
562            Rank1::Other(Rk1KB { ket, bra })
563        }
564    }
565}
566
567impl<T: FloatExt + Hash> MixPair<T> {
568    /// Returns the complex conjugate of this mixed state.
569    ///
570    /// Conjugates the underlying normalized state while preserving the probability weight.
571    pub fn conj(&self) -> Self {
572        Self {
573            prob: self.prob,
574            norm_pair: self.norm_pair.conj(),
575        }
576    }
577
578    /// Returns the orthogonal mixed state with the same probability.
579    ///
580    /// Constructs a new mixed state whose primary component is orthogonal to the original.
581    pub fn orthogonal(&self) -> Self {
582        Self {
583            prob: self.prob,
584            norm_pair: self.norm_pair.orthogonal(),
585        }
586    }
587
588    /// Computes the purity of the mixed state: `Trace(ρ²) = p² + (1 - p)²`.
589    ///
590    /// Purity ranges from `0.5` (maximally mixed) to `1.0` (pure state).
591    pub fn purity(&self) -> T {
592        let p = self.prob;
593        p.powi(2) + (T::one() - p).powi(2)
594    }
595
596    /// Computes the von Neumann entropy of the mixed state.
597    ///
598    /// Returns `−p ln(p) − (1 − p) ln(1 − p)` if `p ∈ (0, 1)`, and `0` if pure.
599    /// Entropy is zero for pure states and maximal (ln 2) when `p = 0.5`.
600    pub fn entropy_vn(&self) -> T {
601        let zero = T::zero();
602        let one = T::one();
603        let p = self.prob;
604        if (p != one) && (p != zero) {
605            -p * p.ln() - (one - p) * (one - p).ln()
606        } else {
607            zero
608        }
609    }
610}
611
612impl<T: FloatExt + Hash> MixQubit<T> {
613    /// Returns the Hermitian conjugate (dagger) of this mixed qubit state.
614    ///
615    /// Flips the `BraKet` orientation while preserving the mixed content.
616    pub fn dag(&self) -> Self {
617        Self {
618            bra_ket: self.bra_ket.dag(),
619            mix_pair: self.mix_pair.conj(),
620        }
621    }
622
623    /// Returns the orthogonal mixed state with the same orientation and probability.
624    ///
625    /// The primary and orthogonal components are swapped, maintaining the same weighting.
626    pub fn orthogonal(&self) -> Self {
627        Self {
628            bra_ket: self.bra_ket,
629            mix_pair: self.mix_pair.orthogonal(),
630        }
631    }
632
633    /// Returns the purity `Trace(ρ²)` of the underlying mixed state.
634    pub fn purity(&self) -> T {
635        self.mix_pair.purity()
636    }
637
638    /// Returns the von Neumann entropy of the underlying mixed state.
639    pub fn entropy_vn(&self) -> T {
640        self.mix_pair.entropy_vn()
641    }
642}
643
644impl<T: FloatExt + Hash> IdxToNorm<T> {
645    /// Returns the complex conjugate of this `IdxToNorm`.
646    ///
647    /// Applies conjugation to all `NormPair<T>` entries and the padding.
648    pub fn conj(&self) -> Self {
649        Self {
650            interval: self.interval,
651            padding: self.padding.conj(),
652            idx_to_norm: self
653                .idx_to_norm
654                .iter()
655                .map(|(k, v)| (*k, v.conj()))
656                .collect(),
657        }
658    }
659
660    /// Factors out the global amplitude and phase from each `NormPair`,
661    /// returning a new `IdxToNorm` with canonicalized entries,
662    /// and extracting the total amplitude and phase.
663    ///
664    /// Returns:
665    /// - `None` if any `NormPair` fails to factor (e.g., is zero),
666    /// - Otherwise, returns `(canonicalized_self, total_sqrt_prob, total_phase)`.
667    pub fn try_factor_out(&self) -> Option<(Self, T, Cpx<T>)> {
668        let mut idx_to_norm = BTreeMap::new();
669        let mut acc_sqrt_prob = T::one();
670        let mut acc_phase = Cpx::ONE;
671
672        for (&k, v) in &self.idx_to_norm {
673            let (canon, sqrt_prob, phase) = v.try_factor_out()?;
674            idx_to_norm.insert(k, canon);
675            acc_sqrt_prob = acc_sqrt_prob * sqrt_prob;
676            acc_phase *= phase;
677        }
678
679        Some((
680            Self {
681                interval: self.interval,
682                padding: self.padding,
683                idx_to_norm,
684            },
685            acc_sqrt_prob,
686            acc_phase,
687        ))
688    }
689
690    /// Returns the list of qubit indices that have explicitly assigned normalized states.
691    pub fn indices_keys(&self) -> Vec<usize> {
692        self.idx_to_norm.keys().copied().collect()
693    }
694
695    /// Returns the inclusive interval of explicitly assigned qubit indices, if any.
696    ///
697    /// This is equivalent to `(min_index, max_index)` over the `idx_to_norm` map.
698    /// Returns `(None, None)` if the map is empty.
699    pub fn try_interval(&self) -> (Option<usize>, Option<usize>) {
700        let min = self.idx_to_norm.keys().next().copied();
701        let max = self.idx_to_norm.keys().next_back().copied();
702        (min, max)
703    }
704
705    /// Returns a new `IdxToNorm` with its interval adjusted to match the minimum and maximum keys
706    /// present in the `idx_to_norm` map.
707    ///
708    /// This method is useful for automatically syncing the `interval` field with the actual
709    /// span of explicitly defined qubit indices.
710    ///
711    /// # Returns
712    /// - `Some(Self)` with the updated interval if `idx_to_norm` is non-empty.
713    /// - `None` if `idx_to_norm` is empty, as there are no keys to determine an interval.
714    pub fn try_fit_interval(&self) -> Option<Self> {
715        let (min, max) = self.try_interval();
716        Some(Self {
717            interval: (min?, max?),
718            padding: self.padding,
719            idx_to_norm: self.idx_to_norm.clone(),
720        })
721    }
722
723    /// Returns a new `IdxToNorm` where all indices in the interval are explicitly filled.
724    ///
725    /// For any index within `self.interval` that does not appear in `idx_to_norm`, this method
726    /// inserts an entry with the `padding` value. The result is a fully populated BTreeMap with
727    /// no missing indices in the defined interval.
728    pub fn extract_padding(&self) -> Self {
729        let mut idx_to_norm = self.idx_to_norm.clone();
730        let (start, end) = self.interval;
731
732        for i in start..=end {
733            idx_to_norm.entry(i).or_insert(self.padding);
734        }
735
736        Self {
737            interval: self.interval,
738            padding: self.padding,
739            idx_to_norm,
740        }
741    }
742
743    /// Returns a new `IdxToNorm` with the specified interval, leaving all other fields unchanged.
744    ///
745    /// This method updates the `interval` field while cloning the existing `padding`
746    /// and `idx_to_norm` map.
747    pub fn set_interval(&self, interval: (usize, usize)) -> Self {
748        Self {
749            interval,
750            padding: self.padding,
751            idx_to_norm: self.idx_to_norm.clone(),
752        }
753    }
754
755    /// Returns a new `IdxToNorm` with the specified padding value,
756    /// leaving the interval and index-to-norm map unchanged.
757    pub fn set_padding(&self, padding: NormPair<T>) -> Self {
758        Self {
759            interval: self.interval,
760            padding,
761            idx_to_norm: self.idx_to_norm.clone(),
762        }
763    }
764    /// Returns a new `IdxToNorm` with the specified indices removed from `idx_to_norm`.
765    ///
766    /// This method creates a copy of the current structure and removes any entries whose keys
767    /// match the provided indices. The original `interval` and `padding` are preserved.
768    ///
769    /// # Type Parameters
770    /// - `I`: An iterable collection of `usize` indices to remove.
771    ///
772    /// # Arguments
773    /// - `indices`: Any iterable over `usize` values (e.g., a `Vec`, array, range, or `HashSet`).
774    ///
775    /// # Returns
776    /// A new `IdxToNorm` with the specified indices removed from the map.
777    ///
778    /// # Examples
779    /// ```rust
780    /// //let pruned = original.discard(vec![1, 2, 4]);
781    /// //let pruned = original.discard([3, 5, 7]);
782    /// //let pruned = original.discard(0..=10);
783    /// ```
784    pub fn discard<I: IntoIterator<Item = usize>>(&self, indices: I) -> Self {
785        let mut new_map = self.idx_to_norm.clone();
786        for idx in indices {
787            new_map.remove(&idx);
788        }
789
790        Self {
791            interval: self.interval,
792            padding: self.padding,
793            idx_to_norm: new_map,
794        }
795    }
796    /// Returns a new `IdxToNorm` with additional entries merged into `idx_to_norm`.
797    ///
798    /// For each `(index, norm_pair)` in the provided `idx_to_norm` map, if the index is not already present
799    /// in the current map, it is inserted. Existing entries are left unchanged.
800    ///
801    /// The resulting interval is updated to span the full range of indices in the combined map, unless the map
802    /// becomes empty, in which case the original interval is retained.
803    ///
804    /// # Arguments
805    ///
806    /// * `idx_to_norm` - A `BTreeMap<usize, NormPair<T>>` containing new entries to insert.
807    ///
808    /// # Returns
809    ///
810    /// A new `IdxToNorm` with the merged map and updated interval.
811    pub fn extend(&self, idx_to_norm: BTreeMap<usize, NormPair<T>>) -> Self {
812        let mut new_map = self.idx_to_norm.clone();
813        for (k, v) in idx_to_norm {
814            new_map.entry(k).or_insert(v);
815        }
816
817        let interval = key_interval_or(&new_map, self.interval);
818
819        Self {
820            interval,
821            padding: self.padding,
822            idx_to_norm: new_map,
823        }
824    }
825
826    /// Returns a new `IdxToNorm` with the given entries forcibly updated.
827    ///
828    /// For each `(index, norm_pair)` in the provided map:
829    /// - Any existing entry at that index is removed.
830    /// - The new entry is inserted unconditionally.
831    ///
832    /// The updated `idx_to_norm` map reflects all changes, and the interval is recomputed to
833    /// span the full range of keys present in the new map (if non-empty).
834    ///
835    /// # Arguments
836    ///
837    /// * `idx_to_norm` - A `BTreeMap<usize, NormPair<T>>` containing entries to overwrite.
838    ///
839    /// # Returns
840    ///
841    /// A new `IdxToNorm` with the forcibly updated entries and recalculated interval.
842    pub fn force_update(&self, idx_to_norm: BTreeMap<usize, NormPair<T>>) -> Self {
843        let mut new_map = self.idx_to_norm.clone();
844
845        for (k, v) in idx_to_norm {
846            new_map.insert(k, v); // `insert` already replaces the value if key exists
847        }
848
849        let interval = key_interval_or(&new_map, self.interval);
850
851        Self {
852            interval,
853            padding: self.padding,
854            idx_to_norm: new_map,
855        }
856    }
857    /// Returns a new `IdxToNorm` with the values at two indices swapped.
858    ///
859    /// If both indices exist in `idx_to_norm`, their values are swapped.
860    /// If only one exists, its value is moved to the other index.
861    /// If neither exist, the mapping is unchanged.
862    ///
863    /// # Arguments
864    /// * `i` - The first index.
865    /// * `j` - The second index.
866    pub fn swap(&self, i: usize, j: usize) -> Self {
867        if i == j {
868            return self.clone(); // No-op
869        }
870
871        let mut new_map = self.idx_to_norm.clone();
872        swap_btree_entries(&mut new_map, i, j);
873
874        let interval = key_interval_or(&new_map, self.interval);
875
876        Self {
877            interval,
878            padding: self.padding,
879            idx_to_norm: new_map,
880        }
881    }
882}
883
884impl<T: FloatExt + Hash> SepQubits<T> {
885    /// Returns the Hermitian conjugate of the separable multi-qubit state.
886    pub fn dag(&self) -> Self {
887        Self {
888            bra_ket: self.bra_ket.dag(),
889            idx_to_norm: self.idx_to_norm.conj(),
890        }
891    }
892
893    /// Converts the `SepQubit` to its bra representation.
894    /// If it is currently a ket, then return the Hermitian conjugate (dagger).
895    pub fn to_bra(&self) -> Self {
896        match self.bra_ket {
897            Bra => self.clone(),
898            Ket => self.dag(),
899        }
900    }
901
902    /// Converts the `SepQubit` to its ket representation.
903    /// If it is currently a bra, then return the Hermitian conjugate (dagger).
904    pub fn to_ket(&self) -> Self {
905        match self.bra_ket {
906            Ket => self.clone(),
907            Bra => self.dag(),
908        }
909    }
910
911    /// Factors out a global amplitude and phase from all `NormPair`s in the `SepQubit`.
912    ///
913    /// Returns:
914    /// - `None` if any entry fails to factor (e.g., all-zero),
915    /// - Otherwise, the canonicalized structure and overall factor.
916    ///
917    /// Satisfies: `original = sqrt_prob × phase × canonical`.
918    pub fn try_factor_out(&self) -> Option<(Self, T, Cpx<T>)> {
919        let (idx_to_norm, sqrt_prob, phase) = self.idx_to_norm.try_factor_out()?;
920        Some((
921            Self {
922                bra_ket: self.bra_ket,
923                idx_to_norm,
924            },
925            sqrt_prob,
926            phase,
927        ))
928    }
929
930    /// Computes the inner product ⟨self|ket⟩ between two `SepQubits<T>`, using the union of their intervals.
931    ///
932    /// This method interprets `self` as a bra and `ket` as a ket, aligning their index intervals and
933    /// applying padding where necessary. Only indices within the specified interval are used;
934    /// entries outside the interval (even if present in the underlying `BTreeMap`) are ignored.
935    ///
936    /// The inner product is computed as a product of local inner products at each index:
937    /// ⟨ψ|ϕ⟩ = ∏ᵢ ⟨ψᵢ|ϕᵢ⟩.
938    /// Returns `Cpx::ZERO` early if any local inner product is zero, and skips multiplying by `1` for efficiency.
939    pub fn inner(&self, ket: &Self) -> Cpx<T> {
940        let bra = self.to_bra().idx_to_norm;
941        let ket = ket.to_ket().idx_to_norm;
942
943        let (min, max) = {
944            let (bra_min, bra_max) = bra.interval;
945            let (ket_min, ket_max) = ket.interval;
946            (min(bra_min, ket_min), max(bra_max, ket_max))
947        };
948
949        let bra = bra.set_interval((min, max)).extract_padding();
950        let ket = ket.set_interval((min, max)).extract_padding();
951
952        let mut acc = Cpx::ONE;
953        for idx in min..max {
954            let NormPair { c0: b0, c1: b1 } = bra.idx_to_norm[&idx];
955            let NormPair { c0: k0, c1: k1 } = ket.idx_to_norm[&idx];
956            let inner = b1 * k1 + b0 * k0;
957            if matches!(inner, Cpx::Zero {}) {
958                return Cpx::ZERO;
959            } else if matches!(inner, Cpx::One {}) {
960                continue;
961            }
962            acc *= inner;
963        }
964
965        acc
966    }
967
968    /// Returns a vector of all explicit qubit indices.
969    pub fn indices_keys(&self) -> Vec<usize> {
970        self.idx_to_norm.indices_keys()
971    }
972
973    /// Returns the inclusive interval of explicitly assigned qubit indices, if any.
974    ///
975    /// This is equivalent to `(min_index, max_index)` over the `idx_to_norm` map.
976    /// Returns `(None, None)` if the map is empty.
977    pub fn try_interval(&self) -> (Option<usize>, Option<usize>) {
978        let min = self.idx_to_norm.idx_to_norm.keys().next().copied();
979        let max = self.idx_to_norm.idx_to_norm.keys().next_back().copied();
980        (min, max)
981    }
982
983    /// Attempts to shrink the interval to fit the range of explicitly defined indices.
984    /// Returns `None` if `idx_to_norm` is empty.
985    pub fn try_fit_interval(&self) -> Option<Self> {
986        self.idx_to_norm.try_fit_interval().map(|idx_to_norm| Self {
987            bra_ket: self.bra_ket,
988            idx_to_norm,
989        })
990    }
991
992    /// Returns a new `SepQubits` where all missing entries in the interval are filled with padding.
993    pub fn extract_padding(&self) -> Self {
994        Self {
995            bra_ket: self.bra_ket,
996            idx_to_norm: self.idx_to_norm.extract_padding(),
997        }
998    }
999
1000    /// Returns a new `SepQubits` with the interval manually updated.
1001    pub fn set_interval(&self, interval: (usize, usize)) -> Self {
1002        Self {
1003            bra_ket: self.bra_ket,
1004            idx_to_norm: self.idx_to_norm.set_interval(interval),
1005        }
1006    }
1007
1008    /// Returns a new `SepQubits` with a new padding value.
1009    pub fn set_padding(&self, padding: NormPair<T>) -> Self {
1010        Self {
1011            bra_ket: self.bra_ket,
1012            idx_to_norm: self.idx_to_norm.set_padding(padding),
1013        }
1014    }
1015
1016    /// Returns a new `SepQubits` with specified indices removed from the explicit assignments.
1017    pub fn discard<I: IntoIterator<Item = usize>>(&self, indices: I) -> Self {
1018        Self {
1019            bra_ket: self.bra_ket,
1020            idx_to_norm: self.idx_to_norm.discard(indices),
1021        }
1022    }
1023
1024    /// Returns a new `SepQubits` with additional explicit assignments added if not already present.
1025    pub fn extend(&self, idx_to_norm: BTreeMap<usize, NormPair<T>>) -> Self {
1026        Self {
1027            bra_ket: self.bra_ket,
1028            idx_to_norm: self.idx_to_norm.extend(idx_to_norm),
1029        }
1030    }
1031
1032    /// Returns a new `SepQubits` with the provided explicit assignments forcibly updated.
1033    pub fn force_update(&self, idx_to_norm: BTreeMap<usize, NormPair<T>>) -> Self {
1034        Self {
1035            bra_ket: self.bra_ket,
1036            idx_to_norm: self.idx_to_norm.force_update(idx_to_norm),
1037        }
1038    }
1039
1040    /// Returns a new `SepQubits` with entries at indices `i` and `j` swapped.
1041    ///
1042    /// If only one of the two exists in the current map, its value is moved to the other index.
1043    /// If neither exists, the map is unchanged.
1044    pub fn swap(&self, i: usize, j: usize) -> Self {
1045        Self {
1046            bra_ket: self.bra_ket,
1047            idx_to_norm: self.idx_to_norm.swap(i, j),
1048        }
1049    }
1050}
1051
1052impl<T: FloatExt + Hash> IdxToMixQubits<T> {
1053    /// Returns the Hermitian conjugate of the separable multi-qubit mixed state.
1054    pub fn dag(&self) -> Self {
1055        Self {
1056            bra_ket: self.bra_ket.dag(),
1057            interval: self.interval,
1058            padding: self.padding.conj(),
1059            idx_to_mix: self
1060                .idx_to_mix
1061                .iter()
1062                .map(|(k, v)| (*k, v.conj()))
1063                .collect(),
1064        }
1065    }
1066    /// Returns a vector of all explicit qubit indices.
1067    pub fn indices_keys(&self) -> Vec<usize> {
1068        self.idx_to_mix.keys().copied().collect()
1069    }
1070
1071    /// Returns the inclusive interval of explicitly assigned qubit indices, if any.
1072    ///
1073    /// This is equivalent to `(min_index, max_index)` over the `idx_to_mix` map.
1074    /// Returns `(None, None)` if the map is empty.
1075    pub fn try_interval(&self) -> (Option<usize>, Option<usize>) {
1076        let min = self.idx_to_mix.keys().next().copied();
1077        let max = self.idx_to_mix.keys().next_back().copied();
1078        (min, max)
1079    }
1080
1081    /// Attempts to shrink the interval to fit the range of explicitly defined indices.
1082    /// Returns `None` if `idx_to_mix` is empty.
1083    pub fn try_fit_interval(&self) -> Option<Self> {
1084        let (min, max) = self.try_interval();
1085        Some(Self {
1086            bra_ket: self.bra_ket,
1087            interval: (min?, max?),
1088            padding: self.padding,
1089            idx_to_mix: self.idx_to_mix.clone(),
1090        })
1091    }
1092
1093    /// Returns a new `IdxToMixQubits` where all missing entries in the interval are filled with padding.
1094    pub fn extract_padding(&self) -> Self {
1095        let mut idx_to_mix = self.idx_to_mix.clone();
1096        let (start, end) = self.interval;
1097
1098        for i in start..=end {
1099            idx_to_mix.entry(i).or_insert(self.padding);
1100        }
1101
1102        Self {
1103            bra_ket: self.bra_ket,
1104            interval: self.interval,
1105            padding: self.padding,
1106            idx_to_mix,
1107        }
1108    }
1109
1110    /// Returns a new `IdxToMixQubits` with the interval manually updated.
1111    pub fn set_interval(&self, interval: (usize, usize)) -> Self {
1112        Self {
1113            bra_ket: self.bra_ket,
1114            interval,
1115            padding: self.padding,
1116            idx_to_mix: self.idx_to_mix.clone(),
1117        }
1118    }
1119
1120    /// Returns a new `IdxToMixQubits` with a new padding value.
1121    pub fn set_padding(&self, padding: MixPair<T>) -> Self {
1122        Self {
1123            bra_ket: self.bra_ket,
1124            interval: self.interval,
1125            padding,
1126            idx_to_mix: self.idx_to_mix.clone(),
1127        }
1128    }
1129
1130    /// Returns a new `IdxToMixQubits` with specified indices removed from the explicit assignments.
1131    pub fn discard<I: IntoIterator<Item = usize>>(&self, indices: I) -> Self {
1132        let mut new_map = self.idx_to_mix.clone();
1133        for idx in indices {
1134            new_map.remove(&idx);
1135        }
1136
1137        Self {
1138            bra_ket: self.bra_ket,
1139            interval: self.interval,
1140            padding: self.padding,
1141            idx_to_mix: new_map,
1142        }
1143    }
1144
1145    /// Returns a new `IdxToMixQubits` with additional explicit assignments added if not already present.
1146    pub fn extend(&self, idx_to_mix: BTreeMap<usize, MixPair<T>>) -> Self {
1147        let mut new_map = self.idx_to_mix.clone();
1148        for (k, v) in idx_to_mix {
1149            new_map.entry(k).or_insert(v);
1150        }
1151
1152        let interval = key_interval_or(&new_map, self.interval);
1153
1154        Self {
1155            bra_ket: self.bra_ket,
1156            interval,
1157            padding: self.padding,
1158            idx_to_mix: new_map,
1159        }
1160    }
1161
1162    /// Returns a new `IdxToMixQubits` with the provided explicit assignments forcibly updated.
1163    pub fn force_update(&self, idx_to_mix: BTreeMap<usize, MixPair<T>>) -> Self {
1164        let mut new_map = self.idx_to_mix.clone();
1165
1166        for (k, v) in idx_to_mix {
1167            new_map.insert(k, v); // `insert` already replaces the value if key exists
1168        }
1169
1170        let interval = key_interval_or(&new_map, self.interval);
1171
1172        Self {
1173            bra_ket: self.bra_ket,
1174            interval,
1175            padding: self.padding,
1176            idx_to_mix: new_map,
1177        }
1178    }
1179
1180    /// Returns a new `IdxToMixQubits` with entries at indices `i` and `j` swapped.
1181    ///
1182    /// If only one of the two exists in the current map, its value is moved to the other index.
1183    /// If neither exists, the map is unchanged.
1184    pub fn swap(&self, i: usize, j: usize) -> Self {
1185        if i == j {
1186            return self.clone(); // No-op
1187        }
1188
1189        let mut new_map = self.idx_to_mix.clone();
1190        swap_btree_entries(&mut new_map, i, j);
1191
1192        let interval = key_interval_or(&new_map, self.interval);
1193
1194        Self {
1195            bra_ket: self.bra_ket,
1196            interval,
1197            padding: self.padding,
1198            idx_to_mix: new_map,
1199        }
1200    }
1201}
1202
1203impl<T: FloatExt + Hash> Rk1PN<T> {
1204    /// Factors out the global amplitude (probability weight) from the rank-1 projector or nilpotent operator.
1205    ///
1206    /// The underlying `norm_qubit` is decomposed into a canonical form and the scalar amplitude.
1207    /// This method returns a pair `(canonical, prob)` where:
1208    /// - `canonical` is a `Rk1PN` with a normalized pure qubit state (unit norm and canonical phase),
1209    /// - `prob` is the squared magnitude (`sqrt_prob²`) representing the overall probability weight.
1210    ///
1211    /// Returns:
1212    /// - `None` if the underlying state is zero (no meaningful decomposition).
1213    /// - `Some(( canonical, prob))` otherwise.
1214    pub fn try_factor_out(self) -> Option<(Self, T)> {
1215        self.norm_pair
1216            .try_factor_out()
1217            .map(|(norm, sqrt_prob, ..)| {
1218                (
1219                    Self {
1220                        bra_ket: self.bra_ket,
1221                        norm_pair: norm,
1222                        pro_nil: self.pro_nil,
1223                    },
1224                    sqrt_prob.powi(2),
1225                )
1226            })
1227    }
1228    /// Returns the Hermitian conjugate (dagger) of the nilpotent matrix representation.
1229    ///
1230    /// If the operator is a nilpotent matrix, it returns a new one with an orthogonal qubit.
1231    /// If it is a projector, it returns itself unchanged.
1232    pub fn dag(self) -> Self {
1233        match self.pro_nil {
1234            Nilpotent => {
1235                let norm_pair = self.norm_pair.orthogonal();
1236                Self {
1237                    bra_ket: self.bra_ket,
1238                    norm_pair,
1239                    pro_nil: Nilpotent,
1240                }
1241            }
1242            Projector => self,
1243        }
1244    }
1245    /// Returns the trace of a rank-1 projector or nilpotent matrix as a `Cpx<T>`.
1246    pub fn trace(self) -> Cpx<T> {
1247        let NormPair { c0, c1 } = self.norm_pair;
1248
1249        let dual = match self.pro_nil {
1250            Projector => self.norm_pair.conj(),
1251            Nilpotent => self.norm_pair.orthogonal().conj(),
1252        };
1253
1254        Cpx::Real { re: c0 * dual.c0 } + c1 * dual.c1
1255    }
1256}
1257
1258impl<T: FloatExt + Hash> Rk1KB<T> {
1259    /// Factors out the combined amplitude and global phase from a general rank-1 operator.
1260    ///
1261    /// Validates that `ket` is a |ψ⟩ and `bra` is a ⟨ϕ|, then factors out their
1262    /// respective global phases and magnitudes, returning:
1263    ///
1264    /// - `None` if either component is the zero state or has incorrect role labels.
1265    /// - `Some(( canonical, prob, phase))` where:
1266    ///   - `canonical` is a normalized version of the operator with unit-norm states,
1267    ///   - `prob` is the product of the sqrt-probabilities,
1268    ///   - `phase` is the product of their global phases.
1269    pub fn try_factor_out(self) -> Option<(Self, T, Cpx<T>)> {
1270        let ket_factored = self.ket.try_factor_out();
1271        let bra_factored = self.bra.try_factor_out();
1272
1273        match (ket_factored, bra_factored) {
1274            (Some((ket_norm, sqrt_ket, ph_ket)), Some((bra_norm, sqrt_bra, ph_bra))) => {
1275                let sqrt_prob = sqrt_ket * sqrt_bra;
1276                let phase = ph_ket * ph_bra;
1277
1278                Some((
1279                    Self {
1280                        ket: ket_norm,
1281                        bra: bra_norm,
1282                    },
1283                    sqrt_prob,
1284                    phase,
1285                ))
1286            }
1287            _ => None,
1288        }
1289    }
1290    /// Returns the Hermitian conjugate (dagger) of the `Rk1KB` matrix representation.
1291    ///
1292    /// This swaps the bra and ket components and applies conjugation to each.
1293    pub fn dag(self) -> Self {
1294        Self {
1295            ket: self.bra.conj(),
1296            bra: self.ket.conj(),
1297        }
1298    }
1299    /// Returns the trace of a general rank-1 matrix that is neither a projector nor nilpotent, as a `Cpx<T>`.
1300    pub fn trace(self) -> Cpx<T> {
1301        let NormPair { c0: k0, c1: k1 } = self.ket;
1302        let NormPair { c0: b0, c1: b1 } = self.bra;
1303
1304        Cpx::Real { re: k0 * b0 } + k1 * b1
1305    }
1306}
1307
1308impl<T: FloatExt + Hash> Rank1<T> {
1309    /// Factors out amplitude and optional global phase from a rank-1 operator.
1310    ///
1311    /// - Returns `None` if underlying state(s) are zero.
1312    /// - `Some(( canonical, prob, phase_opt))` where:
1313    ///   - `canonical` is normalized (projector or outer product),
1314    ///   - `prob` is the total scalar amplitude (squared norm),
1315    ///   - `phase_opt` is `Some(phase)` for `Other`, `None` for `ProNil`.
1316    pub fn try_factor_out(self) -> Option<(Self, T, Option<Cpx<T>>)> {
1317        match self {
1318            Self::ProNil(pn) => pn
1319                .try_factor_out()
1320                .map(|(canon, prob)| (Self::ProNil(canon), prob, None)),
1321            Self::Other(other) => other
1322                .try_factor_out()
1323                .map(|(canon, prob, phase)| (Self::Other(canon), prob, Some(phase))),
1324        }
1325    }
1326    /// Returns the Hermitian conjugate (dagger) of the rank-1 matrix.
1327    ///
1328    /// For `ProNil`, the projector is preserved and the nilpotent vector is conjugated if applicable.
1329    /// For `Other`, the ket and bra are swapped and conjugated.
1330    pub fn dag(self) -> Self {
1331        match self {
1332            Rank1::ProNil(pn) => Rank1::ProNil(pn.dag()),
1333            Rank1::Other(other) => Rank1::Other(other.dag()),
1334        }
1335    }
1336    /// Returns the trace of a general rank-1 matrix as a `Cpx<T>`.
1337    pub fn trace(self) -> Cpx<T> {
1338        match self {
1339            Rank1::ProNil(pn) => pn.trace(),
1340            Rank1::Other(other) => other.trace(),
1341        }
1342    }
1343}
1344
1345impl<T: FloatExt + Hash> RawMat<T> {
1346    /// Constructs a raw 2×2 complex matrix from a rank-1 representation.
1347    ///
1348    /// Computes the outer product `|ket⟩⟨bra|`, with `ket` and `bra` derived from
1349    /// either a projector/nilpotent pairing or explicit components.
1350    pub fn from_rank1(rk1: Rank1<T>) -> Self {
1351        let build_outer = |k0: Cpx<T>, k1: Cpx<T>, b0: Cpx<T>, b1: Cpx<T>| RawMat {
1352            mat: [[k0 * b0, k0 * b1], [k1 * b0, k1 * b1]],
1353        };
1354
1355        let (ket, bra) = match rk1 {
1356            Rank1::ProNil(pn) => {
1357                let dual = pn.norm_pair.conj();
1358                let dual_out = match pn.pro_nil {
1359                    Projector => dual,
1360                    Nilpotent => dual.orthogonal(),
1361                };
1362                match pn.bra_ket {
1363                    Ket => (pn.norm_pair, dual_out),
1364                    Bra => (dual_out, pn.norm_pair),
1365                }
1366            }
1367            Rank1::Other(other) => (other.ket, other.bra),
1368        };
1369
1370        build_outer(
1371            Cpx::Real { re: ket.c0 },
1372            ket.c1,
1373            Cpx::Real { re: bra.c0 },
1374            bra.c1,
1375        )
1376    }
1377
1378    /// Constructs a raw 2×2 matrix representing the density matrix of a mixed qubit state.
1379    ///
1380    /// The result is a convex combination of projectors onto a qubit and its orthogonal complement:
1381    /// ρ = `prob` × |ψ⟩⟨ψ| + (1 − `prob`) × |ψ⊥⟩⟨ψ⊥|.
1382    pub fn from_mixed_qubit(mixed: MixQubit<T>) -> Self {
1383        let prob = mixed.mix_pair.prob;
1384        let comp_prob = T::one() - prob;
1385        let norm = mixed.mix_pair.norm_pair;
1386        let bra_ket = mixed.bra_ket;
1387
1388        let mk_proj = |norm_pair| {
1389            Self::from_rank1(Rank1::ProNil(Rk1PN {
1390                bra_ket,
1391                norm_pair,
1392                pro_nil: Projector,
1393            }))
1394        };
1395
1396        let rho1 = mk_proj(norm) * prob;
1397        let rho2 = mk_proj(norm.orthogonal()) * comp_prob;
1398
1399        rho1 + rho2
1400    }
1401
1402    /// Converts a (complex) quaternion in the Pauli basis back into a raw 2×2 complex matrix.
1403    ///
1404    /// The complex quaternion is of the form:  
1405    /// `M = c₀·I + c₁·X + c₂·Y + c₃·Z`, where `coefficients = [c₀, c₁, c₂, c₃]`.
1406    pub fn from_cpx_quaternion(cq: CpxQuaternion<T>) -> Self {
1407        let [c0, c1, c2, c3] = cq.coefficients;
1408        let mat = [[c0 + c3, c1 - c2 * Cpx::J], [c1 + c2 * Cpx::J, c0 - c3]];
1409        Self { mat }
1410    }
1411
1412    /// Converts a (real) quaternion in the Pauli basis back into a raw 2×2 complex matrix.
1413    ///
1414    /// The real quaternion is of the form:  
1415    /// `M = r₀·I + r₁·X + r₂·Y + r₃·Z`, where `coefficients = [r₀, r₁, r₂, r₃]`.
1416    pub fn from_real_quaternion(rq: RealQuaternion<T>) -> Self {
1417        Self::from_cpx_quaternion(CpxQuaternion::from_real_quaternion(rq))
1418    }
1419
1420    /// Converts a unit-determinant complex quaternion into a raw 2×2 matrix.
1421    ///
1422    /// Computes the implicit identity coefficient `a₀` to satisfy `det(M) = 1`,
1423    /// then reconstructs the matrix from the full Pauli decomposition.
1424    pub fn from_det1_cpx_quaternion(d1cq: Det1CpxQuaternion<T>) -> Self {
1425        Self::from_cpx_quaternion(CpxQuaternion::from_det1_cpx_quaternion(d1cq))
1426    }
1427
1428    /// Converts a unit-determinant real quaternion into a raw 2×2 matrix.
1429    ///
1430    /// Internally lifts the real coefficients into the complex domain and reconstructs
1431    /// the full matrix while enforcing the unit-determinant constraint.
1432    pub fn from_det1_real_quaternion(d1rq: Det1RealQuaternion<T>) -> Self {
1433        Self::from_real_quaternion(RealQuaternion::from_det1_real_quaternion(d1rq))
1434    }
1435
1436    /// Returns the Hermitian conjugate (dagger) of the Raw matrix representation.
1437    pub fn dag(self) -> Self {
1438        let m = self.mat;
1439        Self {
1440            mat: [
1441                [m[0][0].conj(), m[1][0].conj()],
1442                [m[0][1].conj(), m[1][1].conj()],
1443            ],
1444        }
1445    }
1446    /// Returns the trace of a raw matrix as a `Cpx<T>`.
1447    pub fn trace(self) -> Cpx<T> {
1448        let m = self.mat;
1449        m[0][0] + m[1][1]
1450    }
1451    /// Returns the determinant of the matrix.
1452    pub fn det(self) -> Cpx<T> {
1453        let m = self.mat;
1454        (m[0][0] * m[1][1]) - (m[0][1] * m[1][0])
1455    }
1456    /// Returns `true` if all entries of the matrix are zero.
1457    pub fn is_zero(&self) -> bool {
1458        self.mat.iter().flatten().all(|c| matches!(c, Cpx::Zero {}))
1459    }
1460    /// Returns the rank of the matrix (0, 1, or 2).
1461    pub fn rank(&self) -> u8 {
1462        if self.is_zero() {
1463            0
1464        } else if matches!(self.det(), Cpx::Zero {}) {
1465            1
1466        } else {
1467            2
1468        }
1469    }
1470}
1471
1472impl<T: FloatExt + Hash> CpxQuaternion<T> {
1473    /// Constructs a Pauli-basis (cpx-quaternion) representation from a raw 2×2 complex matrix.
1474    ///
1475    /// Decomposes the matrix `M` into the Pauli basis:  
1476    /// `M = a₀·I + a₁·X + a₂·Y + a₃·Z`,  
1477    /// where `a₀ = (M₀₀ + M₁₁)/2`, `a₁ = (M₀₁ + M₁₀)/2`, etc.
1478    pub fn from_raw_mat(raw: RawMat<T>) -> Self {
1479        let half = T::from(0.5).unwrap();
1480        let [[a, b], [c, d]] = raw.mat;
1481
1482        let coefficients = [
1483            (a + d) * half,          // I component
1484            (b + c) * half,          // X component
1485            (b - c) * half * Cpx::J, // Y component
1486            (a - d) * half,          // Z component
1487        ];
1488
1489        Self { coefficients }
1490    }
1491
1492    /// Constructs a Pauli-basis (cpx-quaternion) representation from a Hermitian 2×2 matrix.
1493    pub fn from_real_quaternion(rq: RealQuaternion<T>) -> Self {
1494        let mut coefficients = [Cpx::ZERO; 4];
1495        for (i, &real) in rq.coefficients.iter().enumerate() {
1496            coefficients[i] = Cpx::Real { re: real };
1497        }
1498        Self { coefficients }
1499    }
1500
1501    /// Constructs a `CpxQuaternion` from a `Det1CpxQuaternion`, computing the identity coefficient
1502    /// to satisfy `det(M) = 1` under the Pauli basis expansion.
1503    pub fn from_det1_cpx_quaternion(d1cq: Det1CpxQuaternion<T>) -> Self {
1504        let x2 = d1cq.x.powi(2);
1505        let y2 = d1cq.y.powi(2);
1506        let z2 = d1cq.z.powi(2);
1507
1508        let a0 = (x2 + y2 + z2 + Cpx::ONE).powf(T::from(0.5).unwrap()); // a₀ = sqrt(1 + x² + y² + z²)
1509
1510        let coefficients = [a0, d1cq.x, d1cq.y, d1cq.z];
1511        Self { coefficients }
1512    }
1513
1514    /// Counts how many coefficients are not the `Cpx::Zero` variant.
1515    ///
1516    /// Assumes zeros are exactly represented by `Cpx::Zero {}`.
1517    pub fn nonzero_count(&self) -> usize {
1518        self.coefficients
1519            .iter()
1520            .filter(|c| !matches!(c, Cpx::Zero {}))
1521            .count()
1522    }
1523
1524    /// Checks whether this matrix is an identity matrix or a Pauli matrix.
1525    pub fn is_ixyz(&self) -> bool {
1526        self.nonzero_count() == 1
1527    }
1528
1529    /// Returns the Hermitian conjugate (dagger) of the cpx-quaternion representation.
1530    pub fn dag(&self) -> Self {
1531        let mut coefficients = self.coefficients;
1532        for elem in &mut coefficients {
1533            *elem = elem.conj();
1534        }
1535        Self { coefficients }
1536    }
1537
1538    /// Returns the trace of the cpx-quaternion representation as a `Cpx<T>`.
1539    pub fn trace(&self) -> Cpx<T> {
1540        self.coefficients[0] * T::from(2.0).unwrap()
1541    }
1542
1543    /// Computes the determinant of the matrix represented by this quaternion:
1544    /// det(M) = (a₀)² − (a₁)² − (a₂)² − (a₃)², where M = a₀·I + a₁·X + a₂·Y + a₃·Z.
1545    pub fn det(&self) -> Cpx<T> {
1546        let [a0, a1, a2, a3] = self.coefficients;
1547        a0.powi(2) - a1.powi(2) - a2.powi(2) - a3.powi(2)
1548    }
1549}
1550
1551impl<T: FloatExt + Hash> RealQuaternion<T> {
1552    /// Constructs a `RealQuaternion` from a `Det1RealQuaternion`, computing the identity coefficient
1553    /// to satisfy `det(M) = 1` under the Pauli basis expansion.
1554    pub fn from_det1_real_quaternion(d1rq: Det1RealQuaternion<T>) -> Self {
1555        let x2 = d1rq.x.powi(2);
1556        let y2 = d1rq.y.powi(2);
1557        let z2 = d1rq.z.powi(2);
1558
1559        let a0 = (x2 + y2 + z2 + T::one()).sqrt(); // a₀ = sqrt(1 + x² + y² + z²)
1560
1561        let coefficients = [a0, d1rq.x, d1rq.y, d1rq.z];
1562        Self { coefficients }
1563    }
1564
1565    /// Counts how many coefficients are not the `T::zero()` variant.
1566    ///
1567    /// Assumes zeros are exactly represented by `T::zero()`.
1568    pub fn nonzero_count(&self) -> usize {
1569        self.coefficients
1570            .iter()
1571            .filter(|c| **c != T::zero())
1572            .count()
1573    }
1574    /// Checks whether this matrix is an identity matrix or a Pauli matrix.
1575    pub fn is_ixyz(&self) -> bool {
1576        self.nonzero_count() == 1
1577    }
1578    /// Returns the trace of the real-quaternion representation as a `T`.
1579    pub fn trace(&self) -> T {
1580        self.coefficients[0] * T::from(2.0).unwrap()
1581    }
1582
1583    /// Computes the determinant of the matrix represented by this real quaternion:
1584    /// det(M) = (a₀)² − (a₁)² − (a₂)² − (a₃)², where M = a₀·I + a₁·X + a₂·Y + a₃·Z.
1585    pub fn det(&self) -> T {
1586        let [a0, a1, a2, a3] = self.coefficients;
1587        a0.powi(2) - a1.powi(2) - a2.powi(2) - a3.powi(2)
1588    }
1589}
1590
1591impl<T: FloatExt + Hash> Det1CpxQuaternion<T> {
1592    /// Constructs a `Det1CpxQuaternion` from a `Det1RealQuaternion`, lifting real components
1593    /// into the complex domain while preserving the determinant-1 constraint.
1594    pub fn from_real(d1rq: Det1RealQuaternion<T>) -> Self {
1595        Self {
1596            x: Cpx::Real { re: d1rq.x },
1597            y: Cpx::Real { re: d1rq.y },
1598            z: Cpx::Real { re: d1rq.z },
1599        }
1600    }
1601
1602    /// Returns the implicit identity component `a₀` such that:
1603    /// `a₀ = sqrt(1 + x² + y² + z²)`, ensuring `det(M) = 1`.
1604    pub fn a0(&self) -> Cpx<T> {
1605        (Cpx::ONE + self.x.powi(2) + self.y.powi(2) + self.z.powi(2)).powf(T::from(0.5).unwrap())
1606    }
1607
1608    /// Returns the Hermitian conjugate (dagger) of the cpx-quaternion representation with unit determinant.
1609    pub fn dag(&self) -> Self {
1610        let x = self.x.conj();
1611        let y = self.y.conj();
1612        let z = self.z.conj();
1613        Self { x, y, z }
1614    }
1615
1616    /// Returns the trace of the corresponding 2×2 matrix, which is `2·a₀`.
1617    pub fn trace(&self) -> Cpx<T> {
1618        self.a0() * T::from(2.0).unwrap()
1619    }
1620
1621    /// Checks whether this matrix is an identity matrix or a Pauli matrix.
1622    pub fn is_ixyz(self) -> bool {
1623        CpxQuaternion::from_det1_cpx_quaternion(self).nonzero_count() == 1
1624    }
1625}
1626
1627impl<T: FloatExt + Hash> Det1RealQuaternion<T> {
1628    /// Returns the implicit identity component `a₀` such that:
1629    /// `a₀ = sqrt(1 + x² + y² + z²)`, ensuring `det(M) = 1`.
1630    pub fn a0(&self) -> T {
1631        (T::one() + self.x.powi(2) + self.y.powi(2) + self.z.powi(2)).powf(T::from(0.5).unwrap())
1632    }
1633
1634    /// Returns the trace of the corresponding 2×2 matrix, which is `2·a₀`.
1635    pub fn trace(&self) -> T {
1636        self.a0() * T::from(2.0).unwrap()
1637    }
1638
1639    /// Checks whether this matrix is an identity matrix or a Pauli matrix.
1640    pub fn is_ixyz(self) -> bool {
1641        RealQuaternion::from_det1_real_quaternion(self).nonzero_count() == 1
1642    }
1643}
1644
1645impl<T: FloatExt + Hash> Det1<T> {
1646    /// Returns the trace of the matrix: `tr(M) = 2·a₀`, where `a₀` is the identity component.
1647    pub fn trace(&self) -> Cpx<T> {
1648        match self {
1649            Self::Hermitian(d1rq) => Cpx::Real { re: d1rq.trace() },
1650            Self::Other(d1cq) => d1cq.trace(),
1651        }
1652    }
1653
1654    /// Returns the Hermitian adjoint (dagger) of the matrix.
1655    ///
1656    /// For Hermitian matrices, this is the identity operation.
1657    /// For non-Hermitian matrices, conjugates all Pauli coefficients.
1658    pub fn dag(&self) -> Self {
1659        match self {
1660            Self::Hermitian(d1rq) => Self::Hermitian(*d1rq),
1661            Self::Other(d1cq) => Self::Other(d1cq.dag()),
1662        }
1663    }
1664
1665    /// Returns `true` if the matrix is exactly one of `{I, X, Y, Z}`.
1666    pub fn is_ixyz(self) -> bool {
1667        match self {
1668            Self::Hermitian(d1rq) => d1rq.is_ixyz(),
1669            Self::Other(d1cq) => d1cq.is_ixyz(),
1670        }
1671    }
1672}
1673
1674impl<T: FloatExt + Hash> RankOneTwo<T> {
1675    /// Returns the Hermitian conjugate (dagger) of the matrix.
1676    pub fn dag(self) -> Self {
1677        match self {
1678            Self::Rank1(rk1) => Self::Rank1(rk1.dag()),
1679            Self::Rank2(det1) => Self::Rank2(det1.dag()),
1680        }
1681    }
1682
1683    /// Returns the trace of the matrix.
1684    pub fn trace(self) -> Cpx<T> {
1685        match self {
1686            Self::Rank1(rk1) => rk1.trace(),
1687            Self::Rank2(det1) => det1.trace(),
1688        }
1689    }
1690
1691    /// Returns the determinant of the matrix.
1692    pub fn det(self) -> Cpx<T> {
1693        match self {
1694            Self::Rank1(..) => Cpx::ZERO,
1695            Self::Rank2(..) => Cpx::ONE,
1696        }
1697    }
1698
1699    /// Returns the rank of this finite rank 2x2 matrix.
1700    pub fn rank(self) -> usize {
1701        match self {
1702            Self::Rank1(..) => 1,
1703            Self::Rank2(..) => 2,
1704        }
1705    }
1706}
1707
1708impl<T: FloatExt + Hash> LocalOps<T> {
1709    /// Returns the list of qubit indices that have explicitly assigned `RankOneTwo`.
1710    pub fn indices_keys(&self) -> Vec<usize> {
1711        self.idx_to_mat.keys().copied().collect()
1712    }
1713
1714    /// Returns the inclusive interval of explicitly assigned qubit indices, if any.
1715    ///
1716    /// This is equivalent to `(min_index, max_index)` over the `idx_to_mix` map.
1717    /// Returns `(None, None)` if the map is empty.
1718    pub fn try_interval(&self) -> (Option<usize>, Option<usize>) {
1719        let min = self.idx_to_mat.keys().next().copied();
1720        let max = self.idx_to_mat.keys().next_back().copied();
1721        (min, max)
1722    }
1723
1724    /// Returns a new `LocalOps` with its interval adjusted to match the minimum and maximum keys
1725    /// present in the `idx_to_mat` map.
1726    ///
1727    /// This method is useful for automatically syncing the `interval` field with the actual
1728    /// span of explicitly defined qubit indices.
1729    ///
1730    /// # Returns
1731    /// - `Some(Self)` with the updated interval if `idx_to_mat` is non-empty.
1732    /// - `None` if `idx_to_mat` is empty, as there are no keys to determine an interval.
1733    pub fn try_fit_interval(&self) -> Option<Self> {
1734        let (min, max) = self.try_interval();
1735        Some(Self {
1736            interval: (min?, max?),
1737            padding: self.padding,
1738            idx_to_mat: self.idx_to_mat.clone(),
1739        })
1740    }
1741
1742    /// Returns a new `LocalOps` where all indices in the interval are explicitly filled.
1743    ///
1744    /// For any index within `self.interval` that does not appear in `idx_to_mat`, this method
1745    /// inserts an entry with the `padding` value. The result is a fully populated BTreeMap with
1746    /// no missing indices in the defined interval.
1747    pub fn extract_padding(&self) -> Self {
1748        let mut idx_to_mat = self.idx_to_mat.clone();
1749        let (start, end) = self.interval;
1750
1751        for i in start..=end {
1752            idx_to_mat.entry(i).or_insert(self.padding);
1753        }
1754
1755        Self {
1756            interval: self.interval,
1757            padding: self.padding,
1758            idx_to_mat,
1759        }
1760    }
1761
1762    /// Returns a new `LocalOps` with the specified interval, leaving all other fields unchanged.
1763    ///
1764    /// This method updates the `interval` field while cloning the existing `padding`
1765    /// and `idx_to_mat` map.
1766    pub fn set_interval(&self, interval: (usize, usize)) -> Self {
1767        Self {
1768            interval,
1769            padding: self.padding,
1770            idx_to_mat: self.idx_to_mat.clone(),
1771        }
1772    }
1773
1774    /// Returns a new `LocalOps` with the specified padding value,
1775    /// leaving the interval and index-to-mat map unchanged.
1776    pub fn set_padding(&self, padding: RankOneTwo<T>) -> Self {
1777        Self {
1778            interval: self.interval,
1779            padding,
1780            idx_to_mat: self.idx_to_mat.clone(),
1781        }
1782    }
1783    /// Returns a new `LocalOps` with the specified indices removed from `idx_to_mat`.
1784    ///
1785    /// This method creates a copy of the current structure and removes any entries whose keys
1786    /// match the provided indices. The original `interval` and `padding` are preserved.
1787    ///
1788    /// # Type Parameters
1789    /// - `I`: An iterable collection of `usize` indices to remove.
1790    ///
1791    /// # Arguments
1792    /// - `indices`: Any iterable over `usize` values (e.g., a `Vec`, array, range, or `HashSet`).
1793    ///
1794    /// # Returns
1795    /// A new `LocalOps` with the specified indices removed from the map.
1796    ///
1797    /// # Examples
1798    /// ```rust
1799    /// //let pruned = original.discard(vec![1, 2, 4]);
1800    /// //let pruned = original.discard([3, 5, 7]);
1801    /// //let pruned = original.discard(0..=10);
1802    /// ```
1803    pub fn discard<I: IntoIterator<Item = usize>>(&self, indices: I) -> Self {
1804        let mut new_map = self.idx_to_mat.clone();
1805        for idx in indices {
1806            new_map.remove(&idx);
1807        }
1808
1809        Self {
1810            interval: self.interval,
1811            padding: self.padding,
1812            idx_to_mat: new_map,
1813        }
1814    }
1815    /// Returns a new `LocalOps` with additional entries merged into `idx_to_mat`.
1816    ///
1817    /// For each `(index, RankOneTwo<T>)` in the provided `idx_to_mat` map, if the index is not already present
1818    /// in the current map, it is inserted. Existing entries are left unchanged.
1819    ///
1820    /// The resulting interval is updated to span the full range of indices in the combined map, unless the map
1821    /// becomes empty, in which case the original interval is retained.
1822    ///
1823    /// # Arguments
1824    ///
1825    /// * `idx_to_mat` - A `BTreeMap<usize, RankOneTwo<T>>` containing new entries to insert.
1826    ///
1827    /// # Returns
1828    ///
1829    /// A new `LocalOps` with the merged map and updated interval.
1830    pub fn extend(&self, idx_to_mat: BTreeMap<usize, RankOneTwo<T>>) -> Self {
1831        let mut new_map = self.idx_to_mat.clone();
1832        for (k, v) in idx_to_mat.iter() {
1833            new_map.entry(*k).or_insert(*v);
1834        }
1835
1836        let interval = key_interval_or(&new_map, self.interval);
1837
1838        Self {
1839            interval,
1840            padding: self.padding,
1841            idx_to_mat: new_map,
1842        }
1843    }
1844
1845    /// Returns a new `LocalOps` with the given entries forcibly updated.
1846    ///
1847    /// For each `(index, RankOneTwo<T>)` in the provided map:
1848    /// - Any existing entry at that index is removed.
1849    /// - The new entry is inserted unconditionally.
1850    ///
1851    /// The updated `idx_to_mat` map reflects all changes, and the interval is recomputed to
1852    /// span the full range of keys present in the new map (if non-empty).
1853    ///
1854    /// # Arguments
1855    ///
1856    /// * `idx_to_mat` - A `BTreeMap<usize, RankOneTwo<T>>` containing entries to overwrite.
1857    ///
1858    /// # Returns
1859    ///
1860    /// A new `LocalOps` with the forcibly updated entries and recalculated interval.
1861    pub fn force_update(&self, idx_to_mat: BTreeMap<usize, RankOneTwo<T>>) -> Self {
1862        let mut new_map = self.idx_to_mat.clone();
1863
1864        for (k, v) in idx_to_mat {
1865            new_map.insert(k, v); // `insert` already replaces the value if key exists
1866        }
1867
1868        let interval = key_interval_or(&new_map, self.interval);
1869
1870        Self {
1871            interval,
1872            padding: self.padding,
1873            idx_to_mat: new_map,
1874        }
1875    }
1876    /// Returns a new `LocalOps` with the values at two indices swapped.
1877    ///
1878    /// If both indices exist in `idx_to_mat`, their values are swapped.
1879    /// If only one exists, its value is moved to the other index.
1880    /// If neither exist, the mapping is unchanged.
1881    ///
1882    /// # Arguments
1883    /// * `i` - The first index.
1884    /// * `j` - The second index.
1885    pub fn swap(&self, i: usize, j: usize) -> Self {
1886        if i == j {
1887            return self.clone(); // No-op
1888        }
1889
1890        let mut new_map = self.idx_to_mat.clone();
1891        swap_btree_entries(&mut new_map, i, j);
1892
1893        let interval = key_interval_or(&new_map, self.interval);
1894
1895        Self {
1896            interval,
1897            padding: self.padding,
1898            idx_to_mat: new_map,
1899        }
1900    }
1901}
1902
1903impl<T: FloatExt + Hash> Add for RawMat<T> {
1904    type Output = Self;
1905
1906    fn add(self, other: Self) -> Self {
1907        let mut mat = self.mat;
1908        for (i, row) in mat.iter_mut().enumerate() {
1909            for (j, elem) in row.iter_mut().enumerate() {
1910                *elem += other.mat[i][j];
1911            }
1912        }
1913        Self { mat }
1914    }
1915}
1916
1917impl<T: FloatExt + Hash> AddAssign for RawMat<T> {
1918    fn add_assign(&mut self, other: Self) {
1919        for (i, row) in self.mat.iter_mut().enumerate() {
1920            for (j, elem) in row.iter_mut().enumerate() {
1921                *elem += other.mat[i][j];
1922            }
1923        }
1924    }
1925}
1926
1927impl<T: FloatExt + Hash> Neg for RawMat<T> {
1928    type Output = Self;
1929    fn neg(self) -> Self {
1930        let mut mat = self.mat;
1931        for row in mat.iter_mut() {
1932            for elem in row.iter_mut() {
1933                *elem = -*elem;
1934            }
1935        }
1936        Self { mat }
1937    }
1938}
1939
1940impl<T: FloatExt + Hash> Sub for RawMat<T> {
1941    type Output = Self;
1942    fn sub(self, other: Self) -> Self {
1943        self + (-other)
1944    }
1945}
1946
1947impl<T: FloatExt + Hash> SubAssign for RawMat<T> {
1948    fn sub_assign(&mut self, other: Self) {
1949        *self += -other;
1950    }
1951}
1952
1953impl<T: FloatExt + Hash> Mul for RawMat<T> {
1954    type Output = Self;
1955
1956    fn mul(self, rhs: Self) -> Self::Output {
1957        let [[a, b], [c, d]] = self.mat;
1958        let [[e, f], [g, h]] = rhs.mat;
1959
1960        Self {
1961            mat: [
1962                [a * e + b * g, a * f + b * h],
1963                [c * e + d * g, c * f + d * h],
1964            ],
1965        }
1966    }
1967}
1968
1969impl<T: FloatExt + Hash> MulAssign for RawMat<T> {
1970    fn mul_assign(&mut self, rhs: Self) {
1971        *self = *self * rhs;
1972    }
1973}
1974impl<T: FloatExt + Hash> Mul<T> for RawMat<T> {
1975    type Output = Self;
1976    fn mul(self, rhs: T) -> Self::Output {
1977        let mut mat = self.mat;
1978        for row in mat.iter_mut() {
1979            for elem in row.iter_mut() {
1980                *elem *= rhs;
1981            }
1982        }
1983        Self { mat }
1984    }
1985}
1986
1987impl<T: FloatExt + Hash> MulAssign<T> for RawMat<T> {
1988    fn mul_assign(&mut self, rhs: T) {
1989        *self = *self * rhs;
1990    }
1991}
1992
1993impl<T: FloatExt + Hash> Mul<Cpx<T>> for RawMat<T> {
1994    type Output = Self;
1995    fn mul(self, rhs: Cpx<T>) -> Self::Output {
1996        let mut mat = self.mat;
1997        for row in mat.iter_mut() {
1998            for elem in row.iter_mut() {
1999                *elem *= rhs;
2000            }
2001        }
2002        Self { mat }
2003    }
2004}
2005
2006impl<T: FloatExt + Hash> MulAssign<Cpx<T>> for RawMat<T> {
2007    fn mul_assign(&mut self, rhs: Cpx<T>) {
2008        *self = *self * rhs;
2009    }
2010}
2011
2012impl<T: FloatExt + Hash> Add for CpxQuaternion<T> {
2013    type Output = Self;
2014    fn add(self, other: Self) -> Self {
2015        let mut coefficients = self.coefficients;
2016        for (i, elem) in coefficients.iter_mut().enumerate() {
2017            *elem += other.coefficients[i];
2018        }
2019        Self { coefficients }
2020    }
2021}
2022
2023impl<T: FloatExt + Hash> AddAssign for CpxQuaternion<T> {
2024    fn add_assign(&mut self, other: Self) {
2025        let mut coefficients = self.coefficients;
2026        for (i, elem) in coefficients.iter_mut().enumerate() {
2027            *elem += other.coefficients[i];
2028        }
2029    }
2030}
2031
2032impl<T: FloatExt + Hash> Neg for CpxQuaternion<T> {
2033    type Output = Self;
2034    fn neg(self) -> Self::Output {
2035        let mut coefficients = self.coefficients;
2036        for elem in coefficients.iter_mut() {
2037            *elem = -*elem;
2038        }
2039        Self { coefficients }
2040    }
2041}
2042
2043impl<T: FloatExt + Hash> Sub for CpxQuaternion<T> {
2044    type Output = Self;
2045    fn sub(self, other: Self) -> Self {
2046        self + (-other)
2047    }
2048}
2049
2050impl<T: FloatExt + Hash> SubAssign for CpxQuaternion<T> {
2051    fn sub_assign(&mut self, other: Self) {
2052        *self += -other;
2053    }
2054}
2055
2056impl<T: FloatExt + Hash> Mul for CpxQuaternion<T> {
2057    type Output = Self;
2058    fn mul(self, rhs: Self) -> Self {
2059        let mat_lhs = RawMat::from_cpx_quaternion(self);
2060        let mat_rhs = RawMat::from_cpx_quaternion(rhs);
2061        let mat_new = mat_lhs * mat_rhs;
2062        Self::from_raw_mat(mat_new)
2063    }
2064}
2065
2066impl<T: FloatExt + Hash> MulAssign for CpxQuaternion<T> {
2067    fn mul_assign(&mut self, rhs: Self) {
2068        *self = *self * rhs;
2069    }
2070}
2071
2072impl<T: FloatExt + Hash> Mul<T> for CpxQuaternion<T> {
2073    type Output = Self;
2074    fn mul(self, rhs: T) -> Self::Output {
2075        let mut coefficients = self.coefficients;
2076        for elem in coefficients.iter_mut() {
2077            *elem *= rhs;
2078        }
2079        Self { coefficients }
2080    }
2081}
2082
2083impl<T: FloatExt + Hash> MulAssign<T> for CpxQuaternion<T> {
2084    fn mul_assign(&mut self, rhs: T) {
2085        *self = *self * rhs;
2086    }
2087}
2088
2089impl<T: FloatExt + Hash> Mul<Cpx<T>> for CpxQuaternion<T> {
2090    type Output = Self;
2091    fn mul(self, rhs: Cpx<T>) -> Self::Output {
2092        let mut coefficients = self.coefficients;
2093        for elem in coefficients.iter_mut() {
2094            *elem *= rhs;
2095        }
2096        Self { coefficients }
2097    }
2098}
2099
2100impl<T: FloatExt + Hash> MulAssign<Cpx<T>> for CpxQuaternion<T> {
2101    fn mul_assign(&mut self, rhs: Cpx<T>) {
2102        *self = *self * rhs;
2103    }
2104}