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}