Skip to main content

cqlib_core/qis/fermion/
operator.rs

1// This code is part of Cqlib.
2//
3// (C) Copyright China Telecom Quantum Group 2026
4//
5// This code is licensed under the Apache License, Version 2.0. You may
6// obtain a copy of this license in the LICENSE.txt file in the root directory
7// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8//
9// Any modifications or derivative works of this code must retain this
10// copyright notice, and modified files need to carry a notice indicating
11// that they have been altered from the originals.
12
13//! Structured fermionic operators and canonical anticommutation algebra.
14
15use crate::qis::QisError;
16use crate::qis::error::validate_coefficient;
17use num_complex::Complex64;
18use std::collections::{BTreeMap, BTreeSet};
19use std::ops::{Add, Mul, Neg, Sub};
20
21/// A creation or annihilation operator on a zero-based fermionic mode.
22///
23/// A sequence `[A, B]` denotes the mathematical product `A B`: `B` acts first.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
25pub enum FermionicLadder {
26    /// Creates a particle in the given mode.
27    Create(usize),
28    /// Removes a particle from the given mode.
29    Annihilate(usize),
30}
31
32impl FermionicLadder {
33    /// Returns the mode on which this operator acts.
34    pub fn mode(self) -> usize {
35        match self {
36            Self::Create(mode) | Self::Annihilate(mode) => mode,
37        }
38    }
39
40    /// Exchanges creation and annihilation.
41    pub fn adjoint(self) -> Self {
42        match self {
43            Self::Create(mode) => Self::Annihilate(mode),
44            Self::Annihilate(mode) => Self::Create(mode),
45        }
46    }
47}
48
49/// A finite complex linear combination of ordered fermionic products.
50///
51/// The declared mode count is retained even for zero and identity operators.
52/// Equal ordered products are merged automatically; only exact zero coefficients
53/// are removed. Products are not automatically normal ordered. Use [`Self::chop`]
54/// for explicit numerical truncation and [`Self::equiv`] for algebraic equivalence.
55/// Structural equality also compares the declared mode count.
56///
57/// Fallible arithmetic operators return `Result`, for example `(&a + &b)?`.
58#[derive(Debug, Clone, PartialEq)]
59pub struct FermionicOp {
60    num_modes: usize,
61    terms: BTreeMap<Vec<FermionicLadder>, Complex64>,
62}
63
64pub(crate) fn validate_tolerance(atol: f64) -> Result<(), QisError> {
65    if !atol.is_finite() || atol < 0.0 {
66        return Err(QisError::InvalidParameterValue(
67            "tolerance must be finite and nonnegative".into(),
68        ));
69    }
70    Ok(())
71}
72
73impl FermionicOp {
74    /// Creates the zero operator on the declared mode space, including zero modes.
75    pub fn new(num_modes: usize) -> Self {
76        Self {
77            num_modes,
78            terms: BTreeMap::new(),
79        }
80    }
81
82    /// Creates the identity. An empty product is the identity, not the zero operator.
83    pub fn identity(num_modes: usize) -> Self {
84        let mut result = Self::new(num_modes);
85        result.terms.insert(Vec::new(), Complex64::new(1.0, 0.0));
86        result
87    }
88
89    /// Creates a weighted ordered product. Even zero-weight terms are validated.
90    pub fn from_term(
91        num_modes: usize,
92        term: impl IntoIterator<Item = FermionicLadder>,
93        coefficient: Complex64,
94    ) -> Result<Self, QisError> {
95        let mut result = Self::new(num_modes);
96        result.add_term(term, coefficient)?;
97        Ok(result)
98    }
99
100    /// Creates a sum of products, merging duplicates without numerical truncation.
101    pub fn from_terms(
102        num_modes: usize,
103        terms: impl IntoIterator<Item = (Vec<FermionicLadder>, Complex64)>,
104    ) -> Result<Self, QisError> {
105        let mut result = Self::new(num_modes);
106        for (term, coefficient) in terms {
107            result.add_term(term, coefficient)?;
108        }
109        Ok(result)
110    }
111
112    /// Adds a weighted product atomically; an error leaves this operator unchanged.
113    pub fn add_term(
114        &mut self,
115        term: impl IntoIterator<Item = FermionicLadder>,
116        coefficient: Complex64,
117    ) -> Result<(), QisError> {
118        validate_coefficient(coefficient)?;
119        let term: Vec<_> = term.into_iter().collect();
120        for ladder in &term {
121            let mode = ladder.mode();
122            if mode >= self.num_modes {
123                return Err(QisError::ModeOutOfBounds {
124                    mode,
125                    num_modes: self.num_modes,
126                });
127            }
128        }
129        let merged = self.terms.get(&term).copied().unwrap_or_default() + coefficient;
130        validate_coefficient(merged)?;
131        if merged == Complex64::new(0.0, 0.0) {
132            self.terms.remove(&term);
133        } else {
134            self.terms.insert(term, merged);
135        }
136        Ok(())
137    }
138
139    /// Creates a single creation operator.
140    pub fn creation(num_modes: usize, mode: usize) -> Result<Self, QisError> {
141        Self::from_term(num_modes, [FermionicLadder::Create(mode)], 1.0.into())
142    }
143
144    /// Creates a single annihilation operator.
145    pub fn annihilation(num_modes: usize, mode: usize) -> Result<Self, QisError> {
146        Self::from_term(num_modes, [FermionicLadder::Annihilate(mode)], 1.0.into())
147    }
148
149    /// Creates the number operator `a†_p a_p`.
150    pub fn number(num_modes: usize, mode: usize) -> Result<Self, QisError> {
151        Self::from_term(
152            num_modes,
153            [
154                FermionicLadder::Create(mode),
155                FermionicLadder::Annihilate(mode),
156            ],
157            1.0.into(),
158        )
159    }
160
161    /// Creates the total particle-number operator, or zero for zero modes.
162    pub fn total_number(num_modes: usize) -> Self {
163        let terms = (0..num_modes)
164            .map(|mode| {
165                (
166                    vec![
167                        FermionicLadder::Create(mode),
168                        FermionicLadder::Annihilate(mode),
169                    ],
170                    1.0.into(),
171                )
172            })
173            .collect();
174        Self { num_modes, terms }
175    }
176
177    /// Creates the unit-weight Hermitian excitation generator `G = i(T - T†)`.
178    ///
179    /// For `sources = [i_0, ..., i_{r-1}]` and `targets = [a_0, ..., a_{r-1}]`,
180    /// the ordered excitation is
181    /// `T = a†_{a_0} ... a†_{a_{r-1}} a_{i_{r-1}} ... a_{i_0}`.
182    /// The rightmost operator acts first. Thus `[i, j] -> [a, b]` means
183    /// `T = a†_a a†_b a_j a_i`.
184    ///
185    /// Input order is preserved, without implicit sorting or normal ordering.
186    /// An odd permutation of either list negates `G`; applying odd permutations
187    /// to both lists preserves it. Explicit [`Self::normal_order`] preserves
188    /// these fermionic signs. No factorial or other normalization is applied.
189    ///
190    /// Mapping `G` and evolving it with [`crate::circuit::ansatz::PauliEvolutionAnsatz`]
191    /// implements `exp(-i θ G) = exp(θ (T - T†))`. The evolution parameter belongs
192    /// to the ansatz, not this operator. The generator conserves total particle
193    /// number. Reference occupations and spin interpretations are caller choices.
194    ///
195    /// # Errors
196    ///
197    /// Returns [`QisError::InvalidParameterValue`] if the lists are empty, have
198    /// unequal lengths, contain repeated modes, or overlap. Returns
199    /// [`QisError::ModeOutOfBounds`] if an index is outside `0..num_modes`.
200    ///
201    /// # Example
202    /// ```
203    /// use cqlib_core::qis::FermionicOp;
204    /// let single = FermionicOp::excitation_generator(4, &[0], &[2])?;
205    /// let double = FermionicOp::excitation_generator(4, &[0, 1], &[2, 3])?;
206    /// assert!(single.is_hermitian(0.0)?);
207    /// assert!(double.is_hermitian(0.0)?);
208    /// # Ok::<(), cqlib_core::qis::QisError>(())
209    /// ```
210    pub fn excitation_generator(
211        num_modes: usize,
212        sources: &[usize],
213        targets: &[usize],
214    ) -> Result<Self, QisError> {
215        if sources.len() != targets.len() {
216            return Err(QisError::InvalidParameterValue(
217                "excitation sources and targets must have equal lengths".into(),
218            ));
219        }
220        if sources.is_empty() {
221            return Err(QisError::InvalidParameterValue(
222                "excitation sources and targets must be nonempty".into(),
223            ));
224        }
225
226        let mut source_modes = BTreeSet::new();
227        let mut target_modes = BTreeSet::new();
228        for (label, modes, seen) in [
229            ("sources", sources, &mut source_modes),
230            ("targets", targets, &mut target_modes),
231        ] {
232            for &mode in modes {
233                if mode >= num_modes {
234                    return Err(QisError::ModeOutOfBounds { mode, num_modes });
235                }
236                if !seen.insert(mode) {
237                    return Err(QisError::InvalidParameterValue(format!(
238                        "excitation {label} contain repeated mode {mode}"
239                    )));
240                }
241            }
242        }
243        if let Some(mode) = source_modes.intersection(&target_modes).next() {
244            return Err(QisError::InvalidParameterValue(format!(
245                "excitation sources and targets overlap at mode {mode}"
246            )));
247        }
248
249        let term = targets.iter().copied().map(FermionicLadder::Create).chain(
250            sources
251                .iter()
252                .rev()
253                .copied()
254                .map(FermionicLadder::Annihilate),
255        );
256        let excitation = Self::from_term(num_modes, term, 1.0.into())?;
257        excitation
258            .try_sub(&excitation.adjoint())?
259            .scaled(Complex64::new(0.0, 1.0))
260    }
261
262    /// Returns the declared mode count.
263    pub fn num_modes(&self) -> usize {
264        self.num_modes
265    }
266
267    /// Returns the number of stored ordered products.
268    pub fn num_terms(&self) -> usize {
269        self.terms.len()
270    }
271
272    /// Iterates over products and their coefficients in deterministic structural order.
273    pub fn terms(&self) -> impl ExactSizeIterator<Item = (&[FermionicLadder], Complex64)> + '_ {
274        self.terms
275            .iter()
276            .map(|(term, coefficient)| (term.as_slice(), *coefficient))
277    }
278
279    /// Reports whether the stored sum is zero. Algebraic cancellations require normal ordering.
280    pub fn is_zero(&self) -> bool {
281        self.terms.is_empty()
282    }
283
284    fn check_modes(&self, other: &Self) -> Result<(), QisError> {
285        if self.num_modes != other.num_modes {
286            return Err(QisError::ModeMismatch {
287                expected: self.num_modes,
288                actual: other.num_modes,
289            });
290        }
291        Ok(())
292    }
293
294    /// Adds operators on the same mode space, without modifying either operand.
295    pub fn try_add(&self, other: &Self) -> Result<Self, QisError> {
296        self.check_modes(other)?;
297        let mut result = self.clone();
298        for (term, coefficient) in other.terms() {
299            result.add_term(term.iter().copied(), coefficient)?;
300        }
301        Ok(result)
302    }
303
304    /// Subtracts operators on the same mode space.
305    pub fn try_sub(&self, other: &Self) -> Result<Self, QisError> {
306        self.check_modes(other)?;
307        let mut result = self.clone();
308        for (term, coefficient) in other.terms() {
309            result.add_term(term.iter().copied(), -coefficient)?;
310        }
311        Ok(result)
312    }
313
314    /// Forms `self * other`; the right operand acts first. Does not normal order.
315    pub fn try_mul(&self, other: &Self) -> Result<Self, QisError> {
316        self.check_modes(other)?;
317        let mut result = Self::new(self.num_modes);
318        for (left, a) in self.terms() {
319            for (right, b) in other.terms() {
320                result.add_term(left.iter().chain(right).copied(), a * b)?;
321            }
322        }
323        Ok(result)
324    }
325
326    /// Scales all coefficients. The factor is validated even for the zero operator.
327    pub fn scaled(&self, factor: Complex64) -> Result<Self, QisError> {
328        validate_coefficient(factor)?;
329        Self::from_terms(
330            self.num_modes,
331            self.terms
332                .iter()
333                .map(|(term, c)| (term.clone(), *c * factor)),
334        )
335    }
336
337    /// Takes the conjugate transpose, reversing each product without reordering it.
338    pub fn adjoint(&self) -> Self {
339        Self {
340            num_modes: self.num_modes,
341            terms: self
342                .terms
343                .iter()
344                .map(|(term, coefficient)| {
345                    (
346                        term.iter().rev().map(|ladder| ladder.adjoint()).collect(),
347                        coefficient.conj(),
348                    )
349                })
350                .collect(),
351        }
352    }
353
354    /// Expands using the canonical anticommutation relations.
355    ///
356    /// Creators precede annihilators; each group uses ascending mode indices.
357    /// This is an equivalent operator expansion, including contraction terms:
358    /// `a_p a†_p = I - a†_p a_p`. Small nonzero coefficients are retained.
359    /// An explicit work stack avoids recursion in long products.
360    /// Sorting a branch of k ladders takes O(k²) comparisons; contractions can
361    /// produce additional branches.
362    pub fn normal_order(&self) -> Result<Self, QisError> {
363        let mut result = Self::new(self.num_modes);
364        let mut pending: Vec<_> = self
365            .terms
366            .iter()
367            .map(|(term, c)| (term.clone(), *c, 0usize))
368            .collect();
369        while let Some((mut term, mut coefficient, mut index)) = pending.pop() {
370            let mut complete = true;
371            while index + 1 < term.len() {
372                let left = term[index];
373                let right = term[index + 1];
374                if left == right {
375                    complete = false;
376                    break;
377                }
378                if left > right {
379                    // Swapping distinct fermionic ladders contributes a minus sign.
380                    // Equal-mode annihilator/creator pairs also contribute a contraction.
381                    if let (FermionicLadder::Annihilate(p), FermionicLadder::Create(q)) =
382                        (left, right)
383                        && p == q
384                    {
385                        let mut contracted = term.clone();
386                        contracted.drain(index..index + 2);
387                        // The prefix is already sorted. Removing this pair only
388                        // requires rechecking the new boundary with that prefix.
389                        pending.push((contracted, coefficient, index.saturating_sub(1)));
390                    }
391                    term.swap(index, index + 1);
392                    coefficient = -coefficient;
393                    // Revisit the changed boundary, rather than scanning from zero.
394                    index = index.saturating_sub(1);
395                } else {
396                    index += 1;
397                }
398            }
399            if complete {
400                result.add_term(term, coefficient)?;
401            }
402        }
403        Ok(result)
404    }
405
406    /// Explicitly removes coefficients whose complex magnitude is at most `atol`.
407    /// Invalid tolerances leave the operator unchanged.
408    pub fn chop(&mut self, atol: f64) -> Result<(), QisError> {
409        validate_tolerance(atol)?;
410        self.terms
411            .retain(|_, coefficient| coefficient.norm() > atol);
412        Ok(())
413    }
414
415    /// Compares operators algebraically after normal ordering their difference.
416    /// Different mode counts are an error; `atol` must be finite and nonnegative.
417    pub fn equiv(&self, other: &Self, atol: f64) -> Result<bool, QisError> {
418        validate_tolerance(atol)?;
419        let difference = self.try_sub(other)?.normal_order()?;
420        Ok(difference
421            .terms
422            .values()
423            .all(|coefficient| coefficient.norm() <= atol))
424    }
425
426    /// Checks Hermiticity algebraically, allowing non-normal-ordered inputs.
427    pub fn is_hermitian(&self, atol: f64) -> Result<bool, QisError> {
428        self.equiv(&self.adjoint(), atol)
429    }
430}
431
432macro_rules! binary_operator {
433    ($trait:ident, $method:ident, $checked:ident) => {
434        impl $trait<&FermionicOp> for &FermionicOp {
435            type Output = Result<FermionicOp, QisError>;
436            fn $method(self, rhs: &FermionicOp) -> Self::Output {
437                self.$checked(rhs)
438            }
439        }
440        impl $trait<FermionicOp> for FermionicOp {
441            type Output = Result<FermionicOp, QisError>;
442            fn $method(self, rhs: FermionicOp) -> Self::Output {
443                self.$checked(&rhs)
444            }
445        }
446        impl $trait<&FermionicOp> for FermionicOp {
447            type Output = Result<FermionicOp, QisError>;
448            fn $method(self, rhs: &FermionicOp) -> Self::Output {
449                self.$checked(rhs)
450            }
451        }
452        impl $trait<FermionicOp> for &FermionicOp {
453            type Output = Result<FermionicOp, QisError>;
454            fn $method(self, rhs: FermionicOp) -> Self::Output {
455                self.$checked(&rhs)
456            }
457        }
458    };
459}
460
461binary_operator!(Add, add, try_add);
462binary_operator!(Sub, sub, try_sub);
463binary_operator!(Mul, mul, try_mul);
464
465macro_rules! scalar_operator {
466    ($scalar:ty) => {
467        impl Mul<$scalar> for &FermionicOp {
468            type Output = Result<FermionicOp, QisError>;
469            fn mul(self, rhs: $scalar) -> Self::Output {
470                self.scaled(rhs.into())
471            }
472        }
473        impl Mul<$scalar> for FermionicOp {
474            type Output = Result<FermionicOp, QisError>;
475            fn mul(self, rhs: $scalar) -> Self::Output {
476                self.scaled(rhs.into())
477            }
478        }
479        impl Mul<&FermionicOp> for $scalar {
480            type Output = Result<FermionicOp, QisError>;
481            fn mul(self, rhs: &FermionicOp) -> Self::Output {
482                rhs.scaled(self.into())
483            }
484        }
485        impl Mul<FermionicOp> for $scalar {
486            type Output = Result<FermionicOp, QisError>;
487            fn mul(self, rhs: FermionicOp) -> Self::Output {
488                rhs.scaled(self.into())
489            }
490        }
491    };
492}
493
494scalar_operator!(f64);
495scalar_operator!(Complex64);
496
497impl Neg for &FermionicOp {
498    type Output = FermionicOp;
499    fn neg(self) -> Self::Output {
500        let mut result = self.clone();
501        for coefficient in result.terms.values_mut() {
502            *coefficient = -*coefficient;
503        }
504        result
505    }
506}
507
508impl Neg for FermionicOp {
509    type Output = Self;
510    fn neg(mut self) -> Self::Output {
511        for coefficient in self.terms.values_mut() {
512            *coefficient = -*coefficient;
513        }
514        self
515    }
516}