Skip to main content

quantrs2_sim/
fermionic_simulation.rs

1//! Fermionic quantum simulation with `SciRS2` integration.
2//!
3//! This module provides comprehensive support for simulating fermionic systems,
4//! including Jordan-Wigner transformations, fermionic operators, and specialized
5//! algorithms for electronic structure and many-body fermionic systems.
6
7use scirs2_core::ndarray::{Array1, Array2, Array3};
8use scirs2_core::Complex64;
9use std::collections::HashMap;
10
11use crate::error::{Result, SimulatorError};
12use crate::pauli::{PauliOperator, PauliOperatorSum, PauliString};
13use crate::scirs2_integration::SciRS2Backend;
14
15/// Fermionic creation and annihilation operators
16#[derive(Debug, Clone, PartialEq, Eq, Hash)]
17pub enum FermionicOperator {
18    /// Creation operator c†_i
19    Creation(usize),
20    /// Annihilation operator `c_i`
21    Annihilation(usize),
22    /// Number operator `n_i` = c†_i `c_i`
23    Number(usize),
24    /// Hopping term c†_i `c_j`
25    Hopping { from: usize, to: usize },
26    /// Interaction term c†_i c†_j `c_k` `c_l`
27    Interaction { sites: [usize; 4] },
28}
29
30/// Fermionic operator string (product of fermionic operators)
31#[derive(Debug, Clone)]
32pub struct FermionicString {
33    /// Ordered list of fermionic operators
34    pub operators: Vec<FermionicOperator>,
35    /// Coefficient of the operator string
36    pub coefficient: Complex64,
37    /// Number of fermionic modes
38    pub num_modes: usize,
39}
40
41/// Sum of fermionic operator strings (fermionic Hamiltonian)
42#[derive(Debug, Clone)]
43pub struct FermionicHamiltonian {
44    /// Terms in the Hamiltonian
45    pub terms: Vec<FermionicString>,
46    /// Number of fermionic modes
47    pub num_modes: usize,
48    /// Whether the Hamiltonian is Hermitian
49    pub is_hermitian: bool,
50}
51
52/// Jordan-Wigner transformation for mapping fermions to qubits
53pub struct JordanWignerTransform {
54    /// Number of fermionic modes
55    num_modes: usize,
56    /// Cached Pauli string representations
57    pauli_cache: HashMap<FermionicOperator, PauliString>,
58}
59
60/// Fermionic simulator with `SciRS2` optimization
61pub struct FermionicSimulator {
62    /// Number of fermionic modes
63    num_modes: usize,
64    /// Jordan-Wigner transformer
65    jw_transform: JordanWignerTransform,
66    /// Current fermionic state (in qubit representation)
67    state: Array1<Complex64>,
68    /// `SciRS2` backend for optimization
69    backend: Option<SciRS2Backend>,
70    /// Simulation statistics
71    stats: FermionicStats,
72}
73
74/// Statistics for fermionic simulation
75#[derive(Debug, Clone, Default)]
76pub struct FermionicStats {
77    /// Number of Jordan-Wigner transformations performed
78    pub jw_transformations: usize,
79    /// Number of fermionic operators applied
80    pub fermionic_ops_applied: usize,
81    /// Time spent in Jordan-Wigner transformation
82    pub jw_time_ms: f64,
83    /// Memory usage for operator storage
84    pub operator_memory_bytes: usize,
85    /// Maximum Pauli string length encountered
86    pub max_pauli_string_length: usize,
87}
88
89impl FermionicOperator {
90    /// Check if operator is creation type
91    #[must_use]
92    pub const fn is_creation(&self) -> bool {
93        matches!(self, Self::Creation(_))
94    }
95
96    /// Check if operator is annihilation type
97    #[must_use]
98    pub const fn is_annihilation(&self) -> bool {
99        matches!(self, Self::Annihilation(_))
100    }
101
102    /// Get site index for single-site operators
103    #[must_use]
104    pub const fn site(&self) -> Option<usize> {
105        match self {
106            Self::Creation(i) | Self::Annihilation(i) | Self::Number(i) => Some(*i),
107            _ => None,
108        }
109    }
110
111    /// Get canonical ordering for operator comparison
112    #[must_use]
113    pub fn ordering_key(&self) -> (usize, usize) {
114        match self {
115            Self::Creation(i) => (1, *i),
116            Self::Annihilation(i) => (0, *i),
117            Self::Number(i) => (2, *i),
118            Self::Hopping { from, to } => (3, from.min(to) * 1000 + from.max(to)),
119            Self::Interaction { sites } => {
120                let mut sorted_sites = *sites;
121                sorted_sites.sort_unstable();
122                (
123                    4,
124                    sorted_sites[0] * 1_000_000
125                        + sorted_sites[1] * 10_000
126                        + sorted_sites[2] * 100
127                        + sorted_sites[3],
128                )
129            }
130        }
131    }
132}
133
134impl FermionicString {
135    /// Create new fermionic string
136    #[must_use]
137    pub const fn new(
138        operators: Vec<FermionicOperator>,
139        coefficient: Complex64,
140        num_modes: usize,
141    ) -> Self {
142        Self {
143            operators,
144            coefficient,
145            num_modes,
146        }
147    }
148
149    /// Create single fermionic operator
150    #[must_use]
151    pub fn single_operator(
152        op: FermionicOperator,
153        coefficient: Complex64,
154        num_modes: usize,
155    ) -> Self {
156        Self::new(vec![op], coefficient, num_modes)
157    }
158
159    /// Create creation operator c†_i
160    #[must_use]
161    pub fn creation(site: usize, coefficient: Complex64, num_modes: usize) -> Self {
162        Self::single_operator(FermionicOperator::Creation(site), coefficient, num_modes)
163    }
164
165    /// Create annihilation operator `c_i`
166    #[must_use]
167    pub fn annihilation(site: usize, coefficient: Complex64, num_modes: usize) -> Self {
168        Self::single_operator(
169            FermionicOperator::Annihilation(site),
170            coefficient,
171            num_modes,
172        )
173    }
174
175    /// Create number operator `n_i`
176    #[must_use]
177    pub fn number(site: usize, coefficient: Complex64, num_modes: usize) -> Self {
178        Self::single_operator(FermionicOperator::Number(site), coefficient, num_modes)
179    }
180
181    /// Create hopping term t c†_i `c_j`
182    #[must_use]
183    pub fn hopping(from: usize, to: usize, coefficient: Complex64, num_modes: usize) -> Self {
184        Self::single_operator(
185            FermionicOperator::Hopping { from, to },
186            coefficient,
187            num_modes,
188        )
189    }
190
191    /// Multiply two fermionic strings
192    pub fn multiply(&self, other: &Self) -> Result<Self> {
193        if self.num_modes != other.num_modes {
194            return Err(SimulatorError::DimensionMismatch(
195                "Fermionic strings must have same number of modes".to_string(),
196            ));
197        }
198
199        let mut result_ops = self.operators.clone();
200        result_ops.extend(other.operators.clone());
201
202        // Apply fermionic anticommutation rules
203        let (canonical_ops, sign) = self.canonicalize_operators(&result_ops)?;
204
205        Ok(Self {
206            operators: canonical_ops,
207            coefficient: self.coefficient * other.coefficient * sign,
208            num_modes: self.num_modes,
209        })
210    }
211
212    /// Canonicalize fermionic operators (apply anticommutation)
213    fn canonicalize_operators(
214        &self,
215        ops: &[FermionicOperator],
216    ) -> Result<(Vec<FermionicOperator>, Complex64)> {
217        let mut canonical = ops.to_vec();
218        let mut sign = Complex64::new(1.0, 0.0);
219
220        // Bubble sort with fermionic anticommutation
221        for i in 0..canonical.len() {
222            for j in (i + 1)..canonical.len() {
223                if canonical[i].ordering_key() > canonical[j].ordering_key() {
224                    // Swap with anticommutation sign
225                    canonical.swap(i, j);
226                    sign *= Complex64::new(-1.0, 0.0);
227                }
228            }
229        }
230
231        // Apply fermionic algebra rules (c_i c_i = 0, c†_i c_i = n_i, etc.)
232        let simplified = self.apply_fermionic_algebra(&canonical)?;
233
234        Ok((simplified, sign))
235    }
236
237    /// Apply fermionic algebra rules
238    fn apply_fermionic_algebra(&self, ops: &[FermionicOperator]) -> Result<Vec<FermionicOperator>> {
239        let mut result = Vec::new();
240        let mut i = 0;
241
242        while i < ops.len() {
243            if i + 1 < ops.len() {
244                match (&ops[i], &ops[i + 1]) {
245                    // c†_i c_i = n_i
246                    (FermionicOperator::Creation(a), FermionicOperator::Annihilation(b))
247                        if a == b =>
248                    {
249                        result.push(FermionicOperator::Number(*a));
250                        i += 2;
251                    }
252                    // c_i c_i = 0 (skip both)
253                    (FermionicOperator::Annihilation(a), FermionicOperator::Annihilation(b))
254                        if a == b =>
255                    {
256                        // Result is zero - would need to handle this properly
257                        i += 2;
258                    }
259                    // c†_i c†_i = 0 (skip both)
260                    (FermionicOperator::Creation(a), FermionicOperator::Creation(b)) if a == b => {
261                        // Result is zero - would need to handle this properly
262                        i += 2;
263                    }
264                    _ => {
265                        result.push(ops[i].clone());
266                        i += 1;
267                    }
268                }
269            } else {
270                result.push(ops[i].clone());
271                i += 1;
272            }
273        }
274
275        Ok(result)
276    }
277
278    /// Compute Hermitian conjugate
279    #[must_use]
280    pub fn hermitian_conjugate(&self) -> Self {
281        let mut conjugate_ops = Vec::new();
282
283        // Reverse order and conjugate each operator
284        for op in self.operators.iter().rev() {
285            let conjugate_op = match op {
286                FermionicOperator::Creation(i) => FermionicOperator::Annihilation(*i),
287                FermionicOperator::Annihilation(i) => FermionicOperator::Creation(*i),
288                FermionicOperator::Number(i) => FermionicOperator::Number(*i),
289                FermionicOperator::Hopping { from, to } => FermionicOperator::Hopping {
290                    from: *to,
291                    to: *from,
292                },
293                FermionicOperator::Interaction { sites } => {
294                    // Reverse the order for interaction terms
295                    let mut rev_sites = *sites;
296                    rev_sites.reverse();
297                    FermionicOperator::Interaction { sites: rev_sites }
298                }
299            };
300            conjugate_ops.push(conjugate_op);
301        }
302
303        Self {
304            operators: conjugate_ops,
305            coefficient: self.coefficient.conj(),
306            num_modes: self.num_modes,
307        }
308    }
309}
310
311impl FermionicHamiltonian {
312    /// Create new fermionic Hamiltonian
313    #[must_use]
314    pub const fn new(num_modes: usize) -> Self {
315        Self {
316            terms: Vec::new(),
317            num_modes,
318            is_hermitian: true,
319        }
320    }
321
322    /// Add term to Hamiltonian
323    pub fn add_term(&mut self, term: FermionicString) -> Result<()> {
324        if term.num_modes != self.num_modes {
325            return Err(SimulatorError::DimensionMismatch(
326                "Term must have same number of modes as Hamiltonian".to_string(),
327            ));
328        }
329
330        self.terms.push(term);
331        Ok(())
332    }
333
334    /// Add Hermitian conjugate terms automatically
335    pub fn make_hermitian(&mut self) {
336        let mut conjugate_terms = Vec::new();
337
338        for term in &self.terms {
339            let conjugate = term.hermitian_conjugate();
340            // Only add if it's different from the original term
341            if !self.terms_equal(term, &conjugate) {
342                conjugate_terms.push(conjugate);
343            }
344        }
345
346        self.terms.extend(conjugate_terms);
347        self.is_hermitian = true;
348    }
349
350    /// Check if two terms are equal
351    fn terms_equal(&self, term1: &FermionicString, term2: &FermionicString) -> bool {
352        term1.operators == term2.operators && (term1.coefficient - term2.coefficient).norm() < 1e-12
353    }
354
355    /// Create molecular Hamiltonian
356    pub fn molecular_hamiltonian(
357        num_modes: usize,
358        one_body_integrals: &Array2<f64>,
359        two_body_integrals: &Array3<f64>,
360    ) -> Result<Self> {
361        let mut hamiltonian = Self::new(num_modes);
362
363        // One-body terms: ∑_{i,j} h_{ij} c†_i c_j
364        for i in 0..num_modes {
365            for j in 0..num_modes {
366                if one_body_integrals[[i, j]].abs() > 1e-12 {
367                    let coeff = Complex64::new(one_body_integrals[[i, j]], 0.0);
368                    let term = FermionicString::new(
369                        vec![
370                            FermionicOperator::Creation(i),
371                            FermionicOperator::Annihilation(j),
372                        ],
373                        coeff,
374                        num_modes,
375                    );
376                    hamiltonian.add_term(term)?;
377                }
378            }
379        }
380
381        // Two-body terms: ∑_{i,j,k,l} V_{ijkl} c†_i c†_j c_l c_k
382        for i in 0..num_modes {
383            for j in 0..num_modes {
384                for k in 0..num_modes {
385                    if two_body_integrals[[i, j, k]].abs() > 1e-12 {
386                        for l in 0..num_modes {
387                            let coeff = Complex64::new(0.5 * two_body_integrals[[i, j, k]], 0.0);
388                            let term = FermionicString::new(
389                                vec![
390                                    FermionicOperator::Creation(i),
391                                    FermionicOperator::Creation(j),
392                                    FermionicOperator::Annihilation(l),
393                                    FermionicOperator::Annihilation(k),
394                                ],
395                                coeff,
396                                num_modes,
397                            );
398                            hamiltonian.add_term(term)?;
399                        }
400                    }
401                }
402            }
403        }
404
405        hamiltonian.make_hermitian();
406        Ok(hamiltonian)
407    }
408
409    /// Create Hubbard model Hamiltonian
410    pub fn hubbard_model(
411        sites: usize,
412        hopping: f64,
413        interaction: f64,
414        chemical_potential: f64,
415    ) -> Result<Self> {
416        let num_modes = 2 * sites; // Spin up and spin down
417        let mut hamiltonian = Self::new(num_modes);
418
419        // Hopping terms: -t ∑_{⟨i,j⟩,σ} (c†_{i,σ} c_{j,σ} + h.c.)
420        for i in 0..sites {
421            for sigma in 0..2 {
422                let site_i = 2 * i + sigma;
423
424                // Nearest neighbor hopping (1D chain)
425                if i + 1 < sites {
426                    let site_j = 2 * (i + 1) + sigma;
427
428                    // Forward hopping
429                    let hopping_term = FermionicString::hopping(
430                        site_i,
431                        site_j,
432                        Complex64::new(-hopping, 0.0),
433                        num_modes,
434                    );
435                    hamiltonian.add_term(hopping_term)?;
436
437                    // Backward hopping (Hermitian conjugate)
438                    let back_hopping_term = FermionicString::hopping(
439                        site_j,
440                        site_i,
441                        Complex64::new(-hopping, 0.0),
442                        num_modes,
443                    );
444                    hamiltonian.add_term(back_hopping_term)?;
445                }
446            }
447        }
448
449        // Interaction terms: U ∑_i n_{i,↑} n_{i,↓}
450        for i in 0..sites {
451            let up_site = 2 * i;
452            let down_site = 2 * i + 1;
453
454            let interaction_term = FermionicString::new(
455                vec![
456                    FermionicOperator::Number(up_site),
457                    FermionicOperator::Number(down_site),
458                ],
459                Complex64::new(interaction, 0.0),
460                num_modes,
461            );
462            hamiltonian.add_term(interaction_term)?;
463        }
464
465        // Chemical potential terms: -μ ∑_{i,σ} n_{i,σ}
466        for i in 0..num_modes {
467            let mu_term =
468                FermionicString::number(i, Complex64::new(-chemical_potential, 0.0), num_modes);
469            hamiltonian.add_term(mu_term)?;
470        }
471
472        Ok(hamiltonian)
473    }
474}
475
476impl JordanWignerTransform {
477    /// Create new Jordan-Wigner transformer
478    #[must_use]
479    pub fn new(num_modes: usize) -> Self {
480        Self {
481            num_modes,
482            pauli_cache: HashMap::new(),
483        }
484    }
485
486    /// Transform a single fermionic operator to its complete Pauli operator sum via JW.
487    ///
488    /// Returns the full Jordan-Wigner representation as a `PauliOperatorSum` (possibly
489    /// containing multiple terms for operators like creation/annihilation).
490    ///
491    /// JW mappings (n = num_modes, Z_k below denotes Z on mode k):
492    ///
493    /// - `n_i = (I − Z_i)/2`           → two Pauli strings (constant + Z_i)
494    /// - `c†_i = (Z_0⋯Z_{i-1})(X_i − iY_i)/2` → two Pauli strings
495    /// - `c_i  = (Z_0⋯Z_{i-1})(X_i + iY_i)/2` → two Pauli strings
496    /// - `Hopping c†_from c_to` via full product    → four raw terms reduced to two
497    /// - `Interaction c†⋯c_l` via full product
498    pub fn transform_operator_to_sum(
499        &mut self,
500        op: &FermionicOperator,
501    ) -> Result<PauliOperatorSum> {
502        let mut sum = PauliOperatorSum::new(self.num_modes);
503        match op {
504            FermionicOperator::Number(site) => {
505                // n_i = (I - Z_i)/2 = 0.5·I - 0.5·Z_i
506                let identity = PauliString::new(self.num_modes); // coefficient = 1.0, all I
507                let mut id = identity;
508                id.coefficient = Complex64::new(0.5, 0.0);
509                sum.add_term(id)?;
510
511                let z_term =
512                    self.single_site_pauli(*site, PauliOperator::Z, Complex64::new(-0.5, 0.0))?;
513                sum.add_term(z_term)?;
514            }
515            FermionicOperator::Creation(site) => {
516                // c†_i = (Z_{0..i} X_i)/2 − i(Z_{0..i} Y_i)/2
517                let x_term =
518                    self.jw_pauli_string(*site, PauliOperator::X, Complex64::new(0.5, 0.0))?;
519                let y_term =
520                    self.jw_pauli_string(*site, PauliOperator::Y, Complex64::new(0.0, -0.5))?;
521                sum.add_term(x_term)?;
522                sum.add_term(y_term)?;
523            }
524            FermionicOperator::Annihilation(site) => {
525                // c_i = (Z_{0..i} X_i)/2 + i(Z_{0..i} Y_i)/2
526                let x_term =
527                    self.jw_pauli_string(*site, PauliOperator::X, Complex64::new(0.5, 0.0))?;
528                let y_term =
529                    self.jw_pauli_string(*site, PauliOperator::Y, Complex64::new(0.0, 0.5))?;
530                sum.add_term(x_term)?;
531                sum.add_term(y_term)?;
532            }
533            FermionicOperator::Hopping { from, to } => {
534                // c†_from c_to: multiply the two full JW sums
535                let creation_sum =
536                    self.transform_operator_to_sum(&FermionicOperator::Creation(*from))?;
537                let annihilation_sum =
538                    self.transform_operator_to_sum(&FermionicOperator::Annihilation(*to))?;
539                for ca in &creation_sum.terms {
540                    for an in &annihilation_sum.terms {
541                        let product = ca.multiply(an)?;
542                        // Skip near-zero terms
543                        if product.coefficient.norm() > 1e-15 {
544                            sum.add_term(product)?;
545                        }
546                    }
547                }
548            }
549            FermionicOperator::Interaction { sites } => {
550                // c†_{s0} c†_{s1} c_{s2} c_{s3}: full product
551                let sums: Vec<PauliOperatorSum> = [
552                    FermionicOperator::Creation(sites[0]),
553                    FermionicOperator::Creation(sites[1]),
554                    FermionicOperator::Annihilation(sites[2]),
555                    FermionicOperator::Annihilation(sites[3]),
556                ]
557                .iter()
558                .map(|fop| self.transform_operator_to_sum(fop))
559                .collect::<Result<Vec<_>>>()?;
560
561                // Iteratively multiply all operator sums
562                let mut current: Vec<PauliString> = sums[0].terms.clone();
563                for next_sum in sums.iter().skip(1) {
564                    let mut new_current = Vec::new();
565                    for ca in &current {
566                        for nb in &next_sum.terms {
567                            let product = ca.multiply(nb)?;
568                            if product.coefficient.norm() > 1e-15 {
569                                new_current.push(product);
570                            }
571                        }
572                    }
573                    current = new_current;
574                }
575                for term in current {
576                    sum.add_term(term)?;
577                }
578            }
579        }
580        Ok(sum)
581    }
582
583    /// Build a JW Pauli string: Z_{0}⋯Z_{site-1} op_{site} I_{site+1}⋯ with given coefficient.
584    fn jw_pauli_string(
585        &self,
586        site: usize,
587        op: PauliOperator,
588        coeff: Complex64,
589    ) -> Result<PauliString> {
590        if site >= self.num_modes {
591            return Err(SimulatorError::IndexOutOfBounds(site));
592        }
593        let mut paulis = vec![PauliOperator::I; self.num_modes];
594        paulis[..site].fill(PauliOperator::Z);
595        paulis[site] = op;
596        let ops: Vec<(usize, PauliOperator)> = paulis
597            .iter()
598            .enumerate()
599            .filter(|(_, &p)| p != PauliOperator::I)
600            .map(|(i, &p)| (i, p))
601            .collect();
602        PauliString::from_ops(self.num_modes, &ops, coeff)
603    }
604
605    /// Build a single-site Pauli string (no JW Z-string): I⋯I op_site I⋯I.
606    fn single_site_pauli(
607        &self,
608        site: usize,
609        op: PauliOperator,
610        coeff: Complex64,
611    ) -> Result<PauliString> {
612        if site >= self.num_modes {
613            return Err(SimulatorError::IndexOutOfBounds(site));
614        }
615        PauliString::from_ops(self.num_modes, &[(site, op)], coeff)
616    }
617
618    /// Transform fermionic operator to a single representative Pauli string.
619    ///
620    /// This returns only the X-part of the JW decomposition (for creation/annihilation)
621    /// or the Z-only part of the number operator, and is used internally for
622    /// Pauli-composition chains.  For complete expectation values use
623    /// `transform_operator_to_sum`.
624    pub fn transform_operator(&mut self, op: &FermionicOperator) -> Result<PauliString> {
625        if let Some(cached) = self.pauli_cache.get(op) {
626            return Ok(cached.clone());
627        }
628
629        let pauli_string = match op {
630            FermionicOperator::Creation(i) => {
631                self.jw_pauli_string(*i, PauliOperator::X, Complex64::new(0.5, 0.0))?
632            }
633            FermionicOperator::Annihilation(i) => {
634                self.jw_pauli_string(*i, PauliOperator::X, Complex64::new(0.5, 0.0))?
635            }
636            FermionicOperator::Number(i) => {
637                // Only Z term; identity term handled in transform_operator_to_sum
638                self.single_site_pauli(*i, PauliOperator::Z, Complex64::new(-0.5, 0.0))?
639            }
640            FermionicOperator::Hopping { from, to } => self
641                .creation_to_pauli(*from)?
642                .multiply(&self.annihilation_to_pauli(*to)?)?,
643            FermionicOperator::Interaction { sites } => self
644                .creation_to_pauli(sites[0])?
645                .multiply(&self.creation_to_pauli(sites[1])?)?
646                .multiply(&self.annihilation_to_pauli(sites[2])?)?
647                .multiply(&self.annihilation_to_pauli(sites[3])?)?,
648        };
649
650        self.pauli_cache.insert(op.clone(), pauli_string.clone());
651        Ok(pauli_string)
652    }
653
654    /// Transform creation operator c†_i to single Pauli string (X-part of JW).
655    fn creation_to_pauli(&self, site: usize) -> Result<PauliString> {
656        if site >= self.num_modes {
657            return Err(SimulatorError::IndexOutOfBounds(site));
658        }
659        self.jw_pauli_string(site, PauliOperator::X, Complex64::new(0.5, 0.0))
660    }
661
662    /// Transform annihilation operator c_i to single Pauli string (X-part of JW).
663    fn annihilation_to_pauli(&self, site: usize) -> Result<PauliString> {
664        if site >= self.num_modes {
665            return Err(SimulatorError::IndexOutOfBounds(site));
666        }
667        self.jw_pauli_string(site, PauliOperator::X, Complex64::new(0.5, 0.0))
668    }
669
670    /// Transform hopping term c†_from c_to to Pauli string (X-part only).
671    fn hopping_to_pauli(&self, from: usize, to: usize) -> Result<PauliString> {
672        self.creation_to_pauli(from)?
673            .multiply(&self.annihilation_to_pauli(to)?)
674    }
675
676    /// Transform four-body interaction c†_{s0} c†_{s1} c_{s2} c_{s3} to Pauli string.
677    fn interaction_to_pauli(&self, sites: [usize; 4]) -> Result<PauliString> {
678        self.creation_to_pauli(sites[0])?
679            .multiply(&self.creation_to_pauli(sites[1])?)?
680            .multiply(&self.annihilation_to_pauli(sites[2])?)?
681            .multiply(&self.annihilation_to_pauli(sites[3])?)
682    }
683
684    /// Transform fermionic string to Pauli operator sum.
685    ///
686    /// Each fermionic operator is expanded to its complete JW Pauli representation;
687    /// successive operator sums are then multiplied together (outer product).
688    /// The overall fermionic coefficient is applied last.
689    pub fn transform_string(
690        &mut self,
691        fermionic_string: &FermionicString,
692    ) -> Result<PauliOperatorSum> {
693        let mut pauli_sum = PauliOperatorSum::new(self.num_modes);
694
695        if fermionic_string.operators.is_empty() {
696            let mut identity_string = PauliString::new(self.num_modes);
697            identity_string.coefficient = fermionic_string.coefficient;
698            pauli_sum.add_term(identity_string)?;
699            return Ok(pauli_sum);
700        }
701
702        // Expand each fermionic operator to its complete JW sum (possibly multi-term),
703        // then multiply the sums together.
704        let mut current: Vec<PauliString> = {
705            let first_sum = self.transform_operator_to_sum(&fermionic_string.operators[0])?;
706            first_sum.terms
707        };
708
709        for op in fermionic_string.operators.iter().skip(1) {
710            let next_sum = self.transform_operator_to_sum(op)?;
711            let mut new_current = Vec::new();
712            for ca in &current {
713                for nb in &next_sum.terms {
714                    let product = ca.multiply(nb)?;
715                    if product.coefficient.norm() > 1e-15 {
716                        new_current.push(product);
717                    }
718                }
719            }
720            current = new_current;
721        }
722
723        // Apply the overall fermionic coefficient and collect terms
724        for mut term in current {
725            term.coefficient *= fermionic_string.coefficient;
726            pauli_sum.add_term(term)?;
727        }
728
729        Ok(pauli_sum)
730    }
731
732    /// Transform fermionic Hamiltonian to Pauli Hamiltonian
733    pub fn transform_hamiltonian(
734        &mut self,
735        hamiltonian: &FermionicHamiltonian,
736    ) -> Result<PauliOperatorSum> {
737        let mut pauli_hamiltonian = PauliOperatorSum::new(self.num_modes);
738
739        for term in &hamiltonian.terms {
740            let pauli_terms = self.transform_string(term)?;
741            for pauli_term in pauli_terms.terms {
742                let _ = pauli_hamiltonian.add_term(pauli_term);
743            }
744        }
745
746        Ok(pauli_hamiltonian)
747    }
748}
749
750impl FermionicSimulator {
751    /// Create new fermionic simulator
752    pub fn new(num_modes: usize) -> Result<Self> {
753        let dim = 1 << num_modes;
754        let mut state = Array1::zeros(dim);
755        state[0] = Complex64::new(1.0, 0.0); // |0...0⟩ (vacuum state)
756
757        Ok(Self {
758            num_modes,
759            jw_transform: JordanWignerTransform::new(num_modes),
760            state,
761            backend: None,
762            stats: FermionicStats::default(),
763        })
764    }
765
766    /// Initialize with `SciRS2` backend
767    pub fn with_scirs2_backend(mut self) -> Result<Self> {
768        self.backend = Some(SciRS2Backend::new());
769        Ok(self)
770    }
771
772    /// Set initial fermionic state
773    pub fn set_initial_state(&mut self, occupation: &[bool]) -> Result<()> {
774        if occupation.len() != self.num_modes {
775            return Err(SimulatorError::DimensionMismatch(
776                "Occupation must match number of modes".to_string(),
777            ));
778        }
779
780        // Create Fock state |n_0, n_1, ..., n_{N-1}⟩
781        let mut index = 0;
782        for (i, &occupied) in occupation.iter().enumerate() {
783            if occupied {
784                index |= 1 << (self.num_modes - 1 - i);
785            }
786        }
787
788        self.state.fill(Complex64::new(0.0, 0.0));
789        self.state[index] = Complex64::new(1.0, 0.0);
790
791        Ok(())
792    }
793
794    /// Apply fermionic operator
795    pub fn apply_fermionic_operator(&mut self, op: &FermionicOperator) -> Result<()> {
796        let start_time = std::time::Instant::now();
797
798        // Transform to Pauli representation
799        let pauli_string = self.jw_transform.transform_operator(op)?;
800
801        // Apply Pauli string to state
802        self.apply_pauli_string(&pauli_string)?;
803
804        self.stats.fermionic_ops_applied += 1;
805        self.stats.jw_transformations += 1;
806        self.stats.jw_time_ms += start_time.elapsed().as_secs_f64() * 1000.0;
807
808        Ok(())
809    }
810
811    /// Apply fermionic string
812    pub fn apply_fermionic_string(&mut self, fermionic_string: &FermionicString) -> Result<()> {
813        let pauli_sum = self.jw_transform.transform_string(fermionic_string)?;
814
815        // Apply each Pauli term
816        for pauli_term in &pauli_sum.terms {
817            self.apply_pauli_string(pauli_term)?;
818        }
819
820        Ok(())
821    }
822
823    /// Apply a Pauli string in-place to `self.state`.
824    ///
825    /// For each qubit/mode `q` in order:
826    ///   - I → no-op
827    ///   - Z → multiply amplitude by −1 for all states with bit `q_bit` set
828    ///   - X → swap amplitude pairs (i, i XOR (1<<q_bit))
829    ///   - Y → X-then-Z combined with ±i phase: swap and multiply
830    ///
831    /// The state-vector index bit ordering follows `set_initial_state`:
832    /// mode `q` corresponds to bit `n_modes − 1 − q` of the state index.
833    ///
834    /// After all single-qubit operators are applied the overall coefficient
835    /// of the Pauli string is multiplied into every amplitude.
836    fn apply_pauli_string(&mut self, pauli_string: &PauliString) -> Result<()> {
837        let n = self.num_modes;
838        let size = self.state.len();
839
840        for (q, &op) in pauli_string.operators.iter().enumerate() {
841            let q_bit = n - 1 - q;
842            match op {
843                PauliOperator::I => {}
844                PauliOperator::Z => {
845                    for i in 0..size {
846                        if (i >> q_bit) & 1 == 1 {
847                            self.state[i] = -self.state[i];
848                        }
849                    }
850                }
851                PauliOperator::X => {
852                    for i in 0..size {
853                        if (i >> q_bit) & 1 == 0 {
854                            let j = i | (1 << q_bit);
855                            self.state.swap(i, j);
856                        }
857                    }
858                }
859                PauliOperator::Y => {
860                    // Y = i·X·Z: swap with phase ±i
861                    // Y|0⟩ = i|1⟩,  Y|1⟩ = -i|0⟩
862                    for i in 0..size {
863                        if (i >> q_bit) & 1 == 0 {
864                            let j = i | (1 << q_bit);
865                            let a = self.state[i];
866                            let b = self.state[j];
867                            self.state[i] = Complex64::new(0.0, 1.0) * b;
868                            self.state[j] = Complex64::new(0.0, -1.0) * a;
869                        }
870                    }
871                }
872            }
873        }
874
875        // Apply overall coefficient
876        let coeff = pauli_string.coefficient;
877        for amp in self.state.iter_mut() {
878            *amp *= coeff;
879        }
880
881        Ok(())
882    }
883
884    /// Compute expectation value of a fermionic operator in the current state.
885    ///
886    /// Uses the complete JW expansion (`transform_operator_to_sum`) so that
887    /// all Pauli string terms — including constant identity contributions — are
888    /// correctly included in the expectation value.
889    pub fn expectation_value(&mut self, op: &FermionicOperator) -> Result<Complex64> {
890        let pauli_sum = self.jw_transform.transform_operator_to_sum(op)?;
891
892        let mut total = Complex64::new(0.0, 0.0);
893        for term in &pauli_sum.terms {
894            total += self.compute_pauli_expectation(term)?;
895        }
896        Ok(total)
897    }
898
899    /// Compute ⟨ψ|P|ψ⟩ for a Pauli string P without mutating `self.state`.
900    ///
901    /// The Pauli string is applied analytically to a clone of the state vector
902    /// and then the inner product with the original state is taken.
903    fn compute_pauli_expectation(&self, pauli_string: &PauliString) -> Result<Complex64> {
904        let n = self.num_modes;
905        let size = self.state.len();
906        let mut psi_prime = self.state.clone();
907
908        // Apply each Pauli operator to the cloned state (without the coefficient)
909        for (q, &op) in pauli_string.operators.iter().enumerate() {
910            let q_bit = n - 1 - q;
911            match op {
912                PauliOperator::I => {}
913                PauliOperator::Z => {
914                    for i in 0..size {
915                        if (i >> q_bit) & 1 == 1 {
916                            psi_prime[i] = -psi_prime[i];
917                        }
918                    }
919                }
920                PauliOperator::X => {
921                    for i in 0..size {
922                        if (i >> q_bit) & 1 == 0 {
923                            let j = i | (1 << q_bit);
924                            psi_prime.swap(i, j);
925                        }
926                    }
927                }
928                PauliOperator::Y => {
929                    for i in 0..size {
930                        if (i >> q_bit) & 1 == 0 {
931                            let j = i | (1 << q_bit);
932                            let a = psi_prime[i];
933                            let b = psi_prime[j];
934                            psi_prime[i] = Complex64::new(0.0, 1.0) * b;
935                            psi_prime[j] = Complex64::new(0.0, -1.0) * a;
936                        }
937                    }
938                }
939            }
940        }
941
942        // ⟨ψ|P|ψ⟩ = coeff * Σ_i conj(ψ[i]) * (P|ψ⟩)[i]
943        let raw: Complex64 = self
944            .state
945            .iter()
946            .zip(psi_prime.iter())
947            .map(|(&a, &b)| a.conj() * b)
948            .sum();
949
950        Ok(pauli_string.coefficient * raw)
951    }
952
953    /// Evolve under fermionic Hamiltonian
954    pub fn evolve_hamiltonian(
955        &mut self,
956        hamiltonian: &FermionicHamiltonian,
957        time: f64,
958    ) -> Result<()> {
959        // Transform to Pauli Hamiltonian
960        let pauli_hamiltonian = self.jw_transform.transform_hamiltonian(hamiltonian)?;
961
962        // Evolve under Pauli Hamiltonian (would use Trotter-Suzuki or exact methods)
963        self.evolve_pauli_hamiltonian(&pauli_hamiltonian, time)?;
964
965        Ok(())
966    }
967
968    /// Apply a single Pauli string action P|ψ⟩ to `target` (in-place).
969    ///
970    /// The `target` array is modified by the unit Pauli operators (coefficient ignored).
971    fn apply_pauli_operators(
972        operators: &[PauliOperator],
973        num_modes: usize,
974        target: &mut Array1<Complex64>,
975    ) {
976        let size = target.len();
977        for (q, &op) in operators.iter().enumerate() {
978            let q_bit = num_modes - 1 - q;
979            match op {
980                PauliOperator::I => {}
981                PauliOperator::Z => {
982                    for i in 0..size {
983                        if (i >> q_bit) & 1 == 1 {
984                            target[i] = -target[i];
985                        }
986                    }
987                }
988                PauliOperator::X => {
989                    for i in 0..size {
990                        if (i >> q_bit) & 1 == 0 {
991                            let j = i | (1 << q_bit);
992                            target.swap(i, j);
993                        }
994                    }
995                }
996                PauliOperator::Y => {
997                    for i in 0..size {
998                        if (i >> q_bit) & 1 == 0 {
999                            let j = i | (1 << q_bit);
1000                            let a = target[i];
1001                            let b = target[j];
1002                            target[i] = Complex64::new(0.0, 1.0) * b;
1003                            target[j] = Complex64::new(0.0, -1.0) * a;
1004                        }
1005                    }
1006                }
1007            }
1008        }
1009    }
1010
1011    /// Evolve the state under a Pauli Hamiltonian for time `t` using first-order Trotter.
1012    ///
1013    /// Before applying the Trotter steps, terms with the same Pauli operator pattern
1014    /// are merged by summing their coefficients.  This is essential for Hermitian
1015    /// Hamiltonians derived from JW transformation, where conjugate-pair terms have
1016    /// imaginary coefficients that must cancel before the exponential is applied.
1017    ///
1018    /// For each merged term `c·P` (c real after merging, P² = I Hermitian Pauli):
1019    ///   exp(−i·t·c·P) = cos(c·t)·I − i·sin(c·t)·P
1020    ///
1021    /// For residual terms with complex coefficients (non-Hermitian parts), the
1022    /// complete formula exp(−i·t·c·P) = cos(|c|t)·I − i·(c/|c|)·sin(|c|t)·P is used.
1023    fn evolve_pauli_hamiltonian(
1024        &mut self,
1025        hamiltonian: &PauliOperatorSum,
1026        time: f64,
1027    ) -> Result<()> {
1028        // Step 1: merge terms by Pauli operator pattern (sum coefficients)
1029        let mut merged: HashMap<Vec<PauliOperator>, Complex64> = HashMap::new();
1030        for term in &hamiltonian.terms {
1031            *merged
1032                .entry(term.operators.clone())
1033                .or_insert(Complex64::new(0.0, 0.0)) += term.coefficient;
1034        }
1035
1036        let n = self.num_modes;
1037        let size = self.state.len();
1038
1039        for (operators, coeff) in &merged {
1040            let c_re = coeff.re;
1041            let c_im = coeff.im;
1042            // Skip negligible terms
1043            if c_re.abs() < 1e-14 && c_im.abs() < 1e-14 {
1044                continue;
1045            }
1046
1047            // Compute P|ψ⟩ on a clone (unit Pauli action only, no coefficient)
1048            let mut p_psi = self.state.clone();
1049            Self::apply_pauli_operators(operators, n, &mut p_psi);
1050
1051            // exp(−i·t·c·P) where c may be complex and P is a Hermitian Pauli string.
1052            // For real c: exp(−i·t·c·P) = cos(ct)·I − i·sin(ct)·P  (exactly unitary)
1053            // For complex c = a+ib:
1054            //   exp(−i·t·c·P) = cos(|c|t)·I − i·(c/|c|)·sin(|c|t)·P
1055            // phase = −i·c/|c|·sin(|c|t) = (c_im·sin_t/|c|) + i·(−c_re·sin_t/|c|)
1056            let magnitude = (c_re * c_re + c_im * c_im).sqrt();
1057            let theta = magnitude * time;
1058            let cos_t = theta.cos();
1059            let sin_t = theta.sin();
1060
1061            // For a Hermitian Hamiltonian (c purely real after merging), c_im ≈ 0 and
1062            // phase = (0, −c_re·sin_t/|c|) = (0, −sign(c_re)·sin_t)
1063            let phase = Complex64::new(c_im * sin_t / magnitude, -c_re * sin_t / magnitude);
1064
1065            for i in 0..size {
1066                self.state[i] = cos_t * self.state[i] + phase * p_psi[i];
1067            }
1068        }
1069
1070        Ok(())
1071    }
1072
1073    /// Get current state vector
1074    #[must_use]
1075    pub const fn get_state(&self) -> &Array1<Complex64> {
1076        &self.state
1077    }
1078
1079    /// Get number of particles in current state
1080    #[must_use]
1081    pub fn get_particle_number(&self) -> f64 {
1082        let mut total_number = 0.0;
1083
1084        for (index, amplitude) in self.state.iter().enumerate() {
1085            let prob = amplitude.norm_sqr();
1086            let popcount = f64::from(index.count_ones());
1087            total_number += prob * popcount;
1088        }
1089
1090        total_number
1091    }
1092
1093    /// Get simulation statistics
1094    #[must_use]
1095    pub const fn get_stats(&self) -> &FermionicStats {
1096        &self.stats
1097    }
1098
1099    /// Compute connected particle-number correlation ⟨n_i n_j⟩ − ⟨n_i⟩⟨n_j⟩.
1100    ///
1101    /// Both single-site expectation values and the joint ⟨n_i n_j⟩ are computed
1102    /// exactly from the current state vector via the complete Jordan-Wigner expansion.
1103    pub fn particle_correlation(&mut self, site1: usize, site2: usize) -> Result<f64> {
1104        // Individual number operator expectations (full JW expansion: I/2 - Z/2)
1105        let n1 = self
1106            .expectation_value(&FermionicOperator::Number(site1))?
1107            .re;
1108        let n2 = self
1109            .expectation_value(&FermionicOperator::Number(site2))?
1110            .re;
1111
1112        // ⟨n_i n_j⟩ via the product n_i * n_j expanded through transform_string
1113        // which properly handles the multi-term JW product (I/2-Z_i/2)(I/2-Z_j/2)
1114        let n1n2_string = FermionicString {
1115            operators: vec![
1116                FermionicOperator::Number(site1),
1117                FermionicOperator::Number(site2),
1118            ],
1119            coefficient: Complex64::new(1.0, 0.0),
1120            num_modes: self.num_modes,
1121        };
1122        let pauli_sum = self.jw_transform.transform_string(&n1n2_string)?;
1123
1124        let mut n1n2 = 0.0_f64;
1125        for term in &pauli_sum.terms {
1126            n1n2 += self.compute_pauli_expectation(term)?.re;
1127        }
1128
1129        Ok(n1n2 - n1 * n2)
1130    }
1131}
1132
1133/// Benchmark fermionic simulation
1134pub fn benchmark_fermionic_simulation(num_modes: usize) -> Result<FermionicStats> {
1135    let mut simulator = FermionicSimulator::new(num_modes)?;
1136
1137    // Create simple Hubbard model
1138    let hamiltonian = FermionicHamiltonian::hubbard_model(num_modes / 2, 1.0, 2.0, 0.5)?;
1139
1140    // Apply some fermionic operators
1141    let creation_op = FermionicOperator::Creation(0);
1142    simulator.apply_fermionic_operator(&creation_op)?;
1143
1144    let annihilation_op = FermionicOperator::Annihilation(1);
1145    simulator.apply_fermionic_operator(&annihilation_op)?;
1146
1147    // Evolve under Hamiltonian
1148    simulator.evolve_hamiltonian(&hamiltonian, 0.1)?;
1149
1150    Ok(simulator.get_stats().clone())
1151}
1152
1153#[cfg(test)]
1154mod tests {
1155    use super::*;
1156
1157    #[test]
1158    fn test_fermionic_operator_creation() {
1159        let op = FermionicOperator::Creation(0);
1160        assert!(op.is_creation());
1161        assert!(!op.is_annihilation());
1162        assert_eq!(op.site(), Some(0));
1163    }
1164
1165    #[test]
1166    fn test_fermionic_string() {
1167        let ops = vec![
1168            FermionicOperator::Creation(0),
1169            FermionicOperator::Annihilation(1),
1170        ];
1171        let string = FermionicString::new(ops, Complex64::new(1.0, 0.0), 4);
1172        assert_eq!(string.operators.len(), 2);
1173        assert_eq!(string.num_modes, 4);
1174    }
1175
1176    #[test]
1177    fn test_hubbard_hamiltonian() {
1178        let hamiltonian = FermionicHamiltonian::hubbard_model(2, 1.0, 2.0, 0.5)
1179            .expect("Failed to create Hubbard model Hamiltonian");
1180        assert_eq!(hamiltonian.num_modes, 4); // 2 sites × 2 spins
1181        assert!(!hamiltonian.terms.is_empty());
1182    }
1183
1184    #[test]
1185    fn test_jordan_wigner_transform() {
1186        let mut jw = JordanWignerTransform::new(4);
1187        let creation_op = FermionicOperator::Creation(1);
1188        let pauli_string = jw
1189            .transform_operator(&creation_op)
1190            .expect("Failed to transform creation operator via Jordan-Wigner");
1191
1192        assert_eq!(pauli_string.num_qubits, 4);
1193        assert_eq!(pauli_string.operators[0], PauliOperator::Z); // Jordan-Wigner string
1194        assert_eq!(pauli_string.operators[1], PauliOperator::X);
1195    }
1196
1197    #[test]
1198    fn test_fermionic_simulator() {
1199        let mut simulator =
1200            FermionicSimulator::new(4).expect("Failed to create fermionic simulator");
1201
1202        // Set initial state with one particle
1203        simulator
1204            .set_initial_state(&[true, false, false, false])
1205            .expect("Failed to set initial fermionic state");
1206
1207        let particle_number = simulator.get_particle_number();
1208        assert!((particle_number - 1.0).abs() < 1e-10);
1209    }
1210
1211    #[test]
1212    fn test_fermionic_string_multiplication() {
1213        let string1 = FermionicString::creation(0, Complex64::new(1.0, 0.0), 4);
1214        let string2 = FermionicString::annihilation(1, Complex64::new(1.0, 0.0), 4);
1215
1216        let product = string1
1217            .multiply(&string2)
1218            .expect("Failed to multiply fermionic strings");
1219        assert!(!product.operators.is_empty());
1220    }
1221}