use crate::pimc::paths::{squared_distance, RingPolymer};
#[derive(Debug, Clone)]
pub struct EnergyEstimator {
energy_sum: f64,
energy_sq_sum: f64, kinetic_sum: f64,
potential_sum: f64,
pub count: u64,
}
impl Default for EnergyEstimator {
fn default() -> Self {
Self::new()
}
}
impl EnergyEstimator {
pub fn new() -> Self {
Self {
energy_sum: 0.0,
energy_sq_sum: 0.0,
kinetic_sum: 0.0,
potential_sum: 0.0,
count: 0,
}
}
pub fn accumulate(
&mut self,
polymer: &RingPolymer,
potential: &dyn Fn(&[f64]) -> f64,
mass: f64,
tau: f64,
) {
let n = polymer.n_particles as f64;
let m = polymer.n_slices as f64;
let d = polymer.dimension as f64;
let spring_sq_sum: f64 = (0..polymer.n_particles)
.flat_map(|p| {
(0..polymer.n_slices).map(move |s| {
let s_next = (s + 1) % polymer.n_slices;
squared_distance(&polymer.beads[p][s], &polymer.beads[p][s_next])
})
})
.sum();
let kinetic = d * n / (2.0 * tau) - (mass / (2.0 * tau * tau)) * spring_sq_sum / m;
let pot_sum: f64 = (0..polymer.n_particles)
.flat_map(|p| (0..polymer.n_slices).map(move |s| potential(&polymer.beads[p][s])))
.sum();
let potential_energy = pot_sum / m;
let energy = kinetic + potential_energy;
self.kinetic_sum += kinetic;
self.potential_sum += potential_energy;
self.energy_sum += energy;
self.energy_sq_sum += energy * energy;
self.count += 1;
}
pub fn mean_energy(&self) -> f64 {
if self.count == 0 {
return 0.0;
}
self.energy_sum / self.count as f64
}
pub fn mean_kinetic(&self) -> f64 {
if self.count == 0 {
return 0.0;
}
self.kinetic_sum / self.count as f64
}
pub fn mean_potential(&self) -> f64 {
if self.count == 0 {
return 0.0;
}
self.potential_sum / self.count as f64
}
pub fn variance(&self) -> f64 {
if self.count < 2 {
return 0.0;
}
let n = self.count as f64;
let mean = self.energy_sum / n;
let mean_sq = self.energy_sq_sum / n;
(mean_sq - mean * mean).max(0.0)
}
pub fn std_energy(&self, n_samples: u64) -> f64 {
if n_samples < 2 {
return 0.0;
}
(self.variance() / n_samples as f64).sqrt()
}
pub fn reset(&mut self) {
self.energy_sum = 0.0;
self.energy_sq_sum = 0.0;
self.kinetic_sum = 0.0;
self.potential_sum = 0.0;
self.count = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pimc::{config::PimcConfig, paths::RingPolymer};
fn flat_polymer(n_particles: usize, n_slices: usize, dim: usize, val: f64) -> RingPolymer {
RingPolymer {
beads: vec![vec![vec![val; dim]; n_slices]; n_particles],
n_particles,
n_slices,
dimension: dim,
}
}
#[test]
fn test_estimator_initial_count() {
let est = EnergyEstimator::new();
assert_eq!(est.count, 0);
}
#[test]
fn test_estimator_after_one_accumulate() {
let mut est = EnergyEstimator::new();
let poly = flat_polymer(1, 4, 1, 0.0);
let tau = 0.25;
est.accumulate(&poly, &|_| 0.0, 1.0, tau);
assert_eq!(est.count, 1);
}
#[test]
fn test_estimator_reset() {
let mut est = EnergyEstimator::new();
let poly = flat_polymer(1, 4, 1, 0.0);
est.accumulate(&poly, &|_| 0.5, 1.0, 0.25);
assert_ne!(est.count, 0);
est.reset();
assert_eq!(est.count, 0);
assert_eq!(est.mean_energy(), 0.0);
}
#[test]
fn test_estimator_constant_path_zero_potential() {
let mut est = EnergyEstimator::new();
let poly = flat_polymer(1, 4, 1, 0.0);
let tau = 0.25; est.accumulate(&poly, &|_| 0.0, 1.0, tau);
let expected_kin = 1.0 * 1.0 / (2.0 * tau); assert!(
(est.mean_kinetic() - expected_kin).abs() < 1e-10,
"expected kinetic {expected_kin}, got {}",
est.mean_kinetic()
);
assert!(est.mean_potential().abs() < 1e-12);
}
}