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