#![forbid(unsafe_code)]
#![expect(
clippy::multiple_crate_versions,
reason = "transitive dependencies currently resolve several shared crate versions"
)]
#![warn(missing_docs)]
pub mod config;
pub mod errors;
pub mod util;
pub mod geometry {
pub mod operations;
pub mod traits;
pub mod generators;
pub mod backends {
pub mod delaunay;
pub mod mock;
}
pub type DelaunayBackend2D = backends::delaunay::DelaunayBackend<u32, i32, 2>;
pub type DefaultBackend = DelaunayBackend2D;
pub type CdtTriangulation2D = crate::cdt::triangulation::CdtTriangulation<DefaultBackend>;
pub use generators::{GlobalTopology, TopologyGuarantee, ToroidalConstructionMode};
}
pub mod cdt {
pub mod action;
pub mod ergodic_moves;
pub mod foliation;
pub mod metropolis {
pub mod adapter;
pub mod checkpoint;
pub(crate) mod helpers;
pub mod runner;
pub mod telemetry;
pub use adapter::{
CdtProposal, CdtProposalError, CdtProposalInfo, CdtProposalPlan, CdtTarget,
};
pub use checkpoint::CdtMcmcCheckpoint;
pub use markov_chain_monte_carlo::{
ChainId, StepOutcome, Trace, TraceError, TraceRecord, TraceStepOutcome,
};
pub use runner::{MetropolisAlgorithm, MetropolisConfig};
pub use telemetry::{
AcceptedStepTelemetry, MonteCarloStep, MonteCarloStepOutcome, ProposalStatistics,
RejectedProposalStepTelemetry,
};
}
pub mod observables;
pub mod results;
#[path = "triangulation/state.rs"]
pub mod triangulation;
}
pub use cdt::action::{
ActionConfig, CDT_1P1_CRITICAL_TRIANGLE_COSMOLOGICAL_CONSTANT,
DEFAULT_CDT_1P1_EDGE_COSMOLOGICAL_CONSTANT, compute_regge_action,
};
pub use cdt::ergodic_moves::{ErgodicsSystem, MoveResult, MoveStatistics, MoveType};
pub use cdt::foliation::{EdgeType, Foliation, FoliationError, SimplexType};
pub use cdt::metropolis::{
AcceptedStepTelemetry, CdtMcmcCheckpoint, CdtProposal, CdtProposalError, CdtProposalInfo,
CdtProposalPlan, CdtTarget, MetropolisAlgorithm, MetropolisConfig, MonteCarloStep,
MonteCarloStepOutcome, ProposalStatistics, RejectedProposalStepTelemetry, StepOutcome,
};
pub use cdt::observables::{estimate_hausdorff_dimension, estimate_spectral_dimension};
pub use cdt::results::{Measurement, SimulationResultsBackend};
pub use cdt::triangulation::{CdtSimplexCounts, CdtTriangulation, SimulationEvent};
pub use config::{
CdtConfig, CdtConfigOverrides, CdtTopology, DimensionOverride, TestConfig, ValidatedCdtConfig,
ValidatedInitialVolume,
};
pub use errors::{
BackendMutationOperation, CdtError, CdtResult, CdtValidationCheck, CdtValidationFailure,
CheckpointMoveCounter, CheckpointOperation, CheckpointResumeFailure, ConfigurationSetting,
DelaunayValidationLevel, GenerationParameterIssue, MeasurementCountField,
MetropolisMoveApplicationFailure, OutputFormat, ProposalTelemetryCounter, ScalarTraceField,
SimplexCountField, TriangulationMetadataField,
};
pub use geometry::traits::TriangulationQuery;
use crate::cdt::results::SimulationResultsParts;
use std::env;
use std::path::PathBuf;
use std::time::Duration;
pub mod prelude {
pub use crate::geometry::CdtTriangulation2D;
pub use crate::geometry::traits::TriangulationQuery;
pub use crate::{CdtSimplexCounts, CdtTriangulation};
pub use crate::cdt::action::ActionConfig;
pub use crate::cdt::metropolis::{MetropolisAlgorithm, MetropolisConfig};
pub use crate::run_simulation;
pub use crate::config::{CdtConfig, CdtTopology, ValidatedCdtConfig};
pub use crate::errors::{CdtError, CdtResult};
pub mod config {
pub use crate::config::{
CdtConfig, CdtConfigOverrides, CdtTopology, DimensionOverride, TestConfig,
ValidatedCdtConfig, ValidatedInitialVolume,
};
}
pub mod errors {
pub use crate::errors::{
BackendMutationOperation, CdtError, CdtResult, CdtValidationCheck,
CdtValidationFailure, CheckpointMoveCounter, CheckpointOperation,
CheckpointResumeFailure, ConfigurationSetting, DelaunayValidationLevel,
GenerationParameterIssue, MeasurementCountField, MetropolisMoveApplicationFailure,
OutputFormat, ProposalTelemetryCounter, ScalarTraceField, SimplexCountField,
TriangulationMetadataField,
};
}
pub mod action {
pub use crate::cdt::action::{
ActionConfig, CDT_1P1_CRITICAL_TRIANGLE_COSMOLOGICAL_CONSTANT,
DEFAULT_CDT_1P1_EDGE_COSMOLOGICAL_CONSTANT, compute_regge_action,
};
}
pub mod triangulation {
pub use crate::cdt::foliation::{EdgeType, Foliation, FoliationError, SimplexType};
pub use crate::config::CdtTopology;
pub use crate::errors::{CdtError, CdtResult};
pub use crate::geometry::CdtTriangulation2D;
pub use crate::geometry::traits::TriangulationQuery;
pub use crate::{CdtSimplexCounts, CdtTriangulation, SimulationEvent};
}
pub mod moves {
pub use crate::cdt::ergodic_moves::{ErgodicsSystem, MoveResult, MoveStatistics, MoveType};
}
pub mod simulation {
pub use crate::cdt::action::{ActionConfig, compute_regge_action};
pub use crate::cdt::ergodic_moves::MoveType;
pub use crate::cdt::metropolis::{
AcceptedStepTelemetry, CdtMcmcCheckpoint, CdtProposal, CdtProposalError,
CdtProposalInfo, CdtProposalPlan, CdtTarget, MetropolisAlgorithm, MetropolisConfig,
MonteCarloStep, MonteCarloStepOutcome, ProposalStatistics,
RejectedProposalStepTelemetry,
};
pub use crate::cdt::results::{Measurement, SimulationResultsBackend};
pub use crate::cdt::triangulation::SimulationEvent;
pub use crate::config::{CdtConfig, CdtTopology, ValidatedCdtConfig};
pub use crate::errors::{CdtError, CdtResult};
pub use crate::geometry::CdtTriangulation2D;
pub use crate::geometry::traits::TriangulationQuery;
pub use crate::{CdtSimplexCounts, CdtTriangulation, run_simulation};
pub use markov_chain_monte_carlo::{
ChainCheckpoint, ChainId, DelayedProposal, StepOutcome, Target, Trace, TraceError,
TraceRecord, TraceStepOutcome,
};
}
pub mod observables {
pub use crate::CdtTriangulation;
pub use crate::cdt::observables::{
estimate_hausdorff_dimension, estimate_spectral_dimension,
};
pub use crate::geometry::CdtTriangulation2D;
}
pub mod geometry {
pub use crate::geometry::DelaunayBackend2D;
pub use crate::geometry::backends::delaunay::{
DelaunayBackend, DelaunayError, DelaunayOperation, NonFlippableEdgeReason,
};
pub use crate::geometry::generators::{
GlobalTopology, TopologyGuarantee, ToroidalConstructionMode,
build_delaunay2_from_simplices, build_delaunay2_with_data,
build_delaunay2_with_topology, build_periodic_toroidal_delaunay2,
build_toroidal_delaunay2, generate_delaunay2,
};
pub use crate::geometry::operations::TriangulationOps;
pub use crate::geometry::traits::{
EdgeAdjacentFaces, EdgeAdjacentFacesResult, FlipResult, GeometryBackend,
SubdivisionResult, TriangulationMut, TriangulationQuery,
};
}
pub mod testing {
pub use crate::geometry::backends::mock::{
MockBackend, MockError, MockNonFlippableReason, MockOperation, MockStorageTarget,
};
pub use crate::geometry::operations::TriangulationOps;
pub use crate::geometry::traits::{TriangulationMut, TriangulationQuery};
}
}
pub fn run_simulation(config: &ValidatedCdtConfig) -> CdtResult<SimulationResultsBackend> {
let output_paths = resolve_configured_output_paths(config)?;
let vertices = config.vertices();
let timeslices = config.timeslices();
log::info!("Dimensionality: {}", config.dimension());
log::info!("Number of vertices: {vertices}");
log::info!("Number of timeslices: {timeslices}");
if let Some(profile) = config.volume_profile() {
log::info!("Initial spatial volume profile: {profile:?}");
}
log::info!("Topology: {:?}", config.topology());
log::info!("Using trait-based backend system");
let triangulation = match (config.topology(), config.initial_volume()) {
(CdtTopology::Toroidal, ValidatedInitialVolume::ExplicitProfile(profile)) => {
log::info!("Constructing toroidal CDT (S¹×S¹)");
let profile: Vec<_> = profile.iter().map(|volume| volume.get()).collect();
CdtTriangulation::from_toroidal_cdt_profile(&profile)?
}
(CdtTopology::Toroidal, ValidatedInitialVolume::Regular { vertices_per_slice }) => {
log::info!("Constructing toroidal CDT (S¹×S¹)");
CdtTriangulation::from_toroidal_cdt(vertices_per_slice.get(), timeslices.get())?
}
(CdtTopology::OpenBoundary, ValidatedInitialVolume::ExplicitProfile(profile)) => {
log::info!("Constructing open-boundary CDT strip");
let profile: Vec<_> = profile.iter().map(|volume| volume.get()).collect();
CdtTriangulation::from_cdt_strip_profile(&profile)?
}
(CdtTopology::OpenBoundary, ValidatedInitialVolume::Regular { vertices_per_slice }) => {
log::info!("Constructing open-boundary CDT strip");
CdtTriangulation::from_cdt_strip(vertices_per_slice.get(), timeslices.get())?
}
};
log::info!(
"Triangulation created with {} vertices, {} edges, {} faces",
triangulation.vertex_count(),
triangulation.edge_count(),
triangulation.face_count()
);
let results = if config.simulate() {
let metropolis_config = config.to_metropolis_config();
let action_config = config.to_action_config();
let algorithm = MetropolisAlgorithm::new(metropolis_config, action_config);
let results = algorithm.run(triangulation)?;
log::info!("Simulation Results:");
log::info!(
" Acceptance rate: {:.2}%",
results.acceptance_rate() * 100.0
);
log::info!(" Average action: {:.3}", results.average_action());
results
} else {
let counts = triangulation.simplex_counts()?;
let action_config = config.to_action_config();
let initial_action = action_config.calculate_action(
counts.vertex_count(),
counts.edge_count(),
counts.triangle_count(),
);
let measurement = Measurement::try_from_simplex_counts(0, initial_action, counts)?
.try_with_volume_profile(triangulation.volume_profile())?;
SimulationResultsBackend::from_parts(SimulationResultsParts {
config: config.to_metropolis_config(),
action_config,
move_stats: MoveStatistics::new(),
proposal_stats: ProposalStatistics::new(),
steps: vec![],
measurements: vec![measurement],
scalar_trace_rows: vec![],
elapsed_time: Duration::from_millis(0),
triangulation,
})
};
write_configured_outputs(config, &results, &output_paths)?;
Ok(results)
}
struct ResolvedOutputPaths {
csv: Option<PathBuf>,
json: Option<PathBuf>,
}
fn resolve_configured_output_paths(
validated_config: &ValidatedCdtConfig,
) -> CdtResult<ResolvedOutputPaths> {
if validated_config.output_csv().is_none() && validated_config.output_json().is_none() {
return Ok(ResolvedOutputPaths {
csv: None,
json: None,
});
}
let base_dir = env::current_dir().map_err(|err| CdtError::OutputPathResolutionFailed {
base_path: ".".to_string(),
detail: err.to_string(),
})?;
let resolved_csv = validated_config
.output_csv()
.map(|path| CdtConfig::resolve_path(&base_dir, path));
let resolved_json = validated_config
.output_json()
.map(|path| CdtConfig::resolve_path(&base_dir, path));
if let (Some(csv_path), Some(json_path)) = (&resolved_csv, &resolved_json)
&& csv_path == json_path
{
return Err(CdtError::OutputPathConflict {
csv_path: csv_path.display().to_string(),
json_path: json_path.display().to_string(),
});
}
Ok(ResolvedOutputPaths {
csv: resolved_csv,
json: resolved_json,
})
}
fn write_configured_outputs(
validated_config: &ValidatedCdtConfig,
results: &SimulationResultsBackend,
output_paths: &ResolvedOutputPaths,
) -> CdtResult<()> {
if let Some(resolved) = &output_paths.csv {
results.write_trace_csv(resolved)?;
log::info!("Wrote trace CSV to {}", resolved.display());
}
if let Some(resolved) = &output_paths.json {
results.write_summary_json(validated_config, resolved)?;
log::info!("Wrote simulation JSON summary to {}", resolved.display());
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cdt::action::DEFAULT_CDT_1P1_EDGE_COSMOLOGICAL_CONSTANT;
use approx::assert_relative_eq;
use serde_json::{Value, from_str};
use std::assert_matches;
use std::env;
use std::fs;
use std::path::PathBuf;
use std::process;
use std::thread;
fn create_test_config() -> CdtConfig {
CdtConfig {
dimension: Some(2),
vertices: 36,
timeslices: 3,
volume_profile: None,
temperature: 1.0,
steps: 10,
thermalization_steps: 5,
measurement_frequency: 2,
coupling_0: 0.0,
coupling_2: 0.0,
cosmological_constant: DEFAULT_CDT_1P1_EDGE_COSMOLOGICAL_CONSTANT,
simulate: false,
seed: Some(42),
topology: CdtTopology::OpenBoundary,
output_csv: None,
output_json: None,
}
}
fn validated(config: CdtConfig) -> ValidatedCdtConfig {
config
.into_validated()
.expect("test config should validate")
}
fn temp_output_path(name: &str) -> PathBuf {
let thread_name = safe_thread_name();
env::temp_dir().join(format!(
"causal-triangulations-run-{name}-{}-{}",
process::id(),
thread_name
))
}
fn safe_thread_name() -> String {
thread::current()
.name()
.unwrap_or("test")
.chars()
.map(|ch| match ch {
'<' | '>' | ':' | '"' | '/' | '\\' | '|' | '?' | '*' => '_',
ch if ch.is_control() => '_',
ch => ch,
})
.collect()
}
#[test]
fn test_run_simulation() {
let config = validated(create_test_config());
assert_eq!(config.dimension(), 2);
let results = run_simulation(&config).expect("Failed to run triangulation");
assert!(results.triangulation().face_count() > 0);
assert!(results.triangulation().has_foliation());
assert_eq!(results.triangulation().slice_sizes(), &[12, 12, 12]);
assert!(!results.triangulation().volume_profile().is_empty());
results
.triangulation()
.validate_foliation()
.expect("open-boundary run should build a valid foliation");
results
.triangulation()
.validate_causality()
.expect("open-boundary run should preserve adjacent-slice causality");
results
.triangulation()
.validate_simplex_classification()
.expect("open-boundary run should classify CDT simplices");
assert!(!results.measurements().is_empty());
}
#[test]
fn construction_only_results_roundtrip_through_serde() {
let config = validated(create_test_config());
let results = run_simulation(&config).expect("construction-only run should succeed");
assert!(results.steps().is_empty());
assert_eq!(
results.measurements().first().map(Measurement::step),
Some(0)
);
let json = serde_json::to_string(&results).expect("construction snapshot should serialize");
let roundtrip: SimulationResultsBackend =
from_str(&json).expect("construction snapshot should deserialize");
assert!(roundtrip.steps().is_empty());
assert_eq!(
roundtrip.measurements().first().map(Measurement::step),
Some(0)
);
assert_eq!(roundtrip.triangulation().slice_sizes(), &[12, 12, 12]);
}
#[test]
fn triangulation_contains_triangles() {
let config = validated(create_test_config());
let results = run_simulation(&config).expect("Failed to run triangulation");
assert!(results.triangulation().face_count() > 0);
}
#[test]
fn run_simulation_writes_configured_outputs() {
let csv_path = temp_output_path("trace.csv");
let json_path = temp_output_path("summary.json");
let mut config = create_test_config();
config.output_csv = Some(csv_path.clone());
config.output_json = Some(json_path.clone());
let config = validated(config);
run_simulation(&config).expect("configured outputs should write");
let csv = fs::read_to_string(&csv_path).expect("CSV output should be readable");
let json = fs::read_to_string(&json_path).expect("JSON output should be readable");
fs::remove_file(&csv_path).expect("temporary CSV output should be removable");
fs::remove_file(&json_path).expect("temporary JSON output should be removable");
let parsed: Value = from_str(&json).expect("JSON output should parse");
assert!(csv.starts_with(
"chain_id,step,accepted,proposed,log_prob,action,vertices,edges,triangles,move_family"
));
assert_eq!(parsed["config"]["vertices"], config.vertices().get());
assert_eq!(
parsed["final_triangulation"]["time_slices"],
config.timeslices().get()
);
}
#[test]
fn run_simulation_rejects_overlapping_output_paths() {
let path = temp_output_path("shared-output");
let mut config = create_test_config();
config.output_csv = Some(path.clone());
config.output_json = Some(path.clone());
let config = validated(config);
let error = run_simulation(&config).expect_err("overlapping outputs should fail");
let CdtError::OutputPathConflict {
csv_path,
json_path,
} = error
else {
panic!("expected output path conflict error");
};
assert_eq!(csv_path, path.display().to_string());
assert_eq!(json_path, path.display().to_string());
assert!(!path.exists());
}
#[test]
fn test_config_validation_invalid_measurement_frequency() {
let mut config = create_test_config();
config.measurement_frequency = 0;
let result = config.into_validated();
assert!(result.is_err(), "Should reject zero measurement frequency");
if let Err(CdtError::InvalidSimulationConfiguration {
setting,
provided_value,
expected,
}) = result
{
assert_eq!(setting, ConfigurationSetting::MeasurementFrequency);
assert_eq!(provided_value, "0");
assert_eq!(expected, "≥ 1");
} else {
panic!("Expected InvalidSimulationConfiguration error");
}
}
#[test]
fn test_config_validation_measurement_frequency_too_large() {
let mut config = create_test_config();
config.steps = 100;
config.measurement_frequency = 200;
let result = config.into_validated();
assert!(
result.is_err(),
"Should reject measurement frequency greater than steps"
);
if let Err(CdtError::InvalidSimulationConfiguration {
setting,
provided_value,
expected,
}) = result
{
assert_eq!(setting, ConfigurationSetting::MeasurementFrequency);
assert_eq!(provided_value, "200");
assert_eq!(expected, "≤ steps (100)");
} else {
panic!("Expected InvalidSimulationConfiguration error");
}
}
#[test]
fn test_config_validation_invalid_vertices() {
let mut config = create_test_config();
config.vertices = 2;
let result = config.into_validated();
assert!(result.is_err(), "Should reject too few vertices");
if let Err(CdtError::InvalidConfiguration {
setting,
provided_value,
expected,
}) = result
{
assert_eq!(setting, ConfigurationSetting::Vertices);
assert_eq!(provided_value, "2");
assert_eq!(expected, "≥ 3");
} else {
panic!("Expected InvalidConfiguration error");
}
}
#[test]
fn test_config_validation_negative_temperature() {
let mut config = create_test_config();
config.temperature = -1.0;
let result = config.into_validated();
assert!(result.is_err(), "Should reject negative temperature");
if let Err(CdtError::InvalidSimulationConfiguration {
setting,
provided_value,
expected,
}) = result
{
assert_eq!(setting, ConfigurationSetting::Temperature);
assert_eq!(provided_value, "-1");
assert_eq!(expected, "finite and positive");
} else {
panic!("Expected InvalidSimulationConfiguration error");
}
}
#[test]
fn test_run_simulation_with_real_moves() {
let mut config = create_test_config();
config.simulate = true;
let config = validated(config);
let results = run_simulation(&config).expect("simulation should run with real moves");
assert_eq!(
results.steps().len(),
usize::try_from(config.to_metropolis_config().steps().get()).unwrap()
);
assert!(results.triangulation().has_foliation());
results
.triangulation()
.validate_foliation()
.expect("simulated open-boundary run should keep valid foliation");
results
.triangulation()
.validate_causality()
.expect("simulated open-boundary run should keep adjacent-slice causality");
results
.triangulation()
.validate_simplex_classification()
.expect("simulated open-boundary run should keep CDT simplex classification");
assert!(!results.measurements().is_empty());
}
#[test]
fn test_config_conversions() {
let config = create_test_config()
.into_validated()
.expect("test config should validate");
let metropolis_config = config.to_metropolis_config();
assert_relative_eq!(metropolis_config.temperature(), 1.0);
assert_eq!(metropolis_config.steps().get(), 10);
let action_config = config.to_action_config();
assert_relative_eq!(action_config.coupling_0(), 0.0);
assert_relative_eq!(action_config.coupling_2(), 0.0);
assert_relative_eq!(
action_config.cosmological_constant(),
DEFAULT_CDT_1P1_EDGE_COSMOLOGICAL_CONSTANT
);
}
#[test]
fn test_run_simulation_toroidal_uses_total_vertex_count() {
let config = CdtConfig {
dimension: Some(2),
vertices: 12,
timeslices: 3,
volume_profile: None,
temperature: 1.0,
steps: 10,
thermalization_steps: 5,
measurement_frequency: 2,
coupling_0: 0.0,
coupling_2: 0.0,
cosmological_constant: DEFAULT_CDT_1P1_EDGE_COSMOLOGICAL_CONSTANT,
simulate: false,
seed: None,
topology: CdtTopology::Toroidal,
output_csv: None,
output_json: None,
};
let config = validated(config);
let results = run_simulation(&config).expect("toroidal simulation should run");
assert_eq!(
results.triangulation().vertex_count(),
12,
"Toroidal run_simulation must treat config.vertices as the TOTAL vertex count"
);
assert_eq!(
results.triangulation().time_slices().get(),
3,
"Toroidal run_simulation must preserve the configured timeslice count"
);
assert_matches!(
results.triangulation().metadata().topology(),
CdtTopology::Toroidal
);
}
#[test]
fn test_run_simulation_uses_nonuniform_volume_profile() {
let config = CdtConfig {
vertices: 15,
timeslices: 3,
volume_profile: Some(vec![4, 6, 5]),
steps: 4,
thermalization_steps: 0,
measurement_frequency: 1,
..create_test_config()
};
let config = validated(config);
let results = run_simulation(&config).expect("profile-based simulation should run");
assert_eq!(results.triangulation().vertex_count(), 15);
assert_eq!(results.triangulation().slice_sizes(), &[4, 6, 5]);
assert_eq!(results.measurements()[0].volume_profile().len(), 3);
results
.triangulation()
.validate()
.expect("profile-based initial CDT should satisfy evolved invariants");
}
#[test]
fn test_run_simulation_uses_nonuniform_toroidal_volume_profile() {
let config = CdtConfig {
vertices: 16,
timeslices: 4,
topology: CdtTopology::Toroidal,
volume_profile: Some(vec![3, 4, 5, 4]),
steps: 4,
thermalization_steps: 0,
measurement_frequency: 1,
..create_test_config()
};
let config = validated(config);
let results =
run_simulation(&config).expect("toroidal profile-based simulation should run");
assert_eq!(results.triangulation().vertex_count(), 16);
assert_eq!(results.triangulation().slice_sizes(), &[3, 4, 5, 4]);
assert_matches!(
results.triangulation().metadata().topology(),
CdtTopology::Toroidal
);
results
.triangulation()
.validate()
.expect("toroidal profile-based initial CDT should satisfy evolved invariants");
}
}