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