use crate::cdt::action::ActionConfig;
use crate::cdt::ergodic_moves::{ErgodicsSystem, MoveType};
use crate::geometry::traits::TriangulationQuery;
use num_traits::cast::NumCast;
use std::time::Instant;
#[derive(Debug, Clone)]
pub struct MetropolisConfig {
pub temperature: f64,
pub steps: u32,
pub thermalization_steps: u32,
pub measurement_frequency: u32,
}
impl Default for MetropolisConfig {
fn default() -> Self {
Self {
temperature: 1.0,
steps: 1000,
thermalization_steps: 100,
measurement_frequency: 10,
}
}
}
impl MetropolisConfig {
#[must_use]
pub const fn new(
temperature: f64,
steps: u32,
thermalization_steps: u32,
measurement_frequency: u32,
) -> Self {
Self {
temperature,
steps,
thermalization_steps,
measurement_frequency,
}
}
#[must_use]
pub fn beta(&self) -> f64 {
1.0 / self.temperature
}
}
#[derive(Debug, Clone)]
pub struct MonteCarloStep {
pub step: u32,
pub move_type: MoveType,
pub accepted: bool,
pub action_before: f64,
pub action_after: Option<f64>,
pub delta_action: Option<f64>,
}
#[derive(Debug, Clone)]
pub struct Measurement {
pub step: u32,
pub action: f64,
pub vertices: u32,
pub edges: u32,
pub triangles: u32,
}
pub struct MetropolisAlgorithm {
config: MetropolisConfig,
action_config: ActionConfig,
ergodics: ErgodicsSystem,
}
impl MetropolisAlgorithm {
#[must_use]
pub fn new(config: MetropolisConfig, action_config: ActionConfig) -> Self {
Self {
config,
action_config,
ergodics: ErgodicsSystem::new(),
}
}
pub fn run(
&mut self,
triangulation: crate::geometry::CdtTriangulation2D,
) -> SimulationResultsBackend {
let start_time = Instant::now();
let mut steps = Vec::new();
let mut measurements = Vec::new();
log::info!("Starting Metropolis-Hastings simulation with new backend...");
log::info!("Temperature: {}", self.config.temperature);
log::info!("Total steps: {}", self.config.steps);
log::info!("Thermalization steps: {}", self.config.thermalization_steps);
let geometry = triangulation.geometry();
let current_action = self.action_config.calculate_action(
u32::try_from(geometry.vertex_count()).unwrap_or_default(),
u32::try_from(geometry.edge_count()).unwrap_or_default(),
u32::try_from(geometry.face_count()).unwrap_or_default(),
);
for step_num in 0..self.config.steps {
let move_type = self.ergodics.select_random_move();
let mc_step = MonteCarloStep {
step: step_num,
move_type,
accepted: false,
action_before: current_action,
action_after: None,
delta_action: None,
};
steps.push(mc_step);
if step_num % self.config.measurement_frequency == 0 {
let measurement = Measurement {
step: step_num,
action: current_action,
vertices: u32::try_from(geometry.vertex_count()).unwrap_or_default(),
edges: u32::try_from(geometry.edge_count()).unwrap_or_default(),
triangles: u32::try_from(geometry.face_count()).unwrap_or_default(),
};
measurements.push(measurement);
}
if step_num % 100 == 0 {
log::debug!(
"Step {}/{}, Action: {:.3}",
step_num,
self.config.steps,
current_action
);
}
}
let elapsed_time = start_time.elapsed();
log::info!("Simulation completed in {elapsed_time:.2?}");
SimulationResultsBackend {
config: self.config.clone(),
action_config: self.action_config.clone(),
steps,
measurements,
elapsed_time,
triangulation,
}
}
}
#[derive(Debug)]
pub struct SimulationResultsBackend {
pub config: MetropolisConfig,
pub action_config: ActionConfig,
pub steps: Vec<MonteCarloStep>,
pub measurements: Vec<Measurement>,
pub elapsed_time: std::time::Duration,
pub triangulation: crate::geometry::CdtTriangulation2D,
}
impl SimulationResultsBackend {
#[must_use]
pub fn acceptance_rate(&self) -> f64 {
if self.steps.is_empty() {
return 0.0;
}
let accepted_count = self.steps.iter().filter(|step| step.accepted).count();
let total_count = self.steps.len();
let accepted_f64 = NumCast::from(accepted_count).unwrap_or(0.0);
let total_f64 = NumCast::from(total_count).unwrap_or(1.0);
accepted_f64 / total_f64
}
#[must_use]
pub fn average_action(&self) -> f64 {
if self.measurements.is_empty() {
return 0.0;
}
let sum: f64 = self.measurements.iter().map(|m| m.action).sum();
let count = self.measurements.len();
let count_f64 = NumCast::from(count).unwrap_or(1.0);
sum / count_f64
}
#[must_use]
pub fn equilibrium_measurements(&self) -> Vec<&Measurement> {
self.measurements
.iter()
.filter(|m| m.step >= self.config.thermalization_steps)
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cdt::triangulation::CdtTriangulation;
use crate::geometry::traits::TriangulationQuery;
use approx::assert_relative_eq;
#[test]
fn test_metropolis_config() {
let config = MetropolisConfig::new(2.0, 500, 50, 5);
assert_relative_eq!(config.temperature, 2.0);
assert_relative_eq!(config.beta(), 0.5);
assert_eq!(config.steps, 500);
}
#[test]
fn test_backend_vertex_and_edge_counting() {
const TRIANGULATION_SEED: u64 = 53;
let triangulation = CdtTriangulation::from_seeded_points(5, 1, 2, TRIANGULATION_SEED)
.expect("Failed to create triangulation with fixed seed");
let geometry = triangulation.geometry();
assert!(
geometry.is_valid(),
"Triangulation should be structurally valid for backend queries"
);
assert_eq!(
geometry.vertex_count(),
5,
"Vertex count should match requested seeded generation"
);
assert!(geometry.edge_count() > 0, "Should have edges");
assert!(geometry.face_count() > 0, "Should have faces");
}
#[test]
fn test_action_calculation() {
let triangulation =
CdtTriangulation::from_random_points(5, 1, 2).expect("Failed to create triangulation");
let config = MetropolisConfig::default();
let action_config = ActionConfig::default();
let _algorithm = MetropolisAlgorithm::new(config, action_config.clone());
let geometry = triangulation.geometry();
let action = action_config.calculate_action(
u32::try_from(geometry.vertex_count()).unwrap_or_default(),
u32::try_from(geometry.edge_count()).unwrap_or_default(),
u32::try_from(geometry.face_count()).unwrap_or_default(),
);
assert!(action.is_finite());
}
#[test]
fn test_simulation_results() {
let config = MetropolisConfig::default();
let measurements = vec![
Measurement {
step: 0,
action: 1.0,
vertices: 3,
edges: 3,
triangles: 1,
},
Measurement {
step: 10,
action: 2.0,
vertices: 4,
edges: 5,
triangles: 2,
},
];
let triangulation =
CdtTriangulation::from_random_points(3, 1, 2).expect("Failed to create triangulation");
let results = SimulationResultsBackend {
config,
action_config: ActionConfig::default(),
steps: vec![],
measurements,
elapsed_time: std::time::Duration::from_millis(100),
triangulation,
};
assert_relative_eq!(results.average_action(), 1.5);
}
}