Skip to content
This repository was archived by the owner on Aug 12, 2026. It is now read-only.
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: 3 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,19 @@ project(
# the product version used by executables and release metadata.
set(CDT_VERSION_SUFFIX "-rc1")
set(CDT_VERSION "${PROJECT_VERSION}${CDT_VERSION_SUFFIX}")
configure_file(cmake/Version.hpp.in "${PROJECT_BINARY_DIR}/Version.hpp" @ONLY)

# Project settings
include(cmake/StandardProjectSettings.cmake)
configure_file(cmake/Version.hpp.in "${PROJECT_BINARY_DIR}/Version.hpp" @ONLY)

# Prevent in source builds
include(cmake/PreventInSourceBuilds.cmake)

# Link this 'library' to set the c++ standard / compile-time options requested
add_library(project_options INTERFACE)
target_compile_features(project_options INTERFACE cxx_std_23)
target_compile_definitions(
project_options INTERFACE CDT_BUILD_CONFIGURATION="$<CONFIG>")

if(CMAKE_CXX_COMPILER_ID MATCHES ".*Clang")
option(ENABLE_BUILD_WITH_TIME_TRACE "Enable -ftime-trace to generate time tracing .json files on clang" OFF)
Expand Down
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,16 @@ triangulation files:
just run -s -n256 -t4 -a0.6 -k1.1 -l0.1 -p10 -c10 --no-output
```

With output enabled, every generated `.off` triangulation is accompanied by a
`.off.meta` provenance manifest containing the effective seed, configuration,
version/toolchain identity, transition-trace fingerprint, and payload checksum.
Checkpoint files are validated snapshots, not resumable simulation states; see
[`docs/reproducibility.md`](docs/reproducibility.md) for the replay and
persistence contract. Same-seed generation replays the random inputs, while
exact transition replay requires an identical starting manifold; CDT++ does
not alter its spherical construction to force CGAL to reproduce one of several
valid cospherical tetrahedralizations.

- `cdt-viewer` is currently disabled and will be restored as an opt-in v1.0.0 target by
[#98](https://github.com/acgetchell/CDT-plusplus/issues/98)
- `initialize` is used by [CometML] to run [parameter optimization](#optimizing-parameters)
Expand Down Expand Up @@ -471,8 +481,9 @@ uv run --locked --group experiments cdt-mnist-experiment

Run these commands from the repository root. Set `COMET_API_KEY` before starting the parameter optimization; use
`--repository-root` when invoking it from another directory. The optimizer uses seed `92` by default for every
parameter pair so results can be compared and replayed; pass `--seed SEED` to select and record another root seed. The
experiment results are then available in Comet.
parameter pair so stochastic inputs and provenance can be compared; pass `--seed SEED` to select and record another
root seed. Fresh CGAL triangulations are subject to the limits in the
[reproducibility contract](docs/reproducibility.md). The experiment results are then available in Comet.
Migration of these legacy scripts to Python 3.14, PyTorch, and the current Comet API is tracked by
[#104](https://github.com/acgetchell/CDT-plusplus/issues/104).

Expand Down
25 changes: 20 additions & 5 deletions cmake/RunCdtNoOutputTest.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,25 @@ if(NOT DEFINED CDT_EXECUTABLE OR NOT EXISTS "${CDT_EXECUTABLE}")
message(FATAL_ERROR "CDT_EXECUTABLE must name the built cdt executable")
endif()

if(NOT DEFINED TEST_DIRECTORY OR TEST_DIRECTORY STREQUAL "" OR TEST_DIRECTORY STREQUAL "/")
if(NOT DEFINED TEST_ROOT OR TEST_ROOT STREQUAL "")
message(FATAL_ERROR "TEST_ROOT must name the owning test root")
endif()

if(NOT DEFINED TEST_DIRECTORY OR TEST_DIRECTORY STREQUAL "")
message(FATAL_ERROR "TEST_DIRECTORY must name a dedicated test directory")
endif()

set(normalized_test_root "${TEST_ROOT}")
set(normalized_test_directory "${TEST_DIRECTORY}")
cmake_path(ABSOLUTE_PATH normalized_test_root NORMALIZE)
cmake_path(ABSOLUTE_PATH normalized_test_directory NORMALIZE)
cmake_path(
IS_PREFIX normalized_test_root "${normalized_test_directory}" NORMALIZE
test_directory_is_owned)
if(NOT test_directory_is_owned OR normalized_test_directory STREQUAL normalized_test_root)
message(FATAL_ERROR "TEST_DIRECTORY must be a strict descendant of TEST_ROOT")
endif()

execute_process(
COMMAND "${CDT_EXECUTABLE}" --help
RESULT_VARIABLE help_result
Expand All @@ -18,12 +33,12 @@ if(NOT help_output MATCHES "--no-output" OR NOT help_output MATCHES "--seed")
message(FATAL_ERROR "cdt --help does not document --no-output and --seed")
endif()

file(REMOVE_RECURSE "${TEST_DIRECTORY}")
file(MAKE_DIRECTORY "${TEST_DIRECTORY}")
file(REMOVE_RECURSE "${normalized_test_directory}")
file(MAKE_DIRECTORY "${normalized_test_directory}")

execute_process(
COMMAND "${CDT_EXECUTABLE}" -s -n64 -t3 -a0.6 -k1.1 -l0.1 -p1 -c1 --no-output --seed 92
WORKING_DIRECTORY "${TEST_DIRECTORY}"
WORKING_DIRECTORY "${normalized_test_directory}"
RESULT_VARIABLE run_result
OUTPUT_VARIABLE run_output
ERROR_VARIABLE run_error)
Expand All @@ -37,7 +52,7 @@ if(NOT run_output MATCHES "Effective random seed: 92")
message(FATAL_ERROR "cdt did not report the requested effective seed:\n${run_output}")
endif()

file(GLOB_RECURSE generated_files LIST_DIRECTORIES false "${TEST_DIRECTORY}/*")
file(GLOB_RECURSE generated_files LIST_DIRECTORIES false "${normalized_test_directory}/*")
if(generated_files)
list(JOIN generated_files "\n " generated_file_list)
message(FATAL_ERROR "cdt --no-output created files:\n ${generated_file_list}")
Expand Down
98 changes: 98 additions & 0 deletions cmake/RunPersistenceOutputTest.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
if(NOT DEFINED TEST_EXECUTABLE OR NOT EXISTS "${TEST_EXECUTABLE}")
message(FATAL_ERROR "TEST_EXECUTABLE must name a built executable")
endif()

if(NOT DEFINED TEST_ROOT OR TEST_ROOT STREQUAL "")
message(FATAL_ERROR "TEST_ROOT must name the owning test root")
endif()

if(NOT DEFINED TEST_DIRECTORY OR TEST_DIRECTORY STREQUAL "")
message(FATAL_ERROR "TEST_DIRECTORY must name a dedicated test directory")
endif()

set(normalized_test_root "${TEST_ROOT}")
set(normalized_test_directory "${TEST_DIRECTORY}")
cmake_path(ABSOLUTE_PATH normalized_test_root NORMALIZE)
cmake_path(ABSOLUTE_PATH normalized_test_directory NORMALIZE)
cmake_path(
IS_PREFIX normalized_test_root "${normalized_test_directory}" NORMALIZE
test_directory_is_owned)
if(NOT test_directory_is_owned OR normalized_test_directory STREQUAL normalized_test_root)
message(FATAL_ERROR "TEST_DIRECTORY must be a strict descendant of TEST_ROOT")
endif()

if(NOT DEFINED EXPECTED_ARTIFACT OR EXPECTED_ARTIFACT STREQUAL "")
message(FATAL_ERROR "EXPECTED_ARTIFACT must name the persisted artifact kind")
endif()

file(REMOVE_RECURSE "${normalized_test_directory}")
file(MAKE_DIRECTORY "${normalized_test_directory}")

execute_process(
COMMAND "${TEST_EXECUTABLE}" ${TEST_ARGUMENTS}
WORKING_DIRECTORY "${normalized_test_directory}"
RESULT_VARIABLE run_result
OUTPUT_VARIABLE run_output
ERROR_VARIABLE run_error)
if(NOT run_result EQUAL 0)
message(FATAL_ERROR "Persistence command failed:\n${run_output}\n${run_error}")
endif()
if(NOT run_output MATCHES "Effective random seed: ([0-9]+)")
message(FATAL_ERROR "Command did not report the requested seed:\n${run_output}")
endif()
set(reported_seed "${CMAKE_MATCH_1}")
if(DEFINED EXPECTED_SEED AND NOT reported_seed STREQUAL EXPECTED_SEED)
message(FATAL_ERROR "Command reported seed ${reported_seed}, expected ${EXPECTED_SEED}")
endif()

file(GLOB payloads LIST_DIRECTORIES false "${normalized_test_directory}/*.off")
file(GLOB manifests LIST_DIRECTORIES false "${normalized_test_directory}/*.off.meta")
list(LENGTH payloads payload_count)
list(LENGTH manifests manifest_count)
if(NOT payload_count EQUAL 1 OR NOT manifest_count EQUAL 1)
message(
FATAL_ERROR
"Expected one payload and one manifest, found ${payload_count} payloads and ${manifest_count} manifests")
endif()

list(GET payloads 0 payload)
list(GET manifests 0 manifest)
get_filename_component(payload_name "${payload}" NAME)
if(payload_name MATCHES ":")
message(FATAL_ERROR "Generated filename is not Windows-portable: ${payload_name}")
endif()
if(NOT manifest STREQUAL "${payload}.meta")
message(FATAL_ERROR "Manifest is not paired with its payload: ${manifest}")
endif()

file(READ "${manifest}" metadata)
foreach(
required
"cdt-plusplus-metadata-v1"
"artifact=${EXPECTED_ARTIFACT}"
"resume_supported=false"
"fresh_topology_replay_supported=false"
"transition_replay_requires_identical_start=true"
"random.seed=${reported_seed}"
"payload.size="
"payload.fnv1a64="
"placement.fnv1a64="
"topology.fnv1a64="
"cdt.version="
"build.configuration="
"dependency.cgal_version=")
if(NOT metadata MATCHES "${required}")
message(FATAL_ERROR "Manifest is missing '${required}':\n${metadata}")
endif()
endforeach()
if(EXPECTED_ARTIFACT STREQUAL "final-triangulation"
AND (NOT metadata MATCHES "transition_trace.fnv1a64="
OR NOT metadata MATCHES "transition_trace.count="))
message(FATAL_ERROR "Simulation manifest does not record its transition trace:\n${metadata}")
endif()

file(GLOB temporary_files LIST_DIRECTORIES false "${normalized_test_directory}/*.tmp")
if(temporary_files)
list(JOIN temporary_files "\n " temporary_file_list)
message(FATAL_ERROR "Persistence left temporary files behind:\n ${temporary_file_list}")
endif()
12 changes: 12 additions & 0 deletions cmake/Version.hpp.in
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,21 @@

#include <string_view>

#ifndef CDT_BUILD_CONFIGURATION
#define CDT_BUILD_CONFIGURATION "unknown"
#endif

namespace cdt
{
inline constexpr std::string_view VERSION{"@CDT_VERSION@"};
inline constexpr std::string_view BUILD_COMPILER_ID{"@CMAKE_CXX_COMPILER_ID@"};
inline constexpr std::string_view BUILD_COMPILER_VERSION{
"@CMAKE_CXX_COMPILER_VERSION@"};
inline constexpr std::string_view BUILD_CONFIGURATION{
CDT_BUILD_CONFIGURATION};
inline constexpr std::string_view BUILD_SYSTEM_NAME{"@CMAKE_SYSTEM_NAME@"};
inline constexpr std::string_view BUILD_SYSTEM_PROCESSOR{
"@CMAKE_SYSTEM_PROCESSOR@"};
} // namespace cdt

#endif // CDT_VERSION_HPP
129 changes: 116 additions & 13 deletions docs/reproducibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ subsystems:

Pass `--seed SEED` to `cdt` or `initialize` to replay the random inputs to a
run. Without that option, the command obtains operating-system entropy once
and prints the effective seed. Checkpoint and final OFF filenames also include
`seed-SEED`; simulation checkpoints include `pass-PASS`. The seed is metadata
in the filename rather than extra OFF content so files remain readable by
standard CGAL triangulation parsers.
and prints the effective seed. The validated runtime configuration retains that
effective seed rather than the pre-parse default. Checkpoint and final payload
filenames include `seed-SEED`; simulation checkpoints also include
`pass-PASS`. Filename timestamps use UTC and replace the colon separators that
are invalid in Windows filenames.

```console
./out/build/reference/src/cdt -s -n640 -t4 -a0.6 -k1.1 -l0.1 -p10 --seed 92
Expand All @@ -26,13 +27,114 @@ engine construction outside `Random.hpp`. The supported spherical CGAL point
generator receives a single seed derived from its caller-owned initialization
stream rather than using CGAL's hidden default generator.

The reproducibility guarantee is exact for PCG draws and generated
initialization points. Given the same starting manifold, the complete
Metropolis transition sequence and counters also replay exactly. A freshly
constructed triangulation is not promised to have byte-identical topology
across CGAL versions, platforms, or builds: points on the same spherical layer
are cospherical, so CGAL may choose a different valid tetrahedralization when
resolving geometric ties.
Stream consumption is sequential and defined at the subsystem boundary. The
initialization stream supplies the one seed used to construct CGAL's spherical
point generator. The generated vertices retain the existing exact spherical
placement and CGAL range-insertion path; reproducibility does not perturb their
radii or change foliation repair policy. For each Metropolis attempt, the
transition stream selects a
move type, draws the acceptance variate, and then performs the selected raw-site
selection and any candidate shuffles. A pass captures the current simplex count
before its first attempt. Failed candidate construction is an explicit
self-transition and does not retry a different site. The output metadata
records an FNV-1a fingerprint of the ordered move/outcome trace—candidate
failure, acceptance, or Metropolis-Hastings rejection—and its transition count.
The fingerprint is a compact replay diagnostic, not a cryptographic proof.

The reproducibility guarantee is exact for PCG draws and the ordered,
pre-CGAL initialization point sequence on the recorded supported toolchain.
Given an identical starting manifold, the complete Metropolis transition
sequence and counters also replay exactly. Canonical proposal ordering only
maps random draws to the same uniformly selected candidate; it does not change
the candidate set or proposal probabilities.

A freshly constructed triangulation is deliberately not promised to have
identical topology or a matching post-repair vertex set, even in separate
processes using the same toolchain. Points on each spherical layer are
cospherical, so CGAL may choose a different valid tetrahedralization when
resolving geometric ties. Foliation repair operates on that tetrahedralization
and may consequently remove different vertices. CDT++ does not perturb point
radii, replace CGAL's range insertion, or change foliation repair policy solely
to force fresh-topology replay.

The placement fingerprint describes the finite vertices that survive CGAL
construction and foliation repair. Along with the topology fingerprint and
actual counts, it makes output-state differences visible; it is not a
fingerprint of the pre-CGAL generated point sequence. Exact generation replay
is tested directly before insertion.

## Persistence contract

Each new stochastic `.off` payload has a neighboring `.off.meta` text
manifest. The manifest records:

- the root seed, PCG engine, and named stream identifiers;
- the explicit limits on fresh-topology and transition replay;
- requested and actual topology dimensions and counts;
- foliation parameters and, for simulations, the action parameters and pass
cadence;
- completed passes and the transition-trace fingerprint;
- a canonical placement fingerprint derived from sorted finite vertices and
their timeslices;
- a canonical topology fingerprint derived from sorted vertices, causal
metadata, and finite cells;
- the CDT++ version, compiler, build configuration, standard library,
operating system, architecture, C++ standard, and CGAL version;
- the payload byte count and FNV-1a corruption checksum.

The triangulation remains a CGAL-readable payload; provenance is in the sidecar
rather than prepended to the CGAL stream. Because CGAL's native triangulation
stream omits `info()` fields, CDT++ appends a versioned, canonically ordered
causal-data trailer that preserves every finite vertex timeslice and cell type.
Legacy streams without this trailer remain topology-readable. Before
publication, CDT++ serializes with round-trip floating-point precision to a
temporary file, flushes and closes it, parses the complete CGAL stream and
causal trailer, rejects any other trailing data, validates its triangulation
data structure, and requires the parsed dimension, incidence counts, causal
metadata, and canonical topology fingerprint to match the source. It similarly
writes and reparses the complete typed manifest. Payload-derived dimensions,
incidence counts, time bounds, placement fingerprint, and topology fingerprint
are derived from the serialized state rather than trusted from caller-supplied
metadata. The manifest is published first and the payload second using
same-directory atomic replacement (`rename` on POSIX and `MoveFileExW` on
Windows). If a process is interrupted between the two replacements, their
checksum mismatch is detectable rather than silently pairing a payload with
stale provenance.

Reads of a manifested payload verify the size and checksum before parsing, then
repeat the complete-input and causal-metadata checks and compare every
payload-derived manifest field with the parsed state. Evolved CDT states are
abstract causal triangulations and need not retain the Euclidean Delaunay
empty-sphere property after Pachner moves, so integrity validation requires a
valid CGAL triangulation data structure without imposing Delaunayhood. Legacy
files without a manifest remain readable, but naturally have no checksum or
provenance guarantee; legacy CGAL streams also lack the causal `info()` data
that older CDT++ versions never serialized.
Malformed manifests, truncated payloads, trailing input, invalid topology, and
manifest/payload mismatches fail with filesystem diagnostics. FNV-1a protects
against accidental truncation or corruption; it does not authenticate files
against deliberate modification.

Checkpoints are snapshots only. CDT++ does not currently expose a resume CLI,
and a checkpoint does not serialize mutable PCG engine state or enough runtime
state to continue the identical stream. The manifest explicitly records
`resume_supported=false`. The manifest also records
`fresh_topology_replay_supported=false` and
`transition_replay_requires_identical_start=true`. Starting a second CLI run
from the recorded seed and configuration replays the stochastic inputs, but it
does not override CGAL's non-unique cospherical tetrahedralization. Exact
transition replay is conditional on supplying an identical starting manifold;
it is not checkpoint resume.

Payload parsing is supported for the repository's declared build matrix and
pinned dependency set. Exact PCG prefixes replay on the same supported
toolchain; transition traces replay when the starting manifold is identical.
The manifest makes fresh-construction and cross-toolchain differences
diagnosable, but it does not promise matching fresh topology, post-repair
placement, counts, or payload bytes. CGAL may also serialize the same abstract
topology in a different handle iteration order, so equality of a persisted
state is defined by the canonical topology fingerprint and reproducibility
fields rather than raw payload byte order.

## Parallel stream policy

Expand All @@ -56,5 +158,6 @@ just benchmark-rng 10000

The diagnostic reports both durations and their ratio. It is intentionally not
a pass/fail CI test because operating-system entropy latency and runner load are
machine-dependent; replay and distribution boundaries remain correctness tests
in `just ci`.
machine-dependent; RNG-prefix, pre-CGAL point-generation, identical-start
transition replay, and distribution boundaries remain correctness tests in
`just ci`.
Loading
Loading