quantrs2_sim/
trotter.rs

1//! Trotter-Suzuki decomposition for time evolution of quantum systems.
2//!
3//! This module implements various Trotter-Suzuki formulas for approximating
4//! the time evolution operator exp(-iHt) where H = H1 + H2 + ... + Hn.
5
6use scirs2_core::ndarray::{Array1, Array2};
7use scirs2_core::Complex64;
8use std::f64::consts::PI;
9
10use crate::error::{Result, SimulatorError};
11use crate::sparse::{CSRMatrix, SparseMatrixBuilder};
12use quantrs2_core::gate::{
13    multi::CNOT,
14    single::{Hadamard, Phase, PhaseDagger, RotationX, RotationY, RotationZ},
15    GateOp,
16};
17use quantrs2_core::qubit::QubitId;
18
19/// Dynamic circuit that doesn't require compile-time qubit count
20#[derive(Debug)]
21pub struct DynamicCircuit {
22    /// Gates in the circuit
23    gates: Vec<Box<dyn GateOp>>,
24    /// Number of qubits
25    num_qubits: usize,
26}
27
28// Manual Clone implementation since Box<dyn GateOp> doesn't implement Clone
29impl Clone for DynamicCircuit {
30    fn clone(&self) -> Self {
31        // We can't clone Box<dyn GateOp> directly, so we create an empty circuit
32        // In practice, we'd need a method to deep clone gates
33        Self {
34            gates: Vec::new(),
35            num_qubits: self.num_qubits,
36        }
37    }
38}
39
40impl DynamicCircuit {
41    /// Create a new dynamic circuit
42    #[must_use]
43    pub fn new(num_qubits: usize) -> Self {
44        Self {
45            gates: Vec::new(),
46            num_qubits,
47        }
48    }
49
50    /// Add a gate to the circuit
51    pub fn add_gate(&mut self, gate: Box<dyn GateOp>) -> Result<()> {
52        // Validate qubits are in range
53        for qubit in gate.qubits() {
54            if qubit.id() as usize >= self.num_qubits {
55                return Err(SimulatorError::IndexOutOfBounds(qubit.id() as usize));
56            }
57        }
58        self.gates.push(gate);
59        Ok(())
60    }
61
62    /// Get the gates
63    #[must_use]
64    pub fn gates(&self) -> &[Box<dyn GateOp>] {
65        &self.gates
66    }
67
68    /// Get gate count
69    #[must_use]
70    pub fn gate_count(&self) -> usize {
71        self.gates.len()
72    }
73}
74
75/// Hamiltonian term type
76#[derive(Debug, Clone)]
77pub enum HamiltonianTerm {
78    /// Single-qubit Pauli operator
79    SinglePauli {
80        qubit: usize,
81        pauli: String,
82        coefficient: f64,
83    },
84    /// Two-qubit Pauli operator
85    TwoPauli {
86        qubit1: usize,
87        qubit2: usize,
88        pauli1: String,
89        pauli2: String,
90        coefficient: f64,
91    },
92    /// Multi-qubit Pauli string
93    PauliString {
94        qubits: Vec<usize>,
95        paulis: Vec<String>,
96        coefficient: f64,
97    },
98    /// Custom unitary operator
99    Custom {
100        qubits: Vec<usize>,
101        matrix: CSRMatrix,
102        coefficient: f64,
103    },
104}
105
106/// Hamiltonian as sum of terms
107#[derive(Debug, Clone)]
108pub struct Hamiltonian {
109    /// Individual terms in the Hamiltonian
110    pub terms: Vec<HamiltonianTerm>,
111    /// Total number of qubits
112    pub num_qubits: usize,
113}
114
115impl Hamiltonian {
116    /// Create a new Hamiltonian
117    #[must_use]
118    pub const fn new(num_qubits: usize) -> Self {
119        Self {
120            terms: Vec::new(),
121            num_qubits,
122        }
123    }
124
125    /// Add a single-qubit Pauli term
126    pub fn add_single_pauli(&mut self, qubit: usize, pauli: &str, coefficient: f64) -> Result<()> {
127        if qubit >= self.num_qubits {
128            return Err(SimulatorError::IndexOutOfBounds(qubit));
129        }
130
131        self.terms.push(HamiltonianTerm::SinglePauli {
132            qubit,
133            pauli: pauli.to_uppercase(),
134            coefficient,
135        });
136        Ok(())
137    }
138
139    /// Add a two-qubit Pauli term
140    pub fn add_two_pauli(
141        &mut self,
142        qubit1: usize,
143        qubit2: usize,
144        pauli1: &str,
145        pauli2: &str,
146        coefficient: f64,
147    ) -> Result<()> {
148        if qubit1 >= self.num_qubits || qubit2 >= self.num_qubits {
149            return Err(SimulatorError::InvalidInput(
150                "Qubit index out of bounds".to_string(),
151            ));
152        }
153
154        self.terms.push(HamiltonianTerm::TwoPauli {
155            qubit1,
156            qubit2,
157            pauli1: pauli1.to_uppercase(),
158            pauli2: pauli2.to_uppercase(),
159            coefficient,
160        });
161        Ok(())
162    }
163
164    /// Add a Pauli string term
165    pub fn add_pauli_string(
166        &mut self,
167        qubits: Vec<usize>,
168        paulis: Vec<String>,
169        coefficient: f64,
170    ) -> Result<()> {
171        if qubits.len() != paulis.len() {
172            return Err(SimulatorError::InvalidInput(
173                "Qubits and Paulis must have the same length".to_string(),
174            ));
175        }
176
177        for &q in &qubits {
178            if q >= self.num_qubits {
179                return Err(SimulatorError::IndexOutOfBounds(q));
180            }
181        }
182
183        self.terms.push(HamiltonianTerm::PauliString {
184            qubits,
185            paulis: paulis.iter().map(|p| p.to_uppercase()).collect(),
186            coefficient,
187        });
188        Ok(())
189    }
190
191    /// Get the number of qubits
192    #[must_use]
193    pub const fn get_num_qubits(&self) -> usize {
194        self.num_qubits
195    }
196
197    /// Add a term to the Hamiltonian
198    pub fn add_term(&mut self, term: HamiltonianTerm) {
199        self.terms.push(term);
200    }
201
202    /// Add a Pauli term with the signature expected by adiabatic module
203    pub fn add_pauli_term(&mut self, coefficient: f64, paulis: &[(usize, char)]) -> Result<()> {
204        if paulis.is_empty() {
205            return Ok(());
206        }
207
208        if paulis.len() == 1 {
209            let (qubit, pauli) = paulis[0];
210            self.add_single_pauli(qubit, &pauli.to_string(), coefficient)
211        } else if paulis.len() == 2 {
212            let (qubit1, pauli1) = paulis[0];
213            let (qubit2, pauli2) = paulis[1];
214            self.add_two_pauli(
215                qubit1,
216                qubit2,
217                &pauli1.to_string(),
218                &pauli2.to_string(),
219                coefficient,
220            )
221        } else {
222            let qubits: Vec<usize> = paulis.iter().map(|(q, _)| *q).collect();
223            let pauli_strings: Vec<String> = paulis.iter().map(|(_, p)| p.to_string()).collect();
224            self.add_pauli_string(qubits, pauli_strings, coefficient)
225        }
226    }
227
228    /// Convert a term to a circuit with time evolution
229    fn term_to_circuit(&self, term: &HamiltonianTerm, time: f64) -> Result<DynamicCircuit> {
230        let mut circuit = DynamicCircuit::new(self.num_qubits);
231
232        match term {
233            HamiltonianTerm::SinglePauli {
234                qubit,
235                pauli,
236                coefficient,
237            } => {
238                let angle = -2.0 * coefficient * time;
239                match pauli.as_str() {
240                    "X" => circuit.add_gate(Box::new(RotationX {
241                        target: QubitId::new(*qubit as u32),
242                        theta: angle,
243                    }))?,
244                    "Y" => circuit.add_gate(Box::new(RotationY {
245                        target: QubitId::new(*qubit as u32),
246                        theta: angle,
247                    }))?,
248                    "Z" => circuit.add_gate(Box::new(RotationZ {
249                        target: QubitId::new(*qubit as u32),
250                        theta: angle,
251                    }))?,
252                    _ => {
253                        return Err(SimulatorError::InvalidInput(format!(
254                            "Unknown Pauli operator: {pauli}"
255                        )))
256                    }
257                }
258            }
259            HamiltonianTerm::TwoPauli {
260                qubit1,
261                qubit2,
262                pauli1,
263                pauli2,
264                coefficient,
265            } => {
266                // exp(-i*coeff*t*P1⊗P2) implementation
267                let angle = -2.0 * coefficient * time;
268
269                // Basis transformation
270                self.apply_pauli_basis_change(&mut circuit, *qubit1, pauli1, false)?;
271                self.apply_pauli_basis_change(&mut circuit, *qubit2, pauli2, false)?;
272
273                // ZZ rotation
274                circuit.add_gate(Box::new(CNOT {
275                    control: QubitId::new(*qubit1 as u32),
276                    target: QubitId::new(*qubit2 as u32),
277                }))?;
278                circuit.add_gate(Box::new(RotationZ {
279                    target: QubitId::new(*qubit2 as u32),
280                    theta: angle,
281                }))?;
282                circuit.add_gate(Box::new(CNOT {
283                    control: QubitId::new(*qubit1 as u32),
284                    target: QubitId::new(*qubit2 as u32),
285                }))?;
286
287                // Inverse basis transformation
288                self.apply_pauli_basis_change(&mut circuit, *qubit1, pauli1, true)?;
289                self.apply_pauli_basis_change(&mut circuit, *qubit2, pauli2, true)?;
290            }
291            HamiltonianTerm::PauliString {
292                qubits,
293                paulis,
294                coefficient,
295            } => {
296                let angle = -2.0 * coefficient * time;
297
298                // Basis transformations
299                for (q, p) in qubits.iter().zip(paulis.iter()) {
300                    self.apply_pauli_basis_change(&mut circuit, *q, p, false)?;
301                }
302
303                // Multi-qubit Z rotation using CNOT ladder
304                for i in 0..qubits.len() - 1 {
305                    circuit.add_gate(Box::new(CNOT {
306                        control: QubitId::new(qubits[i] as u32),
307                        target: QubitId::new(qubits[i + 1] as u32),
308                    }))?;
309                }
310
311                circuit.add_gate(Box::new(RotationZ {
312                    target: QubitId::new(qubits[qubits.len() - 1] as u32),
313                    theta: angle,
314                }))?;
315
316                // Reverse CNOT ladder
317                for i in (0..qubits.len() - 1).rev() {
318                    circuit.add_gate(Box::new(CNOT {
319                        control: QubitId::new(qubits[i] as u32),
320                        target: QubitId::new(qubits[i + 1] as u32),
321                    }))?;
322                }
323
324                // Inverse basis transformations
325                for (q, p) in qubits.iter().zip(paulis.iter()) {
326                    self.apply_pauli_basis_change(&mut circuit, *q, p, true)?;
327                }
328            }
329            HamiltonianTerm::Custom { .. } => {
330                return Err(SimulatorError::InvalidOperation(
331                    "Custom terms not yet supported in Trotter decomposition".to_string(),
332                ));
333            }
334        }
335
336        Ok(circuit)
337    }
338
339    /// Apply Pauli basis change
340    fn apply_pauli_basis_change(
341        &self,
342        circuit: &mut DynamicCircuit,
343        qubit: usize,
344        pauli: &str,
345        inverse: bool,
346    ) -> Result<()> {
347        match pauli {
348            "X" => {
349                circuit.add_gate(Box::new(Hadamard {
350                    target: QubitId::new(qubit as u32),
351                }))?;
352            }
353            "Y" => {
354                if inverse {
355                    circuit.add_gate(Box::new(PhaseDagger {
356                        target: QubitId::new(qubit as u32),
357                    }))?;
358                    circuit.add_gate(Box::new(Hadamard {
359                        target: QubitId::new(qubit as u32),
360                    }))?;
361                } else {
362                    circuit.add_gate(Box::new(Hadamard {
363                        target: QubitId::new(qubit as u32),
364                    }))?;
365                    circuit.add_gate(Box::new(Phase {
366                        target: QubitId::new(qubit as u32),
367                    }))?;
368                }
369            }
370            "Z" => {
371                // No basis change needed for Z
372            }
373            _ => {
374                return Err(SimulatorError::InvalidInput(format!(
375                    "Unknown Pauli operator: {pauli}"
376                )))
377            }
378        }
379        Ok(())
380    }
381}
382
383/// Trotter-Suzuki decomposition methods
384#[derive(Debug, Clone, Copy)]
385pub enum TrotterMethod {
386    /// First-order Trotter formula: exp(-iHt) ≈ ∏_j exp(-iH_j*t)
387    FirstOrder,
388    /// Second-order Suzuki formula: S2(t) = ∏_j exp(-iH_j*t/2) * ∏_{j'} exp(-iH_{j'}*t/2)
389    SecondOrder,
390    /// Fourth-order Suzuki formula
391    FourthOrder,
392    /// Sixth-order Suzuki formula
393    SixthOrder,
394    /// Randomized Trotter formula
395    Randomized,
396}
397
398/// Trotter-Suzuki decomposer
399pub struct TrotterDecomposer {
400    /// Method to use
401    method: TrotterMethod,
402    /// Number of Trotter steps
403    num_steps: usize,
404}
405
406impl TrotterDecomposer {
407    /// Create a new decomposer
408    #[must_use]
409    pub const fn new(method: TrotterMethod, num_steps: usize) -> Self {
410        Self { method, num_steps }
411    }
412
413    /// Decompose time evolution into a circuit
414    pub fn decompose(&self, hamiltonian: &Hamiltonian, total_time: f64) -> Result<DynamicCircuit> {
415        let dt = total_time / self.num_steps as f64;
416        let mut full_circuit = DynamicCircuit::new(hamiltonian.num_qubits);
417
418        for _ in 0..self.num_steps {
419            let step_circuit = match self.method {
420                TrotterMethod::FirstOrder => self.first_order_step(hamiltonian, dt)?,
421                TrotterMethod::SecondOrder => self.second_order_step(hamiltonian, dt)?,
422                TrotterMethod::FourthOrder => self.fourth_order_step(hamiltonian, dt)?,
423                TrotterMethod::SixthOrder => self.sixth_order_step(hamiltonian, dt)?,
424                TrotterMethod::Randomized => self.randomized_step(hamiltonian, dt)?,
425            };
426
427            // Append step circuit
428            for gate in step_circuit.gates() {
429                full_circuit.add_gate(gate.clone())?;
430            }
431        }
432
433        Ok(full_circuit)
434    }
435
436    /// First-order Trotter step
437    fn first_order_step(&self, hamiltonian: &Hamiltonian, dt: f64) -> Result<DynamicCircuit> {
438        let mut circuit = DynamicCircuit::new(hamiltonian.num_qubits);
439
440        for term in &hamiltonian.terms {
441            let term_circuit = hamiltonian.term_to_circuit(term, dt)?;
442            for gate in term_circuit.gates() {
443                circuit.add_gate(gate.clone())?;
444            }
445        }
446
447        Ok(circuit)
448    }
449
450    /// Second-order Suzuki step
451    fn second_order_step(&self, hamiltonian: &Hamiltonian, dt: f64) -> Result<DynamicCircuit> {
452        let mut circuit = DynamicCircuit::new(hamiltonian.num_qubits);
453
454        // Forward pass with dt/2
455        for term in &hamiltonian.terms {
456            let term_circuit = hamiltonian.term_to_circuit(term, dt / 2.0)?;
457            for gate in term_circuit.gates() {
458                circuit.add_gate(gate.clone())?;
459            }
460        }
461
462        // Backward pass with dt/2
463        for term in hamiltonian.terms.iter().rev() {
464            let term_circuit = hamiltonian.term_to_circuit(term, dt / 2.0)?;
465            for gate in term_circuit.gates() {
466                circuit.add_gate(gate.clone())?;
467            }
468        }
469
470        Ok(circuit)
471    }
472
473    /// Fourth-order Suzuki step
474    fn fourth_order_step(&self, hamiltonian: &Hamiltonian, dt: f64) -> Result<DynamicCircuit> {
475        let mut circuit = DynamicCircuit::new(hamiltonian.num_qubits);
476
477        // S4(t) = S2(s*t)*S2(s*t)*S2((1-4s)*t)*S2(s*t)*S2(s*t)
478        // where s = 1/(4 - 4^(1/3))
479        let s = 1.0 / (4.0 - 4.0_f64.cbrt());
480
481        // Apply S2(s*dt) twice
482        for _ in 0..2 {
483            let step = self.second_order_step(hamiltonian, s * dt)?;
484            for gate in step.gates() {
485                circuit.add_gate(gate.clone())?;
486            }
487        }
488
489        // Apply S2((1-4s)*dt)
490        let middle_step = self.second_order_step(hamiltonian, 4.0f64.mul_add(-s, 1.0) * dt)?;
491        for gate in middle_step.gates() {
492            circuit.add_gate(gate.clone())?;
493        }
494
495        // Apply S2(s*dt) twice more
496        for _ in 0..2 {
497            let step = self.second_order_step(hamiltonian, s * dt)?;
498            for gate in step.gates() {
499                circuit.add_gate(gate.clone())?;
500            }
501        }
502
503        Ok(circuit)
504    }
505
506    /// Sixth-order Suzuki step
507    fn sixth_order_step(&self, hamiltonian: &Hamiltonian, dt: f64) -> Result<DynamicCircuit> {
508        let mut circuit = DynamicCircuit::new(hamiltonian.num_qubits);
509
510        // Sixth-order coefficients
511        let w1: f64 = 1.0 / (2.0 - (1.0_f64 / 5.0).exp2());
512        let w0 = 2.0f64.mul_add(-w1, 1.0);
513
514        // Apply S4(w1*dt) twice
515        for _ in 0..2 {
516            let step = self.fourth_order_step(hamiltonian, w1 * dt)?;
517            for gate in step.gates() {
518                circuit.add_gate(gate.clone())?;
519            }
520        }
521
522        // Apply S4(w0*dt)
523        let middle_step = self.fourth_order_step(hamiltonian, w0 * dt)?;
524        for gate in middle_step.gates() {
525            circuit.add_gate(gate.clone())?;
526        }
527
528        // Apply S4(w1*dt) twice more
529        for _ in 0..2 {
530            let step = self.fourth_order_step(hamiltonian, w1 * dt)?;
531            for gate in step.gates() {
532                circuit.add_gate(gate.clone())?;
533            }
534        }
535
536        Ok(circuit)
537    }
538
539    /// Randomized Trotter step
540    fn randomized_step(&self, hamiltonian: &Hamiltonian, dt: f64) -> Result<DynamicCircuit> {
541        let mut circuit = DynamicCircuit::new(hamiltonian.num_qubits);
542
543        // Randomly permute the terms
544        let mut indices: Vec<usize> = (0..hamiltonian.terms.len()).collect();
545        fastrand::shuffle(&mut indices);
546
547        // Apply terms in random order
548        for &idx in &indices {
549            let term_circuit = hamiltonian.term_to_circuit(&hamiltonian.terms[idx], dt)?;
550            for gate in term_circuit.gates() {
551                circuit.add_gate(gate.clone())?;
552            }
553        }
554
555        Ok(circuit)
556    }
557
558    /// Estimate error bound for the decomposition
559    #[must_use]
560    pub fn error_bound(&self, hamiltonian: &Hamiltonian, total_time: f64) -> f64 {
561        let dt = total_time / self.num_steps as f64;
562        let num_terms = hamiltonian.terms.len();
563
564        // Compute sum of coefficient magnitudes
565        let coeff_sum: f64 = hamiltonian
566            .terms
567            .iter()
568            .map(|term| match term {
569                HamiltonianTerm::SinglePauli { coefficient, .. }
570                | HamiltonianTerm::TwoPauli { coefficient, .. }
571                | HamiltonianTerm::PauliString { coefficient, .. }
572                | HamiltonianTerm::Custom { coefficient, .. } => coefficient.abs(),
573            })
574            .sum();
575
576        // Error bounds for different orders
577        match self.method {
578            TrotterMethod::FirstOrder => {
579                // O(dt^2) error
580                0.5 * coeff_sum.powi(2) * dt.powi(2) * self.num_steps as f64
581            }
582            TrotterMethod::SecondOrder => {
583                // O(dt^3) error
584                coeff_sum.powi(3) * dt.powi(3) * self.num_steps as f64 / 12.0
585            }
586            TrotterMethod::FourthOrder => {
587                // O(dt^5) error
588                coeff_sum.powi(5) * dt.powi(5) * self.num_steps as f64 / 360.0
589            }
590            TrotterMethod::SixthOrder => {
591                // O(dt^7) error
592                coeff_sum.powi(7) * dt.powi(7) * self.num_steps as f64 / 20_160.0
593            }
594            TrotterMethod::Randomized => {
595                // Empirical bound for randomized
596                0.5 * coeff_sum * (total_time / (self.num_steps as f64).sqrt())
597            }
598        }
599    }
600}
601
602/// Create common Hamiltonians
603pub struct HamiltonianLibrary;
604
605impl HamiltonianLibrary {
606    /// Transverse field Ising model: H = -J∑\<ij\> `Z_i` `Z_j` - h∑_i `X_i`
607    pub fn transverse_ising_1d(
608        num_qubits: usize,
609        j: f64,
610        h: f64,
611        periodic: bool,
612    ) -> Result<Hamiltonian> {
613        let mut ham = Hamiltonian::new(num_qubits);
614
615        // ZZ interactions
616        for i in 0..num_qubits - 1 {
617            ham.add_two_pauli(i, i + 1, "Z", "Z", -j)?;
618        }
619
620        // Periodic boundary condition
621        if periodic && num_qubits > 2 {
622            ham.add_two_pauli(num_qubits - 1, 0, "Z", "Z", -j)?;
623        }
624
625        // Transverse field
626        for i in 0..num_qubits {
627            ham.add_single_pauli(i, "X", -h)?;
628        }
629
630        Ok(ham)
631    }
632
633    /// Heisenberg model: H = J∑\<ij\> (`X_i` `X_j` + `Y_i` `Y_j` + Δ `Z_i` `Z_j`)
634    pub fn heisenberg_1d(
635        num_qubits: usize,
636        j: f64,
637        delta: f64,
638        periodic: bool,
639    ) -> Result<Hamiltonian> {
640        let mut ham = Hamiltonian::new(num_qubits);
641
642        for i in 0..num_qubits - 1 {
643            ham.add_two_pauli(i, i + 1, "X", "X", j)?;
644            ham.add_two_pauli(i, i + 1, "Y", "Y", j)?;
645            ham.add_two_pauli(i, i + 1, "Z", "Z", j * delta)?;
646        }
647
648        if periodic && num_qubits > 2 {
649            ham.add_two_pauli(num_qubits - 1, 0, "X", "X", j)?;
650            ham.add_two_pauli(num_qubits - 1, 0, "Y", "Y", j)?;
651            ham.add_two_pauli(num_qubits - 1, 0, "Z", "Z", j * delta)?;
652        }
653
654        Ok(ham)
655    }
656
657    /// XY model: H = J∑\<ij\> (`X_i` `X_j` + `Y_i` `Y_j`)
658    pub fn xy_model(num_qubits: usize, j: f64, periodic: bool) -> Result<Hamiltonian> {
659        Self::heisenberg_1d(num_qubits, j, 0.0, periodic)
660    }
661}
662
663#[cfg(test)]
664mod tests {
665    use super::*;
666
667    #[test]
668    fn test_hamiltonian_construction() {
669        let mut ham = Hamiltonian::new(3);
670
671        ham.add_single_pauli(0, "X", 0.5)
672            .expect("add_single_pauli should succeed");
673        ham.add_two_pauli(0, 1, "Z", "Z", -1.0)
674            .expect("add_two_pauli should succeed");
675        ham.add_pauli_string(
676            vec![0, 1, 2],
677            vec!["X".to_string(), "Y".to_string(), "Z".to_string()],
678            0.25,
679        )
680        .expect("add_pauli_string should succeed");
681
682        assert_eq!(ham.terms.len(), 3);
683    }
684
685    #[test]
686    fn test_ising_model() {
687        let ham = HamiltonianLibrary::transverse_ising_1d(4, 1.0, 0.5, false)
688            .expect("transverse_ising_1d should succeed");
689
690        // 3 ZZ terms + 4 X terms
691        assert_eq!(ham.terms.len(), 7);
692    }
693
694    #[test]
695    fn test_trotter_decomposition() {
696        let ham = HamiltonianLibrary::transverse_ising_1d(3, 1.0, 0.5, false)
697            .expect("transverse_ising_1d should succeed");
698        let decomposer = TrotterDecomposer::new(TrotterMethod::SecondOrder, 10);
699
700        let circuit = decomposer
701            .decompose(&ham, 1.0)
702            .expect("decompose should succeed");
703        assert!(circuit.gate_count() > 0);
704    }
705
706    #[test]
707    fn test_error_bounds() {
708        let ham = HamiltonianLibrary::xy_model(4, 1.0, true).expect("xy_model should succeed");
709        let decomposer = TrotterDecomposer::new(TrotterMethod::FourthOrder, 100);
710
711        let error = decomposer.error_bound(&ham, 1.0);
712        assert!(error < 1e-6); // Fourth order with 100 steps should be very accurate
713    }
714
715    #[test]
716    fn test_heisenberg_model() {
717        let ham = HamiltonianLibrary::heisenberg_1d(3, 1.0, 1.0, false)
718            .expect("heisenberg_1d should succeed");
719
720        // 2 * 3 interactions (XX, YY, ZZ)
721        assert_eq!(ham.terms.len(), 6);
722    }
723}