Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ use causal_triangulations::prelude::simulation::{MetropolisAlgorithm, Metropolis
use causal_triangulations::prelude::triangulation::CdtTriangulation;

fn main() -> CdtResult<()> {
// Create triangulation from random points
let triangulation = CdtTriangulation::from_random_points(20, 1, 2)?;
// Create a foliated open-boundary CDT strip.
let triangulation = CdtTriangulation::from_cdt_strip(8, 4)?;

// Configure and run the Monte Carlo simulation.
let metropolis_config = MetropolisConfig::new(1.0, 1000, 100, 10);
Expand Down
2 changes: 1 addition & 1 deletion docs/code_organization.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ The implementation is split into child modules under `src/cdt/triangulation/`:
- `OpenBoundary` (default) — finite strip with boundary, χ ∈ {1, 2}
- `Toroidal` — periodic in space and time, S¹×S¹, χ = 0
- Wired through `CdtConfig.topology`, `CdtConfigOverrides.topology`, the CLI `--topology` flag, and `CdtMetadata.topology`
- `run_simulation()` dispatches on topology: `Toroidal` → `from_toroidal_cdt`, `OpenBoundary` → `from_seeded_points` / `from_random_points`
- `run_simulation()` dispatches on topology: `Toroidal` → `from_toroidal_cdt`, `OpenBoundary` → `from_cdt_strip`; `vertices` is the total vertex count and must divide evenly across `timeslices`

### `cdt/metropolis.rs` — Metropolis move ordering

Expand Down
2 changes: 2 additions & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ This document summarizes the repository's current test coverage and the main gap

The issue #105 toroidal regression is covered by `tests/integration_tests.rs::test_toroidal_metropolis_preserves_topology_after_many_accepted_moves`, which runs a seeded S¹×S¹ Metropolis simulation, requires at least 100 accepted moves, and verifies topology, foliation, causality, cell classification, and χ = 0 at the end.

Open-boundary simulation entrypoints are covered by crate-root tests that require `run_simulation()` to build a foliated regular CDT strip, preserve adjacent-slice causality, classify cells, and report a non-empty volume profile. Configuration tests also reject open-boundary totals that cannot be split into equal-size spatial slices.

## Remaining Gaps

### Ergodic Move Sampling
Expand Down
6 changes: 3 additions & 3 deletions examples/basic_cdt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ fn main() -> CdtResult<()> {
info!("Starting basic CDT example");

// Configuration parameters
let vertices = 64;
let vertices_per_slice = 16;
let timeslices = 4;
let dimension = 2;
let vertices = vertices_per_slice * timeslices;

// Create initial triangulation
info!("Creating initial triangulation with {vertices} vertices and {timeslices} timeslices");
let triangulation = CdtTriangulation::from_random_points(vertices, timeslices, dimension)?;
let triangulation = CdtTriangulation::from_cdt_strip(vertices_per_slice, timeslices)?;

info!(
"Initial triangulation: {} vertices, {} edges, {} faces",
Expand Down
11 changes: 5 additions & 6 deletions src/cdt/ergodic_moves.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1196,21 +1196,20 @@ mod tests {
}

#[test]
fn toroidal_invariant_rejection_reports_topology_mismatch() {
fn checked_toroidal_wrapper_rejects_topology_mismatch() {
let dt = build_delaunay2_with_data(&[([0.0, 0.0], 0), ([1.0, 0.0], 0), ([0.5, 1.0], 1)])
.expect("build labeled triangle");
let backend = DelaunayBackend2D::from_triangulation(dt);
let triangulation = CdtTriangulation2D::with_topology(backend, 3, 2, CdtTopology::Toroidal)
.expect("toroidal metadata should be internally valid");
let result = CdtTriangulation2D::with_topology(backend, 3, 2, CdtTopology::Toroidal);

assert!(matches!(
toroidal_invariant_rejection(&triangulation),
Some(MoveResult::Rejected(CdtError::TopologyMismatch {
result,
Err(CdtError::TopologyMismatch {
topology,
euler_characteristic: 1,
expected_euler_characteristics,
..
})) if topology == "toroidal" && expected_euler_characteristics == [0]
}) if topology == "toroidal" && expected_euler_characteristics == [0]
));
}

Expand Down
41 changes: 26 additions & 15 deletions src/cdt/metropolis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,13 +250,16 @@ impl Target<CdtTriangulation2D> for CdtTarget {
/// ```
/// use causal_triangulations::prelude::action::ActionConfig;
/// use causal_triangulations::prelude::moves::MoveType;
/// use causal_triangulations::prelude::simulation::{CdtProposal, CdtTriangulation};
/// use causal_triangulations::prelude::simulation::{
/// CdtProposal, CdtProposalError, CdtTriangulation,
/// };
/// use markov_chain_monte_carlo::DelayedProposal;
/// use rand::{SeedableRng, rngs::StdRng};
///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let tri = CdtTriangulation::from_seeded_points(5, 1, 2, 53)?;
/// let mut proposal = CdtProposal::with_seed(ActionConfig::default(), 7)?;
/// # fn main() -> Result<(), CdtProposalError> {
/// let tri = CdtTriangulation::from_cdt_strip(4, 3).expect("valid CDT strip");
/// let mut proposal =
/// CdtProposal::with_seed(ActionConfig::default(), 7).expect("valid action config");
/// let mut rng = StdRng::seed_from_u64(11);
///
/// let Some(plan) = proposal.propose_plan(&tri, &mut rng)? else {
Expand Down Expand Up @@ -326,13 +329,16 @@ impl CdtProposalPlan {
///
/// ```
/// use causal_triangulations::prelude::action::ActionConfig;
/// use causal_triangulations::prelude::simulation::{CdtProposal, CdtTriangulation};
/// use causal_triangulations::prelude::simulation::{
/// CdtProposal, CdtProposalError, CdtTriangulation,
/// };
/// use markov_chain_monte_carlo::DelayedProposal;
/// use rand::{SeedableRng, rngs::StdRng};
///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let tri = CdtTriangulation::from_seeded_points(5, 1, 2, 53)?;
/// let mut proposal = CdtProposal::with_seed(ActionConfig::default(), 7)?;
/// # fn main() -> Result<(), CdtProposalError> {
/// let tri = CdtTriangulation::from_cdt_strip(4, 3).expect("valid CDT strip");
/// let mut proposal =
/// CdtProposal::with_seed(ActionConfig::default(), 7).expect("valid action config");
/// let mut rng = StdRng::seed_from_u64(11);
/// let Some(plan) = proposal.propose_plan(&tri, &mut rng)? else {
/// return Ok(());
Expand Down Expand Up @@ -472,13 +478,15 @@ impl Error for CdtProposalError {
///
/// ```
/// use causal_triangulations::prelude::action::ActionConfig;
/// use causal_triangulations::prelude::simulation::{CdtProposal, CdtTriangulation};
/// use causal_triangulations::prelude::simulation::{
/// CdtProposal, CdtProposalError, CdtTriangulation,
/// };
/// use markov_chain_monte_carlo::DelayedProposal;
/// use rand::{SeedableRng, rngs::StdRng};
///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let tri = CdtTriangulation::from_seeded_points(5, 1, 2, 53)?;
/// let mut proposal = CdtProposal::new(ActionConfig::default())?;
/// # fn main() -> Result<(), CdtProposalError> {
/// let tri = CdtTriangulation::from_cdt_strip(4, 3).expect("valid CDT strip");
/// let mut proposal = CdtProposal::new(ActionConfig::default()).expect("valid action config");
/// let mut rng = StdRng::seed_from_u64(7);
///
/// let plan = proposal.propose_plan(&tri, &mut rng)?;
Expand Down Expand Up @@ -921,15 +929,14 @@ fn apply_accepted_move(
) -> Result<AcceptedMoveResult, MoveApplicationError> {
let mut last_rejection = None;
for attempt in 1..=ACCEPTED_MOVE_RETRIES {
let snapshot = triangulation.clone();
let counts_before = simplex_counts(triangulation);
let result = attempt_move(moves, move_type, triangulation);
let rejection = match result {
MoveResult::Success => {
let action_after = action_for(action_config, triangulation);
return Ok(AcceptedMoveResult::Applied { action_after });
}
MoveResult::HardFailure(err) => {
*triangulation = snapshot;
return Err(MoveApplicationError {
attempt,
source: err,
Expand All @@ -939,7 +946,11 @@ fn apply_accepted_move(
MoveResult::GeometricViolation => CdtProposalSiteRejection::GeometricViolation,
MoveResult::Rejected(err) => CdtProposalSiteRejection::Kernel(err),
};
*triangulation = snapshot;
debug_assert_eq!(
simplex_counts(triangulation),
counts_before,
"failed move kernels must roll back simplex counts before returning"
);
last_rejection = Some(rejection);
}

Expand Down
41 changes: 34 additions & 7 deletions src/cdt/triangulation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,15 +133,17 @@ impl<B: TriangulationQuery> CdtTriangulation<B> {
/// Wraps an existing geometry backend and tags it with [`CdtTopology`].
/// The backend itself is not modified; pass a backend whose Euler
/// characteristic and metadata invariants match the supplied topology,
/// otherwise this constructor or [`Self::validate_topology`] will reject it.
/// otherwise this constructor rejects it.
///
/// Toroidal triangulations must use at least three time slices so the
/// periodic neighbors `t - 1` and `t + 1` remain distinct.
///
/// # Errors
///
/// Returns [`CdtError::InvalidTriangulationMetadata`] when the requested
/// topology metadata is internally inconsistent.
/// topology metadata is internally inconsistent. Returns
/// [`CdtError::TopologyMismatch`] when the backend Euler characteristic does
/// not match the requested topology.
///
/// # Examples
///
Expand Down Expand Up @@ -192,6 +194,32 @@ impl<B: TriangulationQuery> CdtTriangulation<B> {
/// && expected == "≥ 3"
/// ));
/// ```
///
/// ```rust
/// use causal_triangulations::prelude::errors::CdtError;
/// use causal_triangulations::prelude::geometry::*;
/// use causal_triangulations::prelude::triangulation::*;
///
/// let dt = build_delaunay2_with_data(&[
/// ([0.0, 0.0], 0),
/// ([1.0, 0.0], 0),
/// ([0.5, 1.0], 1),
/// ])
/// .expect("build labeled triangle");
/// let backend = DelaunayBackend2D::from_triangulation(dt);
///
/// let err = CdtTriangulation::with_topology(backend, 3, 2, CdtTopology::Toroidal)
/// .expect_err("a planar triangle cannot be published as toroidal");
/// assert!(matches!(
/// err,
/// CdtError::TopologyMismatch {
/// topology,
/// euler_characteristic: 1,
/// expected_euler_characteristics,
/// ..
/// } if topology == "toroidal" && expected_euler_characteristics == vec![0]
/// ));
/// ```
pub fn with_topology(
geometry: B,
time_slices: u32,
Expand All @@ -201,6 +229,7 @@ impl<B: TriangulationQuery> CdtTriangulation<B> {
let mut tri = Self::wrap_unchecked(geometry, time_slices, dimension, topology);
tri.apply_time_slices(time_slices)?;
tri.validate_metadata()?;
tri.validate_topology()?;
Ok(tri)
}

Expand Down Expand Up @@ -1198,15 +1227,13 @@ mod tests {

#[test]
fn test_validate_topology_toroidal_rejects_chi_nonzero() {
// Build a non-toroidal triangulation, then label its metadata as
// Toroidal so validate_topology() expects χ=0 but gets χ1.
// Build a non-toroidal triangulation, then try to label its metadata as
// Toroidal. The checked public constructor must reject it immediately.
let dt = build_delaunay2_with_data(&[([0.0, 0.0], 0), ([1.0, 0.0], 0), ([0.5, 1.0], 1)])
.expect("Should build labeled triangle");
let backend = DelaunayBackend2D::from_triangulation(dt);
let tri = CdtTriangulation::with_topology(backend, 3, 2, CdtTopology::Toroidal)
.expect("valid toroidal metadata");

let result = tri.validate_topology();
let result = CdtTriangulation::with_topology(backend, 3, 2, CdtTopology::Toroidal);
assert!(matches!(
result,
Err(CdtError::TopologyMismatch {
Expand Down
27 changes: 18 additions & 9 deletions src/cdt/triangulation/builders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,17 +178,20 @@ impl CdtTriangulation<DelaunayBackend2D> {
Ok(slice_sizes)
}

/// Creates a CDT triangulation with a Delaunay backend from random points.
/// Creates an unfoliated triangulation with a Delaunay backend from random points.
///
/// This is the recommended way to create triangulations for simulations.
/// This is useful for raw geometry tests and experiments. It does not
/// assign time labels or CDT cell classifications, so production CDT
/// simulations should prefer [`CdtTriangulation::from_cdt_strip`] or
/// [`CdtTriangulation::from_toroidal_cdt`].
///
/// # Errors
///
/// Returns [`CdtError::UnsupportedDimension`] if `dimension != 2`.
/// Returns [`CdtError::InvalidGenerationParameters`] if `vertices < 3`.
/// Returns [`CdtError::DelaunayGenerationFailed`] if random point generation
/// or Delaunay construction fails, and propagates validation errors from
/// [`CdtTriangulation::try_new`].
/// or Delaunay construction fails. Propagates metadata validation errors
/// from [`CdtTriangulation::try_new`], including `time_slices == 0`.
///
/// # Examples
///
Expand All @@ -198,6 +201,7 @@ impl CdtTriangulation<DelaunayBackend2D> {
/// fn main() -> CdtResult<()> {
/// let tri = CdtTriangulation::from_random_points(5, 2, 2)?;
/// assert_eq!(tri.time_slices(), 2);
/// assert!(!tri.has_foliation());
/// Ok(())
/// }
/// ```
Expand All @@ -220,18 +224,22 @@ impl CdtTriangulation<DelaunayBackend2D> {
Self::try_new(backend, time_slices, dimension)
}

/// Creates a CDT triangulation with a Delaunay backend from a fixed random seed.
/// Creates an unfoliated triangulation with a Delaunay backend from a fixed random seed.
///
/// Use this builder for examples, tests, benchmarks, and reproducible
/// simulations that need deterministic input geometry.
/// Use this builder for raw geometry examples, tests, and benchmarks that
/// need deterministic input geometry. It does not assign time labels or
/// CDT cell classifications, so production CDT simulations should prefer
/// [`CdtTriangulation::from_cdt_strip`] or
/// [`CdtTriangulation::from_toroidal_cdt`].
///
/// # Errors
///
/// Returns [`CdtError::UnsupportedDimension`] if `dimension != 2`.
/// Returns [`CdtError::InvalidGenerationParameters`] if `vertices < 3`.
/// Returns [`CdtError::DelaunayGenerationFailed`] if seeded point
/// generation or Delaunay construction fails, and propagates validation
/// errors from [`CdtTriangulation::try_new`].
/// generation or Delaunay construction fails. Propagates metadata
/// validation errors from [`CdtTriangulation::try_new`], including
/// `time_slices == 0`.
///
/// # Examples
///
Expand All @@ -241,6 +249,7 @@ impl CdtTriangulation<DelaunayBackend2D> {
/// fn main() -> CdtResult<()> {
/// let tri = CdtTriangulation::from_seeded_points(5, 2, 2, 53)?;
/// assert_eq!(tri.vertex_count(), 5);
/// assert!(!tri.has_foliation());
/// Ok(())
/// }
/// ```
Expand Down
Loading
Loading