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::{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 const 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 const 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: {pauli}"
246                        )))
247                    }
248                }
249            }
250            HamiltonianTerm::TwoPauli {
251                qubit1,
252                qubit2,
253                pauli1,
254                pauli2,
255                coefficient,
256            } => {
257                // exp(-i*coeff*t*P1⊗P2) implementation
258                let angle = -2.0 * coefficient * time;
259
260                // Basis transformation
261                self.apply_pauli_basis_change(&mut circuit, *qubit1, pauli1, false)?;
262                self.apply_pauli_basis_change(&mut circuit, *qubit2, pauli2, false)?;
263
264                // ZZ rotation
265                circuit.add_gate(Box::new(CNOT {
266                    control: QubitId::new(*qubit1 as u32),
267                    target: QubitId::new(*qubit2 as u32),
268                }))?;
269                circuit.add_gate(Box::new(RotationZ {
270                    target: QubitId::new(*qubit2 as u32),
271                    theta: angle,
272                }))?;
273                circuit.add_gate(Box::new(CNOT {
274                    control: QubitId::new(*qubit1 as u32),
275                    target: QubitId::new(*qubit2 as u32),
276                }))?;
277
278                // Inverse basis transformation
279                self.apply_pauli_basis_change(&mut circuit, *qubit1, pauli1, true)?;
280                self.apply_pauli_basis_change(&mut circuit, *qubit2, pauli2, true)?;
281            }
282            HamiltonianTerm::PauliString {
283                qubits,
284                paulis,
285                coefficient,
286            } => {
287                let angle = -2.0 * coefficient * time;
288
289                // Basis transformations
290                for (q, p) in qubits.iter().zip(paulis.iter()) {
291                    self.apply_pauli_basis_change(&mut circuit, *q, p, false)?;
292                }
293
294                // Multi-qubit Z rotation using CNOT ladder
295                for i in 0..qubits.len() - 1 {
296                    circuit.add_gate(Box::new(CNOT {
297                        control: QubitId::new(qubits[i] as u32),
298                        target: QubitId::new(qubits[i + 1] as u32),
299                    }))?;
300                }
301
302                circuit.add_gate(Box::new(RotationZ {
303                    target: QubitId::new(qubits[qubits.len() - 1] as u32),
304                    theta: angle,
305                }))?;
306
307                // Reverse CNOT ladder
308                for i in (0..qubits.len() - 1).rev() {
309                    circuit.add_gate(Box::new(CNOT {
310                        control: QubitId::new(qubits[i] as u32),
311                        target: QubitId::new(qubits[i + 1] as u32),
312                    }))?;
313                }
314
315                // Inverse basis transformations
316                for (q, p) in qubits.iter().zip(paulis.iter()) {
317                    self.apply_pauli_basis_change(&mut circuit, *q, p, true)?;
318                }
319            }
320            HamiltonianTerm::Custom { .. } => {
321                return Err(SimulatorError::InvalidOperation(
322                    "Custom terms not yet supported in Trotter decomposition".to_string(),
323                ));
324            }
325        }
326
327        Ok(circuit)
328    }
329
330    /// Apply Pauli basis change
331    fn apply_pauli_basis_change(
332        &self,
333        circuit: &mut DynamicCircuit,
334        qubit: usize,
335        pauli: &str,
336        inverse: bool,
337    ) -> Result<()> {
338        match pauli {
339            "X" => {
340                circuit.add_gate(Box::new(Hadamard {
341                    target: QubitId::new(qubit as u32),
342                }))?;
343            }
344            "Y" => {
345                if inverse {
346                    circuit.add_gate(Box::new(PhaseDagger {
347                        target: QubitId::new(qubit as u32),
348                    }))?;
349                    circuit.add_gate(Box::new(Hadamard {
350                        target: QubitId::new(qubit as u32),
351                    }))?;
352                } else {
353                    circuit.add_gate(Box::new(Hadamard {
354                        target: QubitId::new(qubit as u32),
355                    }))?;
356                    circuit.add_gate(Box::new(Phase {
357                        target: QubitId::new(qubit as u32),
358                    }))?;
359                }
360            }
361            "Z" => {
362                // No basis change needed for Z
363            }
364            _ => {
365                return Err(SimulatorError::InvalidInput(format!(
366                    "Unknown Pauli operator: {pauli}"
367                )))
368            }
369        }
370        Ok(())
371    }
372}
373
374/// Trotter-Suzuki decomposition methods
375#[derive(Debug, Clone, Copy)]
376pub enum TrotterMethod {
377    /// First-order Trotter formula: exp(-iHt) ≈ ∏_j exp(-iH_j*t)
378    FirstOrder,
379    /// Second-order Suzuki formula: S2(t) = ∏_j exp(-iH_j*t/2) * ∏_{j'} exp(-iH_{j'}*t/2)
380    SecondOrder,
381    /// Fourth-order Suzuki formula
382    FourthOrder,
383    /// Sixth-order Suzuki formula
384    SixthOrder,
385    /// Randomized Trotter formula
386    Randomized,
387}
388
389/// Trotter-Suzuki decomposer
390pub struct TrotterDecomposer {
391    /// Method to use
392    method: TrotterMethod,
393    /// Number of Trotter steps
394    num_steps: usize,
395}
396
397impl TrotterDecomposer {
398    /// Create a new decomposer
399    pub const fn new(method: TrotterMethod, num_steps: usize) -> Self {
400        Self { method, num_steps }
401    }
402
403    /// Decompose time evolution into a circuit
404    pub fn decompose(&self, hamiltonian: &Hamiltonian, total_time: f64) -> Result<DynamicCircuit> {
405        let dt = total_time / self.num_steps as f64;
406        let mut full_circuit = DynamicCircuit::new(hamiltonian.num_qubits);
407
408        for _ in 0..self.num_steps {
409            let step_circuit = match self.method {
410                TrotterMethod::FirstOrder => self.first_order_step(hamiltonian, dt)?,
411                TrotterMethod::SecondOrder => self.second_order_step(hamiltonian, dt)?,
412                TrotterMethod::FourthOrder => self.fourth_order_step(hamiltonian, dt)?,
413                TrotterMethod::SixthOrder => self.sixth_order_step(hamiltonian, dt)?,
414                TrotterMethod::Randomized => self.randomized_step(hamiltonian, dt)?,
415            };
416
417            // Append step circuit
418            for gate in step_circuit.gates() {
419                full_circuit.add_gate(gate.clone())?;
420            }
421        }
422
423        Ok(full_circuit)
424    }
425
426    /// First-order Trotter step
427    fn first_order_step(&self, hamiltonian: &Hamiltonian, dt: f64) -> Result<DynamicCircuit> {
428        let mut circuit = DynamicCircuit::new(hamiltonian.num_qubits);
429
430        for term in &hamiltonian.terms {
431            let term_circuit = hamiltonian.term_to_circuit(term, dt)?;
432            for gate in term_circuit.gates() {
433                circuit.add_gate(gate.clone())?;
434            }
435        }
436
437        Ok(circuit)
438    }
439
440    /// Second-order Suzuki step
441    fn second_order_step(&self, hamiltonian: &Hamiltonian, dt: f64) -> Result<DynamicCircuit> {
442        let mut circuit = DynamicCircuit::new(hamiltonian.num_qubits);
443
444        // Forward pass with dt/2
445        for term in &hamiltonian.terms {
446            let term_circuit = hamiltonian.term_to_circuit(term, dt / 2.0)?;
447            for gate in term_circuit.gates() {
448                circuit.add_gate(gate.clone())?;
449            }
450        }
451
452        // Backward pass with dt/2
453        for term in hamiltonian.terms.iter().rev() {
454            let term_circuit = hamiltonian.term_to_circuit(term, dt / 2.0)?;
455            for gate in term_circuit.gates() {
456                circuit.add_gate(gate.clone())?;
457            }
458        }
459
460        Ok(circuit)
461    }
462
463    /// Fourth-order Suzuki step
464    fn fourth_order_step(&self, hamiltonian: &Hamiltonian, dt: f64) -> Result<DynamicCircuit> {
465        let mut circuit = DynamicCircuit::new(hamiltonian.num_qubits);
466
467        // S4(t) = S2(s*t)*S2(s*t)*S2((1-4s)*t)*S2(s*t)*S2(s*t)
468        // where s = 1/(4 - 4^(1/3))
469        let s = 1.0 / (4.0 - 4.0_f64.cbrt());
470
471        // Apply S2(s*dt) twice
472        for _ in 0..2 {
473            let step = self.second_order_step(hamiltonian, s * dt)?;
474            for gate in step.gates() {
475                circuit.add_gate(gate.clone())?;
476            }
477        }
478
479        // Apply S2((1-4s)*dt)
480        let middle_step = self.second_order_step(hamiltonian, 4.0f64.mul_add(-s, 1.0) * dt)?;
481        for gate in middle_step.gates() {
482            circuit.add_gate(gate.clone())?;
483        }
484
485        // Apply S2(s*dt) twice more
486        for _ in 0..2 {
487            let step = self.second_order_step(hamiltonian, s * dt)?;
488            for gate in step.gates() {
489                circuit.add_gate(gate.clone())?;
490            }
491        }
492
493        Ok(circuit)
494    }
495
496    /// Sixth-order Suzuki step
497    fn sixth_order_step(&self, hamiltonian: &Hamiltonian, dt: f64) -> Result<DynamicCircuit> {
498        let mut circuit = DynamicCircuit::new(hamiltonian.num_qubits);
499
500        // Sixth-order coefficients
501        let w1: f64 = 1.0 / (2.0 - (1.0_f64 / 5.0).exp2());
502        let w0 = 2.0f64.mul_add(-w1, 1.0);
503
504        // Apply S4(w1*dt) twice
505        for _ in 0..2 {
506            let step = self.fourth_order_step(hamiltonian, w1 * dt)?;
507            for gate in step.gates() {
508                circuit.add_gate(gate.clone())?;
509            }
510        }
511
512        // Apply S4(w0*dt)
513        let middle_step = self.fourth_order_step(hamiltonian, w0 * dt)?;
514        for gate in middle_step.gates() {
515            circuit.add_gate(gate.clone())?;
516        }
517
518        // Apply S4(w1*dt) twice more
519        for _ in 0..2 {
520            let step = self.fourth_order_step(hamiltonian, w1 * dt)?;
521            for gate in step.gates() {
522                circuit.add_gate(gate.clone())?;
523            }
524        }
525
526        Ok(circuit)
527    }
528
529    /// Randomized Trotter step
530    fn randomized_step(&self, hamiltonian: &Hamiltonian, dt: f64) -> Result<DynamicCircuit> {
531        let mut circuit = DynamicCircuit::new(hamiltonian.num_qubits);
532
533        // Randomly permute the terms
534        let mut indices: Vec<usize> = (0..hamiltonian.terms.len()).collect();
535        fastrand::shuffle(&mut indices);
536
537        // Apply terms in random order
538        for &idx in &indices {
539            let term_circuit = hamiltonian.term_to_circuit(&hamiltonian.terms[idx], dt)?;
540            for gate in term_circuit.gates() {
541                circuit.add_gate(gate.clone())?;
542            }
543        }
544
545        Ok(circuit)
546    }
547
548    /// Estimate error bound for the decomposition
549    pub fn error_bound(&self, hamiltonian: &Hamiltonian, total_time: f64) -> f64 {
550        let dt = total_time / self.num_steps as f64;
551        let num_terms = hamiltonian.terms.len();
552
553        // Compute sum of coefficient magnitudes
554        let coeff_sum: f64 = hamiltonian
555            .terms
556            .iter()
557            .map(|term| match term {
558                HamiltonianTerm::SinglePauli { coefficient, .. }
559                | HamiltonianTerm::TwoPauli { coefficient, .. }
560                | HamiltonianTerm::PauliString { coefficient, .. }
561                | HamiltonianTerm::Custom { coefficient, .. } => coefficient.abs(),
562            })
563            .sum();
564
565        // Error bounds for different orders
566        match self.method {
567            TrotterMethod::FirstOrder => {
568                // O(dt^2) error
569                0.5 * coeff_sum.powi(2) * dt.powi(2) * self.num_steps as f64
570            }
571            TrotterMethod::SecondOrder => {
572                // O(dt^3) error
573                coeff_sum.powi(3) * dt.powi(3) * self.num_steps as f64 / 12.0
574            }
575            TrotterMethod::FourthOrder => {
576                // O(dt^5) error
577                coeff_sum.powi(5) * dt.powi(5) * self.num_steps as f64 / 360.0
578            }
579            TrotterMethod::SixthOrder => {
580                // O(dt^7) error
581                coeff_sum.powi(7) * dt.powi(7) * self.num_steps as f64 / 20160.0
582            }
583            TrotterMethod::Randomized => {
584                // Empirical bound for randomized
585                0.5 * coeff_sum * (total_time / (self.num_steps as f64).sqrt())
586            }
587        }
588    }
589}
590
591/// Create common Hamiltonians
592pub struct HamiltonianLibrary;
593
594impl HamiltonianLibrary {
595    /// Transverse field Ising model: H = -J∑\<ij\> Z_i Z_j - h∑_i X_i
596    pub fn transverse_ising_1d(
597        num_qubits: usize,
598        j: f64,
599        h: f64,
600        periodic: bool,
601    ) -> Result<Hamiltonian> {
602        let mut ham = Hamiltonian::new(num_qubits);
603
604        // ZZ interactions
605        for i in 0..num_qubits - 1 {
606            ham.add_two_pauli(i, i + 1, "Z", "Z", -j)?;
607        }
608
609        // Periodic boundary condition
610        if periodic && num_qubits > 2 {
611            ham.add_two_pauli(num_qubits - 1, 0, "Z", "Z", -j)?;
612        }
613
614        // Transverse field
615        for i in 0..num_qubits {
616            ham.add_single_pauli(i, "X", -h)?;
617        }
618
619        Ok(ham)
620    }
621
622    /// Heisenberg model: H = J∑\<ij\> (X_i X_j + Y_i Y_j + Δ Z_i Z_j)
623    pub fn heisenberg_1d(
624        num_qubits: usize,
625        j: f64,
626        delta: f64,
627        periodic: bool,
628    ) -> Result<Hamiltonian> {
629        let mut ham = Hamiltonian::new(num_qubits);
630
631        for i in 0..num_qubits - 1 {
632            ham.add_two_pauli(i, i + 1, "X", "X", j)?;
633            ham.add_two_pauli(i, i + 1, "Y", "Y", j)?;
634            ham.add_two_pauli(i, i + 1, "Z", "Z", j * delta)?;
635        }
636
637        if periodic && num_qubits > 2 {
638            ham.add_two_pauli(num_qubits - 1, 0, "X", "X", j)?;
639            ham.add_two_pauli(num_qubits - 1, 0, "Y", "Y", j)?;
640            ham.add_two_pauli(num_qubits - 1, 0, "Z", "Z", j * delta)?;
641        }
642
643        Ok(ham)
644    }
645
646    /// XY model: H = J∑\<ij\> (X_i X_j + Y_i Y_j)
647    pub fn xy_model(num_qubits: usize, j: f64, periodic: bool) -> Result<Hamiltonian> {
648        Self::heisenberg_1d(num_qubits, j, 0.0, periodic)
649    }
650}
651
652#[cfg(test)]
653mod tests {
654    use super::*;
655
656    #[test]
657    fn test_hamiltonian_construction() {
658        let mut ham = Hamiltonian::new(3);
659
660        ham.add_single_pauli(0, "X", 0.5).unwrap();
661        ham.add_two_pauli(0, 1, "Z", "Z", -1.0).unwrap();
662        ham.add_pauli_string(
663            vec![0, 1, 2],
664            vec!["X".to_string(), "Y".to_string(), "Z".to_string()],
665            0.25,
666        )
667        .unwrap();
668
669        assert_eq!(ham.terms.len(), 3);
670    }
671
672    #[test]
673    fn test_ising_model() {
674        let ham = HamiltonianLibrary::transverse_ising_1d(4, 1.0, 0.5, false).unwrap();
675
676        // 3 ZZ terms + 4 X terms
677        assert_eq!(ham.terms.len(), 7);
678    }
679
680    #[test]
681    fn test_trotter_decomposition() {
682        let ham = HamiltonianLibrary::transverse_ising_1d(3, 1.0, 0.5, false).unwrap();
683        let decomposer = TrotterDecomposer::new(TrotterMethod::SecondOrder, 10);
684
685        let circuit = decomposer.decompose(&ham, 1.0).unwrap();
686        assert!(circuit.gate_count() > 0);
687    }
688
689    #[test]
690    fn test_error_bounds() {
691        let ham = HamiltonianLibrary::xy_model(4, 1.0, true).unwrap();
692        let decomposer = TrotterDecomposer::new(TrotterMethod::FourthOrder, 100);
693
694        let error = decomposer.error_bound(&ham, 1.0);
695        assert!(error < 1e-6); // Fourth order with 100 steps should be very accurate
696    }
697
698    #[test]
699    fn test_heisenberg_model() {
700        let ham = HamiltonianLibrary::heisenberg_1d(3, 1.0, 1.0, false).unwrap();
701
702        // 2 * 3 interactions (XX, YY, ZZ)
703        assert_eq!(ham.terms.len(), 6);
704    }
705}