quantrs2_sim/
qmc.rs

1//! Quantum Monte Carlo (QMC) simulation methods.
2//!
3//! This module implements various QMC algorithms for simulating quantum systems,
4//! including Variational Monte Carlo (VMC) and Diffusion Monte Carlo (DMC).
5
6use crate::prelude::SimulatorError;
7use fastrand;
8use ndarray::{Array1, Array2, Array3};
9use num_complex::Complex64;
10
11use crate::error::Result;
12use crate::trotter::{Hamiltonian, HamiltonianTerm};
13
14/// Walker in QMC simulation
15#[derive(Debug, Clone)]
16pub struct Walker {
17    /// Configuration (bit string representation)
18    pub config: Vec<bool>,
19    /// Weight/amplitude
20    pub weight: Complex64,
21    /// Local energy
22    pub local_energy: Complex64,
23}
24
25impl Walker {
26    /// Create a new walker
27    pub fn new(num_qubits: usize) -> Self {
28        let mut config = vec![false; num_qubits];
29        // Random initial configuration
30        for bit in &mut config {
31            *bit = fastrand::bool();
32        }
33
34        Self {
35            config,
36            weight: Complex64::new(1.0, 0.0),
37            local_energy: Complex64::new(0.0, 0.0),
38        }
39    }
40
41    /// Flip a qubit in the configuration
42    pub fn flip(&mut self, qubit: usize) {
43        if qubit < self.config.len() {
44            self.config[qubit] = !self.config[qubit];
45        }
46    }
47
48    /// Get configuration as integer
49    pub fn as_integer(&self) -> usize {
50        let mut result = 0;
51        for (i, &bit) in self.config.iter().enumerate() {
52            if bit {
53                result |= 1 << i;
54            }
55        }
56        result
57    }
58}
59
60/// Wave function ansatz for VMC
61#[derive(Debug, Clone)]
62pub enum WaveFunction {
63    /// Product state
64    Product(Vec<Complex64>),
65    /// Jastrow factor
66    Jastrow { alpha: f64, beta: f64 },
67    /// Neural network quantum state (simplified)
68    NeuralNetwork {
69        weights: Array2<f64>,
70        biases: Array1<f64>,
71    },
72    /// Matrix product state
73    MPS {
74        tensors: Vec<Array3<Complex64>>,
75        bond_dim: usize,
76    },
77}
78
79impl WaveFunction {
80    /// Evaluate wave function amplitude for a configuration
81    pub fn amplitude(&self, config: &[bool]) -> Complex64 {
82        match self {
83            WaveFunction::Product(amps) => {
84                let mut result = Complex64::new(1.0, 0.0);
85                for (i, &bit) in config.iter().enumerate() {
86                    if i < amps.len() {
87                        result *= if bit {
88                            amps[i]
89                        } else {
90                            Complex64::new(1.0, 0.0) - amps[i]
91                        };
92                    }
93                }
94                result
95            }
96            WaveFunction::Jastrow { alpha, beta } => {
97                // Jastrow factor: exp(sum_ij J_ij n_i n_j)
98                let mut exponent = 0.0;
99                for (i, &n_i) in config.iter().enumerate() {
100                    if n_i {
101                        exponent += alpha;
102                        for (j, &n_j) in config.iter().enumerate() {
103                            if i != j && n_j {
104                                exponent += beta / (1.0 + (i as f64 - j as f64).abs());
105                            }
106                        }
107                    }
108                }
109                Complex64::new(exponent.exp(), 0.0)
110            }
111            WaveFunction::NeuralNetwork { weights, biases } => {
112                // Simplified RBM-like network
113                let input: Vec<f64> = config.iter().map(|&b| if b { 1.0 } else { 0.0 }).collect();
114                let hidden_dim = weights.nrows();
115
116                let mut hidden = vec![0.0; hidden_dim];
117                for h in 0..hidden_dim {
118                    let mut sum = biases[h];
119                    for (v, &x) in input.iter().enumerate() {
120                        if v < weights.ncols() {
121                            sum += weights[[h, v]] * x;
122                        }
123                    }
124                    hidden[h] = 1.0 / (1.0 + (-sum).exp()); // Sigmoid
125                }
126
127                let mut log_psi = 0.0;
128                for &h in &hidden {
129                    log_psi += (1.0 + h).ln();
130                }
131                Complex64::new(log_psi.exp(), 0.0)
132            }
133            WaveFunction::MPS { .. } => {
134                // Simplified MPS evaluation
135                Complex64::new(1.0, 0.0)
136            }
137        }
138    }
139
140    /// Compute log derivative for parameter optimization
141    pub fn log_derivative(&self, config: &[bool], param_idx: usize) -> f64 {
142        match self {
143            WaveFunction::Jastrow { alpha, beta } => {
144                // Gradient w.r.t. Jastrow parameters
145                if param_idx == 0 {
146                    // d/d(alpha)
147                    config.iter().filter(|&&b| b).count() as f64
148                } else {
149                    // d/d(beta)
150                    let mut sum = 0.0;
151                    for (i, &n_i) in config.iter().enumerate() {
152                        if n_i {
153                            for (j, &n_j) in config.iter().enumerate() {
154                                if i != j && n_j {
155                                    sum += 1.0 / (1.0 + (i as f64 - j as f64).abs());
156                                }
157                            }
158                        }
159                    }
160                    sum
161                }
162            }
163            _ => 0.0, // Simplified for other types
164        }
165    }
166}
167
168/// Variational Monte Carlo simulator
169pub struct VMC {
170    /// Wave function ansatz
171    wave_function: WaveFunction,
172    /// Number of qubits
173    num_qubits: usize,
174    /// Hamiltonian
175    hamiltonian: Hamiltonian,
176}
177
178impl VMC {
179    /// Create a new VMC simulator
180    pub fn new(num_qubits: usize, wave_function: WaveFunction, hamiltonian: Hamiltonian) -> Self {
181        Self {
182            wave_function,
183            num_qubits,
184            hamiltonian,
185        }
186    }
187
188    /// Run VMC simulation
189    pub fn run(
190        &mut self,
191        num_samples: usize,
192        num_thermalization: usize,
193        optimization_steps: usize,
194        learning_rate: f64,
195    ) -> Result<VMCResult> {
196        let mut energies = Vec::new();
197        let mut variances = Vec::new();
198
199        for step in 0..optimization_steps {
200            // Thermalization
201            let mut walker = Walker::new(self.num_qubits);
202            for _ in 0..num_thermalization {
203                self.metropolis_step(&mut walker)?;
204            }
205
206            // Sampling
207            let mut local_energies = Vec::new();
208            let mut gradients = [0.0; 2]; // For Jastrow parameters
209
210            for _ in 0..num_samples {
211                self.metropolis_step(&mut walker)?;
212
213                // Compute local energy
214                let e_loc = self.local_energy(&walker.config)?;
215                local_energies.push(e_loc);
216
217                // Compute gradients for optimization
218                if let WaveFunction::Jastrow { .. } = &self.wave_function {
219                    for p in 0..2 {
220                        let deriv = self.wave_function.log_derivative(&walker.config, p);
221                        gradients[p] += (e_loc.re
222                            - local_energies.iter().map(|e| e.re).sum::<f64>()
223                                / local_energies.len() as f64)
224                            * deriv;
225                    }
226                }
227            }
228
229            // Statistics
230            let mean_energy = local_energies.iter().map(|e| e.re).sum::<f64>() / num_samples as f64;
231            let variance = local_energies
232                .iter()
233                .map(|e| (e.re - mean_energy).powi(2))
234                .sum::<f64>()
235                / num_samples as f64;
236
237            energies.push(mean_energy);
238            variances.push(variance);
239
240            // Parameter update (gradient descent)
241            if let WaveFunction::Jastrow {
242                ref mut alpha,
243                ref mut beta,
244            } = &mut self.wave_function
245            {
246                *alpha -= learning_rate * gradients[0] / num_samples as f64;
247                *beta -= learning_rate * gradients[1] / num_samples as f64;
248            }
249
250            // Print progress
251            if step % 10 == 0 {
252                println!(
253                    "VMC Step {}: E = {:.6} ± {:.6}",
254                    step,
255                    mean_energy,
256                    variance.sqrt()
257                );
258            }
259        }
260
261        Ok(VMCResult {
262            final_energy: *energies.last().unwrap(),
263            energy_history: energies,
264            variance_history: variances,
265        })
266    }
267
268    /// Metropolis step
269    fn metropolis_step(&self, walker: &mut Walker) -> Result<()> {
270        // Propose move: flip random qubit
271        let qubit = fastrand::usize(..self.num_qubits);
272        let old_config = walker.config.clone();
273        walker.flip(qubit);
274
275        // Compute acceptance ratio
276        let old_amp = self.wave_function.amplitude(&old_config);
277        let new_amp = self.wave_function.amplitude(&walker.config);
278        let ratio = (new_amp.norm() / old_amp.norm()).powi(2);
279
280        // Accept or reject
281        if fastrand::f64() >= ratio {
282            walker.config = old_config; // Reject
283        }
284
285        Ok(())
286    }
287
288    /// Compute local energy
289    fn local_energy(&self, config: &[bool]) -> Result<Complex64> {
290        let psi = self.wave_function.amplitude(config);
291        if psi.norm() < 1e-15 {
292            return Ok(Complex64::new(0.0, 0.0));
293        }
294
295        let mut h_psi = Complex64::new(0.0, 0.0);
296
297        // Apply Hamiltonian terms
298        for term in &self.hamiltonian.terms {
299            match term {
300                HamiltonianTerm::SinglePauli {
301                    qubit,
302                    pauli,
303                    coefficient,
304                } => {
305                    match pauli.as_str() {
306                        "Z" => {
307                            // Diagonal term
308                            let sign = if config[*qubit] { 1.0 } else { -1.0 };
309                            h_psi += coefficient * sign * psi;
310                        }
311                        "X" => {
312                            // Off-diagonal: flip qubit
313                            let mut flipped = config.to_vec();
314                            flipped[*qubit] = !flipped[*qubit];
315                            let psi_flipped = self.wave_function.amplitude(&flipped);
316                            h_psi += coefficient * psi_flipped;
317                        }
318                        "Y" => {
319                            // Off-diagonal with phase
320                            let mut flipped = config.to_vec();
321                            flipped[*qubit] = !flipped[*qubit];
322                            let psi_flipped = self.wave_function.amplitude(&flipped);
323                            let phase = if config[*qubit] {
324                                Complex64::new(0.0, -1.0)
325                            } else {
326                                Complex64::new(0.0, 1.0)
327                            };
328                            h_psi += coefficient * phase * psi_flipped;
329                        }
330                        _ => {}
331                    }
332                }
333                HamiltonianTerm::TwoPauli {
334                    qubit1,
335                    qubit2,
336                    pauli1,
337                    pauli2,
338                    coefficient,
339                } => {
340                    // Two-qubit terms (simplified)
341                    if pauli1 == "Z" && pauli2 == "Z" {
342                        let sign1 = if config[*qubit1] { 1.0 } else { -1.0 };
343                        let sign2 = if config[*qubit2] { 1.0 } else { -1.0 };
344                        h_psi += coefficient * sign1 * sign2 * psi;
345                    }
346                }
347                _ => {} // Other terms not implemented in this simplified version
348            }
349        }
350
351        Ok(h_psi / psi)
352    }
353}
354
355/// VMC simulation result
356#[derive(Debug)]
357pub struct VMCResult {
358    /// Final energy
359    pub final_energy: f64,
360    /// Energy history
361    pub energy_history: Vec<f64>,
362    /// Variance history
363    pub variance_history: Vec<f64>,
364}
365
366/// Diffusion Monte Carlo simulator
367pub struct DMC {
368    /// Reference energy
369    reference_energy: f64,
370    /// Time step
371    tau: f64,
372    /// Target walker number
373    target_walkers: usize,
374    /// Hamiltonian
375    hamiltonian: Hamiltonian,
376    /// Number of qubits
377    num_qubits: usize,
378}
379
380impl DMC {
381    /// Create a new DMC simulator
382    pub fn new(
383        num_qubits: usize,
384        hamiltonian: Hamiltonian,
385        tau: f64,
386        target_walkers: usize,
387    ) -> Self {
388        Self {
389            reference_energy: 0.0,
390            tau,
391            target_walkers,
392            hamiltonian,
393            num_qubits,
394        }
395    }
396
397    /// Run DMC simulation
398    pub fn run(&mut self, num_blocks: usize, steps_per_block: usize) -> Result<DMCResult> {
399        // Initialize walkers
400        let mut walkers: Vec<Walker> = (0..self.target_walkers)
401            .map(|_| Walker::new(self.num_qubits))
402            .collect();
403
404        let mut energies = Vec::new();
405        let mut walker_counts = Vec::new();
406
407        for block in 0..num_blocks {
408            let mut block_energy = 0.0;
409            let mut total_weight = 0.0;
410
411            for _ in 0..steps_per_block {
412                // Propagate walkers
413                let mut new_walkers = Vec::new();
414
415                for walker in &walkers {
416                    // Diffusion step
417                    let mut new_walker = walker.clone();
418                    self.diffusion_step(&mut new_walker)?;
419
420                    // Branching
421                    let local_e = self.diagonal_energy(&new_walker.config)?;
422                    let growth_factor = (-self.tau * (local_e - self.reference_energy)).exp();
423                    let num_copies = self.branch(growth_factor);
424
425                    for _ in 0..num_copies {
426                        new_walkers.push(new_walker.clone());
427                    }
428
429                    block_energy += local_e * walker.weight.norm();
430                    total_weight += walker.weight.norm();
431                }
432
433                // Ensure at least one walker survives
434                if new_walkers.is_empty() {
435                    // Keep at least one walker from the previous generation
436                    new_walkers.push(walkers[0].clone());
437                }
438
439                walkers = new_walkers;
440
441                // Population control
442                self.population_control(&mut walkers)?;
443            }
444
445            // Record statistics
446            let avg_energy = block_energy / total_weight;
447            energies.push(avg_energy);
448            walker_counts.push(walkers.len());
449
450            // Update reference energy
451            self.reference_energy =
452                avg_energy - (walkers.len() as f64 - self.target_walkers as f64).ln() / self.tau;
453
454            if block % 10 == 0 {
455                println!(
456                    "DMC Block {}: E = {:.6}, Walkers = {}",
457                    block,
458                    avg_energy,
459                    walkers.len()
460                );
461            }
462        }
463
464        Ok(DMCResult {
465            ground_state_energy: *energies.last().unwrap(),
466            energy_history: energies,
467            walker_history: walker_counts,
468        })
469    }
470
471    /// Diffusion step (random walk)
472    fn diffusion_step(&self, walker: &mut Walker) -> Result<()> {
473        // Simple diffusion: flip random qubits
474        let num_flips = fastrand::usize(1..=3.min(self.num_qubits));
475        for _ in 0..num_flips {
476            let qubit = fastrand::usize(..self.num_qubits);
477            walker.flip(qubit);
478        }
479        Ok(())
480    }
481
482    /// Compute diagonal energy
483    fn diagonal_energy(&self, config: &[bool]) -> Result<f64> {
484        let mut energy = 0.0;
485
486        for term in &self.hamiltonian.terms {
487            match term {
488                HamiltonianTerm::SinglePauli {
489                    qubit,
490                    pauli,
491                    coefficient,
492                } => {
493                    if pauli == "Z" {
494                        let sign = if config[*qubit] { 1.0 } else { -1.0 };
495                        energy += coefficient * sign;
496                    }
497                }
498                HamiltonianTerm::TwoPauli {
499                    qubit1,
500                    qubit2,
501                    pauli1,
502                    pauli2,
503                    coefficient,
504                } => {
505                    if pauli1 == "Z" && pauli2 == "Z" {
506                        let sign1 = if config[*qubit1] { 1.0 } else { -1.0 };
507                        let sign2 = if config[*qubit2] { 1.0 } else { -1.0 };
508                        energy += coefficient * sign1 * sign2;
509                    }
510                }
511                _ => {}
512            }
513        }
514
515        Ok(energy)
516    }
517
518    /// Branching process
519    fn branch(&self, growth_factor: f64) -> usize {
520        // Ensure growth factor is reasonable to prevent walker extinction
521        let clamped_factor = growth_factor.clamp(0.1, 3.0);
522        let expected = clamped_factor;
523        let integer_part = expected.floor() as usize;
524        let fractional_part = expected - integer_part as f64;
525
526        if fastrand::f64() < fractional_part {
527            integer_part + 1
528        } else {
529            integer_part
530        }
531    }
532
533    /// Population control
534    fn population_control(&self, walkers: &mut Vec<Walker>) -> Result<()> {
535        let current_size = walkers.len();
536
537        if current_size == 0 {
538            return Err(SimulatorError::ComputationError(
539                "All walkers died".to_string(),
540            ));
541        }
542
543        // Simple comb method
544        if current_size > 2 * self.target_walkers {
545            // Remove every other walker
546            let mut new_walkers = Vec::new();
547            for (i, walker) in walkers.iter().enumerate() {
548                if i % 2 == 0 {
549                    let mut w = walker.clone();
550                    w.weight *= Complex64::new(2.0, 0.0);
551                    new_walkers.push(w);
552                }
553            }
554            *walkers = new_walkers;
555        } else if current_size < self.target_walkers / 2 {
556            // Duplicate walkers
557            let mut new_walkers = walkers.clone();
558            for walker in walkers.iter() {
559                let mut w = walker.clone();
560                w.weight *= Complex64::new(0.5, 0.0);
561                new_walkers.push(w);
562            }
563            *walkers = new_walkers;
564        }
565
566        Ok(())
567    }
568}
569
570/// DMC simulation result
571#[derive(Debug)]
572pub struct DMCResult {
573    /// Ground state energy
574    pub ground_state_energy: f64,
575    /// Energy history
576    pub energy_history: Vec<f64>,
577    /// Walker count history
578    pub walker_history: Vec<usize>,
579}
580
581/// Path Integral Monte Carlo
582pub struct PIMC {
583    /// Number of imaginary time slices
584    num_slices: usize,
585    /// Inverse temperature
586    beta: f64,
587    /// Number of qubits
588    num_qubits: usize,
589    /// Hamiltonian
590    hamiltonian: Hamiltonian,
591}
592
593impl PIMC {
594    /// Create a new PIMC simulator
595    pub fn new(num_qubits: usize, hamiltonian: Hamiltonian, beta: f64, num_slices: usize) -> Self {
596        Self {
597            num_slices,
598            beta,
599            num_qubits,
600            hamiltonian,
601        }
602    }
603
604    /// Run PIMC simulation
605    pub fn run(&self, num_samples: usize, num_thermalization: usize) -> Result<PIMCResult> {
606        // Initialize path (world line configuration)
607        let mut path: Vec<Vec<bool>> = (0..self.num_slices)
608            .map(|_| (0..self.num_qubits).map(|_| fastrand::bool()).collect())
609            .collect();
610
611        let tau = self.beta / self.num_slices as f64;
612        let mut energies = Vec::new();
613        let mut magnetizations = Vec::new();
614
615        // Thermalization
616        for _ in 0..num_thermalization {
617            self.update_path(&mut path, tau)?;
618        }
619
620        // Sampling
621        for _ in 0..num_samples {
622            self.update_path(&mut path, tau)?;
623
624            // Measure observables
625            let energy = self.measure_energy(&path)?;
626            let magnetization = self.measure_magnetization(&path);
627
628            energies.push(energy);
629            magnetizations.push(magnetization);
630        }
631
632        Ok(PIMCResult {
633            average_energy: energies.iter().sum::<f64>() / energies.len() as f64,
634            average_magnetization: magnetizations.iter().sum::<f64>() / magnetizations.len() as f64,
635            energy_samples: energies,
636            magnetization_samples: magnetizations,
637        })
638    }
639
640    /// Update path configuration
641    fn update_path(&self, path: &mut Vec<Vec<bool>>, tau: f64) -> Result<()> {
642        // World line updates
643        for _ in 0..self.num_qubits * self.num_slices {
644            let slice = fastrand::usize(..self.num_slices);
645            let qubit = fastrand::usize(..self.num_qubits);
646
647            // Compute action change
648            let action_old = self.path_action(path, tau)?;
649            path[slice][qubit] = !path[slice][qubit];
650            let action_new = self.path_action(path, tau)?;
651
652            // Metropolis acceptance
653            if fastrand::f64() >= (-(action_new - action_old)).exp() {
654                path[slice][qubit] = !path[slice][qubit]; // Reject
655            }
656        }
657
658        Ok(())
659    }
660
661    /// Compute path action
662    fn path_action(&self, path: &[Vec<bool>], tau: f64) -> Result<f64> {
663        let mut action = 0.0;
664
665        // Kinetic term (periodic boundary conditions)
666        for s in 0..self.num_slices {
667            let next_s = (s + 1) % self.num_slices;
668            for q in 0..self.num_qubits {
669                if path[s][q] != path[next_s][q] {
670                    action += -0.5 * tau.ln();
671                }
672            }
673        }
674
675        // Potential term
676        for s in 0..self.num_slices {
677            action += tau * self.diagonal_energy(&path[s])?;
678        }
679
680        Ok(action)
681    }
682
683    /// Measure energy
684    fn measure_energy(&self, path: &[Vec<bool>]) -> Result<f64> {
685        let mut total = 0.0;
686        for config in path {
687            total += self.diagonal_energy(config)?;
688        }
689        Ok(total / self.num_slices as f64)
690    }
691
692    /// Measure magnetization
693    fn measure_magnetization(&self, path: &[Vec<bool>]) -> f64 {
694        let mut total = 0.0;
695        for config in path {
696            let mag: f64 = config.iter().map(|&b| if b { 1.0 } else { -1.0 }).sum();
697            total += mag;
698        }
699        total / (self.num_slices * self.num_qubits) as f64
700    }
701
702    /// Compute diagonal energy (same as DMC)
703    fn diagonal_energy(&self, config: &[bool]) -> Result<f64> {
704        let mut energy = 0.0;
705
706        for term in &self.hamiltonian.terms {
707            match term {
708                HamiltonianTerm::SinglePauli {
709                    qubit,
710                    pauli,
711                    coefficient,
712                } => {
713                    if pauli == "Z" {
714                        let sign = if config[*qubit] { 1.0 } else { -1.0 };
715                        energy += coefficient * sign;
716                    }
717                }
718                HamiltonianTerm::TwoPauli {
719                    qubit1,
720                    qubit2,
721                    pauli1,
722                    pauli2,
723                    coefficient,
724                } => {
725                    if pauli1 == "Z" && pauli2 == "Z" {
726                        let sign1 = if config[*qubit1] { 1.0 } else { -1.0 };
727                        let sign2 = if config[*qubit2] { 1.0 } else { -1.0 };
728                        energy += coefficient * sign1 * sign2;
729                    }
730                }
731                _ => {}
732            }
733        }
734
735        Ok(energy)
736    }
737}
738
739/// PIMC simulation result
740#[derive(Debug)]
741pub struct PIMCResult {
742    /// Average energy
743    pub average_energy: f64,
744    /// Average magnetization
745    pub average_magnetization: f64,
746    /// Energy samples
747    pub energy_samples: Vec<f64>,
748    /// Magnetization samples
749    pub magnetization_samples: Vec<f64>,
750}
751
752#[cfg(test)]
753mod tests {
754    use super::*;
755    use crate::trotter::HamiltonianLibrary;
756
757    #[test]
758    fn test_walker() {
759        let walker = Walker::new(4);
760        assert_eq!(walker.config.len(), 4);
761        assert_eq!(walker.weight, Complex64::new(1.0, 0.0));
762    }
763
764    #[test]
765    fn test_wave_function_product() {
766        let amps = vec![Complex64::new(0.7, 0.0), Complex64::new(0.6, 0.0)];
767        let wf = WaveFunction::Product(amps);
768
769        let config = vec![true, false];
770        let amp = wf.amplitude(&config);
771        assert!((amp.norm() - 0.7 * 0.4).abs() < 1e-10);
772    }
773
774    #[test]
775    fn test_vmc_ising() {
776        let ham = HamiltonianLibrary::transverse_ising_1d(3, 1.0, 0.5, false).unwrap();
777        let wf = WaveFunction::Jastrow {
778            alpha: 0.5,
779            beta: 0.1,
780        };
781        let mut vmc = VMC::new(3, wf, ham);
782
783        let result = vmc.run(100, 50, 10, 0.01).unwrap();
784        assert!(result.final_energy.is_finite());
785    }
786
787    #[test]
788    fn test_dmc_simple() {
789        let ham = HamiltonianLibrary::transverse_ising_1d(2, 1.0, 1.0, false).unwrap();
790        // Use larger time step and fewer walkers for more stable test
791        let mut dmc = DMC::new(2, ham, 0.1, 50);
792
793        let result = dmc.run(5, 5).unwrap();
794        assert!(result.ground_state_energy.is_finite());
795    }
796
797    #[test]
798    fn test_pimc_thermal() {
799        let ham = HamiltonianLibrary::xy_model(3, 1.0, true).unwrap();
800        let pimc = PIMC::new(3, ham, 1.0, 10);
801
802        let result = pimc.run(100, 50).unwrap();
803        assert!(result.average_energy.is_finite());
804    }
805}