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