Skip to content

Reapply 3+1D CDT support - #138

Open
rakovpublic wants to merge 2 commits into
acgetchell:mainfrom
rakovpublic:develop
Open

Reapply 3+1D CDT support#138
rakovpublic wants to merge 2 commits into
acgetchell:mainfrom
rakovpublic:develop

Conversation

@rakovpublic

@rakovpublic rakovpublic commented Jul 30, 2026

Copy link
Copy Markdown

Summary by CodeRabbit

  • New Features

    • Added support for running 4D spherical CDT simulations with configurable couplings, volumes, thermalization, measurements, seeds, and checkpoints.
    • Added structured run output, including measurements, observables, action traces, move statistics, validation, and phase diagnostics.
    • Added seven 4D moves, detailed-balance verification, topology validation, time reversal, and canonical state tracking.
    • Added spatial-profile, geometry, volume, and phase-analysis diagnostics.
    • Added cross-platform scripts for automated CDS phase scans.
  • Documentation

    • Added guidance for validating standard-CDT candidates and interpreting finite-size phase results.
  • Tests

    • Expanded coverage for 4D simulations, checkpoints, moves, actions, observables, phase analysis, and reversibility.

@rakovpublic
rakovpublic requested a review from acgetchell as a code owner July 30, 2026 15:40
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The PR adds a complete 4D CDT stack: foliated triangulations, seven ergodic moves, action and detailed-balance calculations, Metropolis sampling, checkpoints, structured output, phase diagnostics, CLI support, scan scripts, and tests.

Changes

4D CDT implementation

Layer / File(s) Summary
4D geometry and move contracts
include/Foliated_triangulation_4.hpp, include/S4Action.hpp, include/Move_catalog_4.hpp, include/Move_tracker.hpp, include/Observables_4.hpp, tests/*4_test.cpp, tests/S4Action_test.cpp
Defines the 4D triangulation model, periodic seed, validation, observables, action, seven move descriptors, inverse mappings, and combinatorial deltas.
Move execution and detailed balance
include/Ergodic_moves_4.hpp, include/Detailed_balance_4.hpp, tests/Ergodic_moves_4_test.cpp, tests/Detailed_balance_test.cpp
Applies moves transactionally, checks inverse candidates, computes proposal and acceptance weights, and verifies detailed balance over bounded state ensembles.
Sampling, CLI, and run output
include/Metropolis_4.hpp, include/Simulation_output.hpp, src/cdt.cpp, src/CMakeLists.txt, scripts/run-cds-phase-scan.*, tests/Checkpoint_4_test.cpp, tests/Metropolis_4_test.cpp, tests/CMakeLists.txt
Adds 4D Metropolis runs, checkpoint restart, structured output, command-line validation, integration coverage, and seeded phase-scan scripts.
Phase diagnostics and validation criteria
include/Phase_analysis.hpp, tests/Phase_analysis_test.cpp, docs/4d-standard-cdt-candidate.md
Adds profile statistics, finite-size scaling, cos^3 correlation, phase verdicts, autocorrelation estimates, and documented standard-CDT validation requirements.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant Metropolis4
  participant FoliatedTriangulation4
  participant SimulationOutput
  CLI->>Metropolis4: configure and run 4D simulation
  Metropolis4->>FoliatedTriangulation4: propose and validate moves
  FoliatedTriangulation4-->>Metropolis4: updated state and measurements
  Metropolis4-->>CLI: return simulation result
  CLI->>SimulationOutput: write_run_directory
  SimulationOutput-->>CLI: write structured run files
Loading

Possibly related PRs

Suggested reviewers: acgetchell

Poem

Four-dimensional simplices align,
Moves reverse in balanced time.
Chains record each measured trace,
Profiles map the phase-space place.
Seeds restart without a scar,
CDT charts a wider star.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately identifies the main change: restoring 3+1D CDT support through the added 4D CDT implementation and integration.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 47

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/Phase_analysis_test.cpp (1)

1-105: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing coverage for estimate_autocorrelation_time and to_string.

No TEST_CASE exercises estimate_autocorrelation_time (constant series, positive/negative correlated series, short series below the 3-sample floor) or to_string (round-trip over each Verdict). Both are part of the public header surface. Add direct tests for these two functions to close the coverage gap.

As per path instructions for tests/**/*.cpp: "Comprehensive test coverage and proper test organization."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Phase_analysis_test.cpp` around lines 1 - 105, Add direct TEST_CASEs
for estimate_autocorrelation_time covering constant input, positively and
negatively correlated series, and series shorter than the three-sample minimum,
asserting the documented results. Add a to_string round-trip test that exercises
every Verdict enum value and verifies each converted string maps to the expected
value or representation. Keep these tests organized alongside the existing
phase-analysis coverage.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@include/Detailed_balance_4.hpp`:
- Around line 71-102: Add a max_states parameter to verify_detailed_balance and
enforce it while inserting newly discovered states; when the cap is reached,
stop enumeration and record an explicit error in DetailedBalanceReport4D instead
of allocating or processing further states. Preserve the existing depth limit
and traversal behavior below the cap, and use the report’s established error
mechanism.
- Around line 129-145: Update the detailed-balance verification around
boltzmann_weight, lhs, and rhs to detect non-finite or underflowed weights and
mark report.passed false with an error instead of allowing NaN or zero residuals
to pass. Prefer a log-space residual/comparison for finite values to avoid
overflow. Also document that the current lhs/rhs comparison is an algebraic
identity because both acceptance_probability calls apply the Metropolis-Hastings
ratio, or replace it with a comparison against the acceptance rule used by
Metropolis_4.

In `@include/Ergodic_moves_4.hpp`:
- Around line 33-38: In the 4D move application flow, check that `forward` is
non-zero before creating `candidate`, so rejected proposals avoid constructing a
copy. When constructing `MoveApplication`, move the named `candidate` into it
rather than copying it, while preserving the existing applicability validation
and reverse-move handling.

In `@include/Foliated_triangulation_4.hpp`:
- Around line 581-605: Update apply_move so each successfully applied move
synchronizes m_spatial_profile, m_vertices, and m_simplices with the resulting
triangulation, using the move descriptor’s per-slice deltas and existing
complex-update mechanisms. Ensure validation rollback restores these fields
alongside the existing state, and keep profile-derived observables and
canonical_hash() consistent with the post-move state.
- Around line 760-770: Update canonical_hash() to include m_three_three_forward
in the serialized state identity, alongside the existing m_timeslices and
m_periodic flags. Preserve the current counts and spatial-profile serialization
so states differing only in this direction flag produce distinct keys.
- Around line 351-371: Clamp timeslices to a minimum of 2 in the
FoliatedTriangulation4 counts constructor before storing or using it, matching
periodic_seed behavior. Ensure m_timeslices, profile initialization, and
slice_euler_characteristics() all use the clamped value, including calls through
from_counts_for_validation.
- Around line 425-447: Update spatial_topology() and
slice_euler_characteristics() so they report validated topology rather than
unconditional S3 declarations. Derive each slice’s Euler characteristic from its
spacelike facets, and add connectivity validation over Simplex4D::neighbors in
validate() so non-S3 slices are rejected; do not rely solely on
m_closed_s3_slices or the literal accessor value.
- Around line 88-93: Replace the linear search in vertex_time with an
unordered_map lookup using a new m_vertex_times cache keyed by VertexId. Update
this cache whenever vertices change, specifically in add_vertex and
time_reversed, while preserving -1 for missing vertices and keeping cached times
synchronized with m_vertices.
- Around line 726-758: Update FoliatedTriangulation4::time_reversed so
m_spatial_profile uses the same cyclic time mapping as vertex.time: preserve the
profile entry for slice 0 and shift each other slice from i to (m_timeslices -
i) % m_timeslices instead of applying std::reverse. Also invert
m_three_three_forward in the returned triangulation so candidate_multiplicity
observes the reversed proposal direction.

In `@include/Metropolis_4.hpp`:
- Line 69: Replace the raw pcg64 member m_rng in the Metropolis4 sampler with
cdt::Random, and initialize it from a dedicated random_streams::transitions
stream derived using chain_id. Update the 4D construction path and
Metropolis4Config seed handling so the full stream identity is preserved rather
than passing only root_random.seed().value(), ensuring different chain_id values
produce distinct replayable streams.
- Around line 71-75: Update propose_move to derive the distribution’s upper
bound from move_tracker::NUMBER_OF_4D_MOVES instead of the hardcoded 6,
preserving the existing inclusive range expected by move_tracker::as_move_4d.
- Around line 140-185: Update Metropolis4::run around the main step loop to
honor Metropolis4Config::checkpoint_interval by writing a checkpoint at each
configured interval, after measurements are processed. Reuse the existing
checkpoint-saving mechanism and ensure checkpointing is disabled when the
interval is non-positive; preserve the current final checkpoint behavior.
- Around line 204-225: Update load_checkpoint to validate the state.txt stream
after each extraction group, including the RNG state line, and throw an
appropriate exception on any missing or malformed data instead of constructing a
fallback triangulation. Add the required stdexcept include and ensure
set_rng_state is only called after successfully reading a non-failed RNG state.
- Around line 187-225: Update save_checkpoint and load_checkpoint so checkpoints
preserve and restore the complete FoliatedTriangulation4 topology, including
vertices and simplices, rather than reconstructing only from timeslices, counts,
and profile. Ensure load_checkpoint returns a fully populated complex with
consistent combinatorial data for move and candidate APIs, while preserving step
and RNG state restoration.
- Around line 187-202: Update save_checkpoint to include the required standard
exception header and validate the ofstream both after opening and after
completing all writes; if either check fails, throw an appropriate exception so
callers do not report an unsuccessful checkpoint as written. Prefer writing to a
temporary file and atomically renaming it to state.txt after successful
completion to avoid torn checkpoints.

In `@include/Move_catalog_4.hpp`:
- Around line 182-193: The proposal_inventory_from_counts function incorrectly
maps aggregate N3, N1, and N2 counts to disjoint simplex classes. Extend
FoliatedTriangulation4 counting, including recompute_counts_from_complex, to
retain class-resolved spacelike/timelike tetrahedra, timelike edges, and mixed
triangles, then pass those values into proposal_inventory_from_counts and
preserve the existing aggregate mappings for the remaining fields.

In `@include/Phase_analysis.hpp`:
- Around line 72-78: Implement collapse_error in analyze_finite_size_scaling
instead of leaving FiniteSizeScalingReport::collapse_error at its default zero
value. Compute an actual residual using the regression results from
fit_power_law_exponent, such as the sum of squared log-log residuals, and assign
it to the report’s collapse_error field; otherwise remove the unused field and
its exposed report value.
- Around line 416-421: Document the assignments to
diagnostics.held_out_likelihood, diagnostics.aic, and diagnostics.bic as
heuristic scores derived from the cos3 profile-shape correlation, explicitly
stating they are not formal likelihood, AIC, or BIC statistics; keep the
existing calculations and field names unchanged.
- Around line 292-346: Extract the duplicated per-profile classification from
diagnose_c_ds_finite_size and diagnose into a shared helper such as
classify_profile_shape(Profile const&, long double), preserving the
thermalization, collapse, and alternating-slice verdict behavior. Define named
constexpr thresholds for the collapse peak, alternating ratio, and cos3
correlation limits, then update both callers to reuse the helper and constants.
- Around line 103-146: Validate profile dimensions at the start of mean and
covariance before any indexed access: ensure every profile matches
profiles.front().size() in mean, and every profile matches profile_mean.size()
in covariance. Fail clearly via the project’s established throw or assertion
mechanism when a mismatch is detected, while preserving the existing empty-input
behavior.

In `@include/Simulation_output.hpp`:
- Around line 88-96: The output currently persists only the final profile via
spatial_volume_profile(); update the result-writing block to persist every
per-measurement spatial profile from result.measurements in a
spatial_volume_profiles.csv file using step,timeslice,N3 rows. Preserve the
existing final-profile output and use each measurement’s profile so external
analysis can reconstruct covariance and autocorrelation.
- Around line 111-120: The move_statistics.csv writer currently outputs the
numeric index instead of a stable move name. In the loop using
result.move_stats, include Move_catalog_4.hpp descriptors and convert each index
with as_move_4d(index), then write the corresponding descriptor name in the
first column while preserving the existing statistics columns.
- Around line 122-130: Introduce a shared stream-precision helper using
<iomanip>, <limits>, and <ostream>, and apply it immediately after opening every
numeric output stream for action_trace.csv, measurements.jsonl, covariance.csv,
effective_action.csv, manifest.json, and summary.json so long-double values
retain full precision. In the action_trace writer, validate that
result.volume_trace has at least as many entries as result.action_trace before
indexing it, and preserve safe behavior if the sizes differ.
- Around line 30-41: Update write_counts_csv_header and write_counts_csv_row to
accept std::ostream& instead of std::ofstream&, and include <ostream> in the
header. Preserve their existing CSV output and formatting so callers can provide
files or in-memory streams.
- Around line 170-200: Update the summary-generation block to pass
measured_profiles to phase::diagnose using std::move, adding the required
<utility> include. Extend summary.json to emit diagnostics.autocorrelation_time,
held_out_likelihood, aic, and bic, and include config.chain_id so results can be
attributed to their chain while preserving the existing verdict output.
- Around line 51-73: Update the manifest and summary JSON writers in
Simulation_output.hpp by adding a small JSON string-escaping helper that handles
quotes, backslashes, and control characters such as newlines. Apply it to every
interpolated string field, including the manifest values and config.chain_id,
and reuse the same helper at every string-writing site in summary.json while
leaving numeric fields unchanged.
- Around line 43-49: Update write_run_directory to validate manifest.run_id
before constructing run_dir, requiring a non-empty relative single path
component and rejecting traversal or embedded separators; throw an appropriate
exception and include the needed <stdexcept> header. Also check every
std::ofstream opened within write_run_directory and fail immediately when
opening or writing cannot proceed, so callers do not report successful output
after an I/O error; apply the same success-checking behavior to
Metropolis_4.hpp::save_checkpoint.

In `@scripts/run-cds-phase-scan.ps1`:
- Around line 37-59: Update the scan loops around the `$cdt` invocation to stop
immediately when the native command returns a non-zero `$LASTEXITCODE`,
preventing partial results from being reported. Replace the current `$BaseSeed +
$chain` calculation with a deterministic seed derived from the full parameter
point and chain, including the coupling values and target volume, so every run
has a unique seed. Apply the same parameter-point seed fix in the corresponding
shell scan script.
- Around line 32-36: Validate each coupling point in the PowerShell loop before
assigning or using its fields: after splitting $point, require at least three
components and reject malformed entries with a clear error instead of continuing
with null values. Apply the equivalent validation to the shell script’s read of
COUPLING_POINTS so missing KAPPA4 or DELTA values terminate the run rather than
being passed as empty arguments.

In `@scripts/run-cds-phase-scan.sh`:
- Around line 30-35: Disable pathname expansion in scripts/run-cds-phase-scan.sh
by adding set -f alongside the existing set -euo pipefail, preserving the
intentional unquoted word splitting in the COUPLING_POINTS and TARGET_VOLUMES
loops without allowing wildcard characters to expand into filenames.
- Around line 19-28: Update the CDT executable discovery block around CDT_EXE to
check the four candidate paths through a loop, selecting the first executable
found; if none is executable, fail immediately with a clear message matching the
PowerShell script’s “Could not find cdt executable under …” behavior before the
later scan command runs.

In `@src/cdt.cpp`:
- Around line 151-157: Validate both command-line identifiers after parsing:
reject empty, absolute, or path-separator-containing values for run_id and
chain_id before constructing output paths or manifests. Apply the validation to
the option-handling flow around the run_id and chain_id variables, preserving
the existing defaults and reporting an invalid argument clearly.
- Around line 337-352: Extend the 4D result-reporting block after the
elapsed-time/output messages to print statistics from result.move_stats and
result.measurements: total attempted, accepted, and invalid moves, the final N4,
and the measurement count. Keep the existing output-location and Standard CDT
candidate reporting unchanged, and ensure these values are shown even when
write_files is false.
- Around line 293-318: Make the 4D run control use sweep-equivalent work rather
than treating checked_passes as raw Metropolis4::run steps: scale the steps
argument by a sweep size derived from the current triangulation volume, or
consistently rename the 4D option to move attempts and update scan-script
defaults to a thermalizing value. Preserve the existing 3D pass semantics and
ensure the 4D scan configuration performs enough work relative to target volume
and thermalization.
- Around line 234-291: Move the 4D parameter validation from main into a new
runtime_config::make_4d_simulation function, reusing the shared configuration
validation for simplices, timeslices, passes, and checkpoint and throwing
invalid_argument for invalid command-line values; have main call this factory
and remain a thin driver. Ensure the returned configuration also validates the
remaining 4D parameters currently checked inline, and explicitly handle the
parsed --threads option by honoring it or reporting that it does not apply to
4D.
- Around line 60-67: Update the usage synopsis in the command-line help near the
existing options such as --volume-epsilon to include --thermalization,
--measurement-interval, --chain-id, --run-id, and --output-dir, matching the
option names and argument placeholders defined in the argument parsing section.

In `@src/CMakeLists.txt`:
- Around line 73-83: Add a follow-up CTest named cdt-d4-artifacts after cdt-d4
that uses ${CMAKE_COMMAND} -E cat to verify manifest.json, summary.json, and
checkpoint/state.txt exist, and set LABELS "integration;4d" with DEPENDS cdt-d4.
Add a cdt-d4-cleanup fixture with FIXTURES_SETUP to remove the prior output
directory before cdt-d4 runs, preventing stale artifacts from satisfying the
assertion.

In `@tests/Checkpoint_4_test.cpp`:
- Around line 9-35: Expand the checkpoint tests around
Metropolis4::load_checkpoint to assert errors for a missing state.txt, truncated
or malformed contents, and a state.txt without an rng line; these cases must not
silently return a default triangulation or preserve the unchanged RNG.
Strengthen the happy-path test by comparing resumed rng_state() and the resumed
action_trace and volume_trace against the corresponding tails of the
uninterrupted direct_result after combining partial_result with resumed_result,
while retaining the canonical_hash and step checks.
- Around line 22-25: Update the checkpoint test around partial.save_checkpoint
to use a unique temporary directory per test execution instead of the fixed
"cdtpp-checkpoint-test" path. Encapsulate directory cleanup in an RAII guard so
remove_all runs both on normal completion and when an assertion aborts the test,
including removing any existing setup directory safely.

In `@tests/Detailed_balance_test.cpp`:
- Around line 44-51: The test case should cover verifier failures in addition to
the existing success path. Extend the detailed-balance tests around
verify_detailed_balance to use a state with a move lacking a reverse transition
and assert report.errors is non-empty and report.passed is false; also verify
THREE_THREE is self-inverse, including its direction flag, and check
report.edges contains the expected move values rather than only being non-empty.
- Around line 33-41: Update the test around acceptance probability to call the
production acceptance_probability function from Detailed_balance_4.hpp instead
of reimplementing its formula locally. Validate finiteness directly on the
resulting long double value without narrowing it to double, while preserving the
existing range checks.
- Around line 21-24: Remove the tautological candidate-count assertions around
the moves::apply results, since they compare values guaranteed to match. Replace
them with an assertion that verifies the forward and reverse proposal weights
required for detailed balance, using the existing weight calculation used by the
test or moves API. Preserve the triangulation canonical_hash round-trip
assertion.

In `@tests/Foliated_triangulation_4_test.cpp`:
- Around line 30-35: Expand the test case around FoliatedTriangulation4 with
doctest SUBCASE blocks covering distinct-state canonical_hash changes after
apply_move, counts matching the move descriptor delta, applying THREE_THREE
twice returning the original state, time_reversed() matching reversed vertex
times, and from_counts_for_validation producing the expected validate() error
text. Keep the existing copy-hash assertion and use the relevant apply_move,
time_reversed, counts, and validate APIs.
- Around line 46-49: Update the inventory assertions in the test around
from_counts and seeded to compare each proposal_inventory() field against
concrete expected values rather than against the corresponding value from the
other object. Preserve coverage for spatial_tetrahedra and mixed_triangles so
regressions in proposal_inventory_from_counts are detected.

In `@tests/Metropolis_4_test.cpp`:
- Around line 23-31: Update the determinism assertions in the test’s
action_trace loop to use exact CHECK_EQ comparisons for the long-double values
instead of doctest::Approx. Extend the determinism coverage with a separate run
using a different Metropolis4Config::seed and assert that its action trace
differs from the identical-seed result, while keeping the existing same-seed
equality checks intact.
- Around line 34-52: Update the test case “Metropolis4 records invalid proposals
and accepted moves” to accumulate attempted and accepted using Int_precision,
and add assertions that aggregate and verify stat.invalid so the test covers
invalid proposals as named. Replace the non-informative CHECK_GE(accepted, 0)
with meaningful expected-count assertions, preferably exact counts for seed 7
while preserving the existing attempted-count and triangulation-validity checks.

In `@tests/Phase_analysis_test.cpp`:
- Around line 56-92: Extract the duplicated synthetic profile construction from
both TEST_CASEs into a shared helper named build_synthetic_c_ds_profiles,
preserving the existing volumes, width calculation, profile generation, and
amplitude scaling. Replace each local profiles initialization loop with a call
to this helper so both tests use the same recipe.

---

Outside diff comments:
In `@tests/Phase_analysis_test.cpp`:
- Around line 1-105: Add direct TEST_CASEs for estimate_autocorrelation_time
covering constant input, positively and negatively correlated series, and series
shorter than the three-sample minimum, asserting the documented results. Add a
to_string round-trip test that exercises every Verdict enum value and verifies
each converted string maps to the expected value or representation. Keep these
tests organized alongside the existing phase-analysis coverage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 67087d07-bad9-413a-b2ec-c779fcbdb5b5

📥 Commits

Reviewing files that changed from the base of the PR and between 90a3ea6 and ab27411.

📒 Files selected for processing (28)
  • docs/4d-standard-cdt-candidate.md
  • include/Detailed_balance_4.hpp
  • include/Ergodic_moves_4.hpp
  • include/Foliated_triangulation_4.hpp
  • include/Metropolis_4.hpp
  • include/Move_catalog_4.hpp
  • include/Move_tracker.hpp
  • include/Observables_4.hpp
  • include/Phase_analysis.hpp
  • include/S4Action.hpp
  • include/Simulation_output.hpp
  • scripts/run-cds-phase-scan.ps1
  • scripts/run-cds-phase-scan.sh
  • src/CMakeLists.txt
  • src/cdt.cpp
  • tests/CMakeLists.txt
  • tests/Checkpoint_4_test.cpp
  • tests/Detailed_balance_test.cpp
  • tests/Ergodic_moves_4_test.cpp
  • tests/Foliated_triangulation_4_test.cpp
  • tests/Geometry_4_test.cpp
  • tests/Manifold_4_test.cpp
  • tests/Metropolis_4_test.cpp
  • tests/Move_tracker_test.cpp
  • tests/Observables_4_test.cpp
  • tests/Phase_analysis_test.cpp
  • tests/S4Action_test.cpp
  • tests/Time_reversal_4_test.cpp

Comment on lines +71 to +102
[[nodiscard]] inline auto verify_detailed_balance(
FoliatedTriangulation4 const& seed, S4Couplings const& couplings,
int const max_depth, long double const tolerance = 1.0e-10L)
-> DetailedBalanceReport4D
{
DetailedBalanceReport4D report;
std::map<std::string, FoliatedTriangulation4> states;
std::map<std::string, int> depths;
std::queue<std::pair<FoliatedTriangulation4, int>> frontier;

states.emplace(seed.canonical_hash(), seed);
depths.emplace(seed.canonical_hash(), 0);
frontier.emplace(seed, 0);

while (!frontier.empty())
{
auto [state, depth] = frontier.front();
frontier.pop();
if (depth >= max_depth) { continue; }
for (auto const descriptor : all_move_descriptors_4d())
{
auto moved = moves::apply(state, descriptor.move);
if (!moved) { continue; }
auto const hash = moved->triangulation.canonical_hash();
if (!states.contains(hash))
{
states.emplace(hash, moved->triangulation);
depths.emplace(hash, depth + 1);
frontier.emplace(moved->triangulation, depth + 1);
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Bound the enumerated state count, not only the depth.

The frontier branches by seven moves per state, and states retains a full FoliatedTriangulation4 per visited state. moves::apply copies the triangulation once more per candidate. Growth is therefore about 7^max_depth with full copies. The tests pass max_depth = 1, but the public signature accepts any depth, and there is no cap on the number of states.

Add a max_states parameter. When the enumeration reaches the cap, record an explicit error in the report instead of continuing to allocate.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@include/Detailed_balance_4.hpp` around lines 71 - 102, Add a max_states
parameter to verify_detailed_balance and enforce it while inserting newly
discovered states; when the cap is reached, stop enumeration and record an
explicit error in DetailedBalanceReport4D instead of allocating or processing
further states. Preserve the existing depth limit and traversal behavior below
the cap, and use the report’s established error mechanism.

Comment on lines +129 to +145
auto const lhs = boltzmann_weight(from_state, couplings) *
proposal_probability(from_state, descriptor.move) *
acceptance_probability(from_state, to_state,
descriptor.move, couplings);
auto const rhs = boltzmann_weight(to_state, couplings) *
proposal_probability(to_state, descriptor.inverse) *
acceptance_probability(to_state, from_state,
descriptor.inverse, couplings);
auto const residual = std::abs(lhs - rhs);
auto const scale = std::max({std::abs(lhs), std::abs(rhs), 1.0L});
report.edges.push_back(DetailedBalanceEdge4D{
from_hash, to_hash, descriptor.move, lhs, rhs, residual});
if (residual / scale > tolerance)
{
report.passed = false;
report.errors.emplace_back("Detailed-balance residual exceeds tolerance.");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

A non-finite weight makes the verification pass silently.

boltzmann_weight returns std::exp(-S4_bulk_action(...)) with no bound. The exponent scales with N0 and with the quadratic volume term, so it overflows long double for moderate system sizes. If both weights overflow, lhs - rhs is inf - inf, which is NaN. NaN > tolerance evaluates to false, so report.passed stays true. If both weights underflow to zero, the residual is exactly zero and the result also passes. In both cases the report claims success without testing anything.

Add a finiteness guard, and prefer a log-space residual for the comparison.

🛡️ Proposed finiteness guard
         auto const residual = std::abs(lhs - rhs);
         auto const scale = std::max({std::abs(lhs), std::abs(rhs), 1.0L});
+        if (!std::isfinite(lhs) || !std::isfinite(rhs))
+        {
+          report.passed = false;
+          report.errors.emplace_back(
+              "Non-finite detailed-balance weight; use log-space weights.");
+          continue;
+        }
         report.edges.push_back(DetailedBalanceEdge4D{

Note also that lhs and rhs both reduce to min(pi_x q_xy, pi_y q_yx), because acceptance_probability applies the Metropolis-Hastings ratio on both sides. The residual is therefore an algebraic identity, and only the hash check and the reverse-transition check carry information. State this limitation in the file documentation, or compare against the acceptance rule that the sampler in include/Metropolis_4.hpp actually applies.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@include/Detailed_balance_4.hpp` around lines 129 - 145, Update the
detailed-balance verification around boltzmann_weight, lhs, and rhs to detect
non-finite or underflowed weights and mark report.passed false with an error
instead of allowing NaN or zero residuals to pass. Prefer a log-space
residual/comparison for finite values to avoid overflow. Also document that the
current lhs/rhs comparison is an algebraic identity because both
acceptance_probability calls apply the Metropolis-Hastings ratio, or replace it
with a comparison against the acceptance rule used by Metropolis_4.

Comment on lines +88 to +93
[[nodiscard]] auto vertex_time(VertexId const id) const -> Int_precision
{
auto const it = std::ranges::find_if(
m_vertices, [&](auto const& vertex) { return vertex.id == id; });
return it == m_vertices.end() ? -1 : it->time;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Replace the linear vertex lookup with a hashed index.

vertex_time scans m_vertices on every call. classify_simplex calls it up to ten times per simplex, and validate() classifies every simplex. apply_move calls validate() on every accepted or rejected move, so each move costs O(N_simplices × N_vertices). This dominates the Metropolis hot path.

Cache a std::unordered_map<VertexId, Int_precision> alongside m_vertices and rebuild it when vertices change.

⚡ Proposed lookup cache
     VertexContainer  m_vertices;
     SimplexContainer m_simplices;
+    std::unordered_map<VertexId, Int_precision> m_vertex_times;
     [[nodiscard]] auto vertex_time(VertexId const id) const -> Int_precision
     {
-      auto const it = std::ranges::find_if(
-          m_vertices, [&](auto const& vertex) { return vertex.id == id; });
-      return it == m_vertices.end() ? -1 : it->time;
+      auto const it = m_vertex_times.find(id);
+      return it == m_vertex_times.end() ? -1 : it->second;
     }

Update m_vertex_times inside add_vertex and inside time_reversed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@include/Foliated_triangulation_4.hpp` around lines 88 - 93, Replace the
linear search in vertex_time with an unordered_map lookup using a new
m_vertex_times cache keyed by VertexId. Update this cache whenever vertices
change, specifically in add_vertex and time_reversed, while preserving -1 for
missing vertices and keeping cached times synchronized with m_vertices.

Source: Path instructions

Comment on lines +351 to +371
FoliatedTriangulation4(Int_precision const timeslices, S4Counts counts,
Profile profile)
: m_timeslices{timeslices}
, m_periodic{true}
, m_counts{counts}
, m_proposal_inventory{proposal_inventory_from_counts(m_counts)}
, m_spatial_profile{std::move(profile)}
, m_closed_s3_slices{true}
{
if (m_spatial_profile.empty())
{
m_spatial_profile.assign(static_cast<std::size_t>(m_timeslices), 0);
}
}

[[nodiscard]] static auto from_counts_for_validation(
Int_precision const timeslices, S4Counts counts, Profile profile)
-> FoliatedTriangulation4
{
return FoliatedTriangulation4{timeslices, counts, std::move(profile)};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Clamp timeslices in the counts constructor.

periodic_seed clamps timeslices to at least 2, but this constructor stores the raw value. If a caller passes a negative value, static_cast<std::size_t>(m_timeslices) at line 362 becomes a huge size, and assign throws std::length_error or std::bad_alloc. slice_euler_characteristics() at line 443 has the same exposure. from_counts_for_validation is public, so this path is reachable from callers and tests.

🛡️ Proposed clamp
     FoliatedTriangulation4(Int_precision const timeslices, S4Counts counts,
                            Profile profile)
-        : m_timeslices{timeslices}
+        : m_timeslices{std::max<Int_precision>(2, timeslices)}
         , m_periodic{true}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
FoliatedTriangulation4(Int_precision const timeslices, S4Counts counts,
Profile profile)
: m_timeslices{timeslices}
, m_periodic{true}
, m_counts{counts}
, m_proposal_inventory{proposal_inventory_from_counts(m_counts)}
, m_spatial_profile{std::move(profile)}
, m_closed_s3_slices{true}
{
if (m_spatial_profile.empty())
{
m_spatial_profile.assign(static_cast<std::size_t>(m_timeslices), 0);
}
}
[[nodiscard]] static auto from_counts_for_validation(
Int_precision const timeslices, S4Counts counts, Profile profile)
-> FoliatedTriangulation4
{
return FoliatedTriangulation4{timeslices, counts, std::move(profile)};
}
FoliatedTriangulation4(Int_precision const timeslices, S4Counts counts,
Profile profile)
: m_timeslices{std::max<Int_precision>(2, timeslices)}
, m_periodic{true}
, m_counts{counts}
, m_proposal_inventory{proposal_inventory_from_counts(m_counts)}
, m_spatial_profile{std::move(profile)}
, m_closed_s3_slices{true}
{
if (m_spatial_profile.empty())
{
m_spatial_profile.assign(static_cast<std::size_t>(m_timeslices), 0);
}
}
[[nodiscard]] static auto from_counts_for_validation(
Int_precision const timeslices, S4Counts counts, Profile profile)
-> FoliatedTriangulation4
{
return FoliatedTriangulation4{timeslices, counts, std::move(profile)};
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@include/Foliated_triangulation_4.hpp` around lines 351 - 371, Clamp
timeslices to a minimum of 2 in the FoliatedTriangulation4 counts constructor
before storing or using it, matching periodic_seed behavior. Ensure
m_timeslices, profile initialization, and slice_euler_characteristics() all use
the clamped value, including calls through from_counts_for_validation.

Comment on lines +425 to +447
[[nodiscard]] auto has_closed_s3_slices() const -> bool
{
return m_closed_s3_slices;
}

[[nodiscard]] auto spatial_topology() const -> std::string_view
{
return "S3";
}

[[nodiscard]] auto spacetime_topology() const -> std::string_view
{
return m_periodic ? "S3xS1" : "S3xI";
}

[[nodiscard]] auto slice_euler_characteristics() const
-> std::vector<Int_precision>
{
return std::vector<Int_precision>(
static_cast<std::size_t>(m_timeslices),
m_closed_s3_slices ? static_cast<Int_precision>(0)
: static_cast<Int_precision>(1));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP -C3 'm_closed_s3_slices|m_periodic' --type=cpp --type=hpp -g '!**/build/**' || \
rg -nP -C3 'm_closed_s3_slices|m_periodic' -g '*.hpp' -g '*.cpp'

Repository: acgetchell/CDT-plusplus

Length of output: 193


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files similar to target:"
fd -a 'Foliated_triangulation_4\.hpp$|Foliated_triangulation_4\.cpp$' . || true

echo
echo "Occurrences with line numbers:"
rg -n -C 3 'm_closed_s3_slices|m_periodic|spatial_topology|spacetime_topology|slice_euler_characteristics|validate' \
  -g '*.hpp' -g '*.cpp' -g '*.cc' -g '*.cxx' || true

echo
echo "Git diff summary for context:"
git diff --stat || true
git diff -- include/Foliated_triangulation_4.hpp | sed -n '1,240p' || true

Repository: acgetchell/CDT-plusplus

Length of output: 274


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Target file size:"
wc -l include/Foliated_triangulation_4.hpp || true

echo
echo "Topological member/accessor occurrences (grep, all files):"
grep -RIn -C 3 'm_closed_s3_slices\|m_periodic\|spatial_topology\|spacetime_topology\|slice_euler_characteristics\|validate' . --include='*.hpp' --include='*.cpp' --include='*.cc' --include='*.cxx' || true

echo
echo "Relevant validate/accessor sections:"
sed -n '400,460p' include/Foliated_triangulation_4.hpp || true
sed -n '590,735p' include/Foliated_triangulation_4.hpp || true

Repository: acgetchell/CDT-plusplus

Length of output: 33146


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "FoliatedTriangulation4 outline:"
ast-grep outline include/Foliated_triangulation_4.hpp --view expanded || true

echo
echo "Lines around class docs/topology declarations:"
sed -n '1,95p' include/Foliated_triangulation_4.hpp

echo
echo "Seed and boundary component sections:"
sed -n '305,399p' include/Foliated_triangulation_4.hpp

Repository: acgetchell/CDT-plusplus

Length of output: 7209


Treat the topology accessors as declared values, not S3 guarantees.

spatial_topology() returns a literal "S3" and slice_euler_characteristics() derives its report from m_closed_s3_slices, which is set to true in periodic_seed and in every constructor. validate() checks those same declared values, so it cannot reject an input whose slices are not S3. If the API documents S3 topology, derive the slice Euler characteristic from the spacelike facets and add a connectivity check over Simplex4D::neighbors.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@include/Foliated_triangulation_4.hpp` around lines 425 - 447, Update
spatial_topology() and slice_euler_characteristics() so they report validated
topology rather than unconditional S3 declarations. Derive each slice’s Euler
characteristic from its spacelike facets, and add connectivity validation over
Simplex4D::neighbors in validate() so non-S3 slices are rejected; do not rely
solely on m_closed_s3_slices or the literal accessor value.

Source: Path instructions

Comment on lines +44 to +51
TEST_CASE("4D detailed balance holds on a small enumerable ensemble")
{
auto triangulation = FoliatedTriangulation4::periodic_seed(3);
S4Couplings couplings{1.0L, 0.2L, 0.1L, 36, 0.001L};
auto report = verify_detailed_balance(triangulation, couplings, 1);
CHECK(report.passed);
CHECK_FALSE(report.edges.empty());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a failure case for the verifier.

This case asserts only the success path. The residual inside verify_detailed_balance is an algebraic identity, so a pass adds little confidence. Add cases that prove the verifier can report a problem:

  • Construct a state whose move lacks a reverse transition, and assert that report.errors is not empty and report.passed is false.
  • Assert THREE_THREE self-inverse behavior, which exercises the direction flag in FoliatedTriangulation4.
  • Assert that report.edges contains the expected move values, not only that the list is non-empty.

Do you want me to draft these cases?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Detailed_balance_test.cpp` around lines 44 - 51, The test case should
cover verifier failures in addition to the existing success path. Extend the
detailed-balance tests around verify_detailed_balance to use a state with a move
lacking a reverse transition and assert report.errors is non-empty and
report.passed is false; also verify THREE_THREE is self-inverse, including its
direction flag, and check report.edges contains the expected move values rather
than only being non-empty.

Source: Path instructions

Comment on lines +30 to +35
TEST_CASE("Abstract 4D canonical hash is stable for copies")
{
auto triangulation = FoliatedTriangulation4::periodic_seed(3);
auto copy = triangulation;
CHECK_EQ(copy.canonical_hash(), triangulation.canonical_hash());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Extend the hash and mutation coverage.

The hash test proves only that a copy hashes identically. It does not prove that distinct states hash differently, which is the property include/Detailed_balance_4.hpp relies on for state identity.

No test in this file covers apply_move, time_reversed, or a failing validate() path. Add these cases:

  • Apply a move, then assert that canonical_hash() changes and that counts() matches the descriptor delta.
  • Apply THREE_THREE twice and assert the returned state, which exercises the direction flag.
  • Call time_reversed() and assert that the profile aligns with the reversed vertex times.
  • Build an invalid state through from_counts_for_validation and assert the specific error text from validate().

Group the related assertions with doctest SUBCASE blocks so a failure identifies the property that broke.

Do you want me to draft these test cases?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Foliated_triangulation_4_test.cpp` around lines 30 - 35, Expand the
test case around FoliatedTriangulation4 with doctest SUBCASE blocks covering
distinct-state canonical_hash changes after apply_move, counts matching the move
descriptor delta, applying THREE_THREE twice returning the original state,
time_reversed() matching reversed vertex times, and from_counts_for_validation
producing the expected validate() error text. Keep the existing copy-hash
assertion and use the relevant apply_move, time_reversed, counts, and validate
APIs.

Source: Path instructions

Comment on lines +46 to +49
CHECK_EQ(from_counts.proposal_inventory().spatial_tetrahedra,
seeded.proposal_inventory().spatial_tetrahedra);
CHECK_EQ(from_counts.proposal_inventory().mixed_triangles,
seeded.proposal_inventory().mixed_triangles);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

These inventory assertions cannot fail.

from_counts receives seeded.counts(), and both objects derive proposal_inventory() from proposal_inventory_from_counts(m_counts). The two comparisons therefore restate the constructor and hold for any implementation of proposal_inventory_from_counts.

Assert concrete expected values instead, so the test detects a regression in the inventory mapping.

💚 Proposed assertion change
-  CHECK_EQ(from_counts.proposal_inventory().spatial_tetrahedra,
-           seeded.proposal_inventory().spatial_tetrahedra);
-  CHECK_EQ(from_counts.proposal_inventory().mixed_triangles,
-           seeded.proposal_inventory().mixed_triangles);
+  CHECK_EQ(from_counts.proposal_inventory().spatial_tetrahedra,
+           seeded.counts().N3);
+  CHECK_EQ(from_counts.proposal_inventory().mixed_triangles,
+           seeded.counts().N2);
+  CHECK_GT(from_counts.proposal_inventory().vertices, 0);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
CHECK_EQ(from_counts.proposal_inventory().spatial_tetrahedra,
seeded.proposal_inventory().spatial_tetrahedra);
CHECK_EQ(from_counts.proposal_inventory().mixed_triangles,
seeded.proposal_inventory().mixed_triangles);
CHECK_EQ(from_counts.proposal_inventory().spatial_tetrahedra,
seeded.counts().N3);
CHECK_EQ(from_counts.proposal_inventory().mixed_triangles,
seeded.counts().N2);
CHECK_GT(from_counts.proposal_inventory().vertices, 0);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Foliated_triangulation_4_test.cpp` around lines 46 - 49, Update the
inventory assertions in the test around from_counts and seeded to compare each
proposal_inventory() field against concrete expected values rather than against
the corresponding value from the other object. Preserve coverage for
spatial_tetrahedra and mixed_triangles so regressions in
proposal_inventory_from_counts are detected.

Source: Path instructions

Comment on lines +23 to +31
CHECK_EQ(result_a.triangulation.canonical_hash(),
result_b.triangulation.canonical_hash());
REQUIRE_EQ(result_a.action_trace.size(), result_b.action_trace.size());
for (std::size_t index = 0; index < result_a.action_trace.size(); ++index)
{
CHECK(result_a.action_trace[index] ==
doctest::Approx(result_b.action_trace[index]));
}
CHECK_EQ(result_a.measurements.size(), 5);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Assert exact equality for the determinism test, and add a different-seed case.

Two points:

  1. Lines 28-29 compare long double action values with doctest::Approx, which holds a double. The property under test is bit-level reproducibility from an identical seed, so the comparison should be exact. Approx hides a real divergence in the low-order digits. Use CHECK_EQ on the values.
  2. No test shows that a different seed produces a different trace. A sampler that ignored Metropolis4Config::seed entirely would pass this test. That matters here, because Metropolis4 builds pcg64 from m_config.seed alone and never uses chain_id, so two chains that differ only by --chain-id produce identical output.
💚 Proposed change
   for (std::size_t index = 0; index < result_a.action_trace.size(); ++index)
   {
-    CHECK(result_a.action_trace[index] ==
-          doctest::Approx(result_b.action_trace[index]));
+    CHECK_EQ(result_a.action_trace[index], result_b.action_trace[index]);
   }
   CHECK_EQ(result_a.measurements.size(), 5);
 }
+
+TEST_CASE("Metropolis4 different seeds produce different traces")
+{
+  Metropolis4Config config;
+  config.couplings = S4Couplings{1.0L, 0.2L, 0.1L, 48, 0.001L};
+
+  config.seed = 1234;
+  Metropolis4 run_a(config);
+  auto const result_a = run_a.run(FoliatedTriangulation4::periodic_seed(4), 12);
+
+  config.seed = 5678;
+  Metropolis4 run_b(config);
+  auto const result_b = run_b.run(FoliatedTriangulation4::periodic_seed(4), 12);
+
+  CHECK_NE(result_a.action_trace, result_b.action_trace);
+}

As per path instructions, "Ensure ... Comprehensive test coverage and proper test organization."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
CHECK_EQ(result_a.triangulation.canonical_hash(),
result_b.triangulation.canonical_hash());
REQUIRE_EQ(result_a.action_trace.size(), result_b.action_trace.size());
for (std::size_t index = 0; index < result_a.action_trace.size(); ++index)
{
CHECK(result_a.action_trace[index] ==
doctest::Approx(result_b.action_trace[index]));
}
CHECK_EQ(result_a.measurements.size(), 5);
CHECK_EQ(result_a.triangulation.canonical_hash(),
result_b.triangulation.canonical_hash());
REQUIRE_EQ(result_a.action_trace.size(), result_b.action_trace.size());
for (std::size_t index = 0; index < result_a.action_trace.size(); ++index)
{
CHECK_EQ(result_a.action_trace[index], result_b.action_trace[index]);
}
CHECK_EQ(result_a.measurements.size(), 5);
}
TEST_CASE("Metropolis4 different seeds produce different traces")
{
Metropolis4Config config;
config.couplings = S4Couplings{1.0L, 0.2L, 0.1L, 48, 0.001L};
config.seed = 1234;
Metropolis4 run_a(config);
auto const result_a = run_a.run(FoliatedTriangulation4::periodic_seed(4), 12);
config.seed = 5678;
Metropolis4 run_b(config);
auto const result_b = run_b.run(FoliatedTriangulation4::periodic_seed(4), 12);
CHECK_NE(result_a.action_trace, result_b.action_trace);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Metropolis_4_test.cpp` around lines 23 - 31, Update the determinism
assertions in the test’s action_trace loop to use exact CHECK_EQ comparisons for
the long-double values instead of doctest::Approx. Extend the determinism
coverage with a separate run using a different Metropolis4Config::seed and
assert that its action trace differs from the identical-seed result, while
keeping the existing same-seed equality checks intact.

Source: Path instructions

Comment on lines +34 to +52
TEST_CASE("Metropolis4 records invalid proposals and accepted moves")
{
Metropolis4Config config;
config.seed = 7;
config.couplings = S4Couplings{1.0L, 0.2L, 0.1L, 48, 0.0L};
Metropolis4 run(config);
auto result = run.run(FoliatedTriangulation4::periodic_seed(3), 20);

auto attempted = 0;
auto accepted = 0;
for (auto const& stat : result.move_stats)
{
attempted += stat.attempted;
accepted += stat.accepted;
}
CHECK_EQ(attempted, 20);
CHECK_GE(accepted, 0);
CHECK(result.triangulation.is_valid());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

The assertions do not match the test name.

The name is Metropolis4 records invalid proposals and accepted moves, but:

  • Line 50 asserts CHECK_GE(accepted, 0). MoveStat::accepted is an unsigned-in-spirit counter that only increments, so this assertion can never fail. It verifies nothing.
  • No assertion touches stat.invalid, even though the name claims coverage of invalid proposals.

Lines 42-43 also declare attempted and accepted as int while MoveStat::attempted is Int_precision. Use Int_precision so the accumulators cannot narrow in a longer test.

💚 Proposed change
-  auto attempted = 0;
-  auto accepted = 0;
+  Int_precision attempted{0};
+  Int_precision accepted{0};
+  Int_precision invalid{0};
   for (auto const& stat : result.move_stats)
   {
     attempted += stat.attempted;
     accepted += stat.accepted;
+    invalid += stat.invalid;
   }
   CHECK_EQ(attempted, 20);
-  CHECK_GE(accepted, 0);
+  // Every attempt is accepted, rejected, or invalid.
+  CHECK_LE(accepted + invalid, attempted);
+  CHECK_GT(accepted + invalid, 0);
   CHECK(result.triangulation.is_valid());

A test that pins the exact accepted and invalid counts for seed 7 would be stronger still, because it would catch any change to the acceptance rule.

As per path instructions, "Descriptive test names are used to clearly convey the intent of each test."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
TEST_CASE("Metropolis4 records invalid proposals and accepted moves")
{
Metropolis4Config config;
config.seed = 7;
config.couplings = S4Couplings{1.0L, 0.2L, 0.1L, 48, 0.0L};
Metropolis4 run(config);
auto result = run.run(FoliatedTriangulation4::periodic_seed(3), 20);
auto attempted = 0;
auto accepted = 0;
for (auto const& stat : result.move_stats)
{
attempted += stat.attempted;
accepted += stat.accepted;
}
CHECK_EQ(attempted, 20);
CHECK_GE(accepted, 0);
CHECK(result.triangulation.is_valid());
}
TEST_CASE("Metropolis4 records invalid proposals and accepted moves")
{
Metropolis4Config config;
config.seed = 7;
config.couplings = S4Couplings{1.0L, 0.2L, 0.1L, 48, 0.0L};
Metropolis4 run(config);
auto result = run.run(FoliatedTriangulation4::periodic_seed(3), 20);
Int_precision attempted{0};
Int_precision accepted{0};
Int_precision invalid{0};
for (auto const& stat : result.move_stats)
{
attempted += stat.attempted;
accepted += stat.accepted;
invalid += stat.invalid;
}
CHECK_EQ(attempted, 20);
// Every attempt is accepted, rejected, or invalid.
CHECK_LE(accepted + invalid, attempted);
CHECK_GT(accepted + invalid, 0);
CHECK(result.triangulation.is_valid());
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Metropolis_4_test.cpp` around lines 34 - 52, Update the test case
“Metropolis4 records invalid proposals and accepted moves” to accumulate
attempted and accepted using Int_precision, and add assertions that aggregate
and verify stat.invalid so the test covers invalid proposals as named. Replace
the non-informative CHECK_GE(accepted, 0) with meaningful expected-count
assertions, preferably exact counts for seed 7 while preserving the existing
attempted-count and triangulation-validity checks.

Source: Path instructions

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review continued from previous batch...

Comment on lines +33 to +38
auto candidate = before;
auto const forward = before.candidate_multiplicity(move);
auto const reverse_move = move_tracker::reverse_move(move);
if (forward <= 0 || !candidate.apply_move(move))
{
return std::unexpected("4D move is not applicable.");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate file =="
fd -a 'Ergodic_moves_4\.hpp$' . || true

echo "== File excerpt =="
if [ -f include/Ergodic_moves_4.hpp ]; then
  cat -n include/Ergodic_moves_4.hpp | sed -n '1,90p'
fi

echo "== MoveApplication declarations/usages =="
rg -n "struct MoveApplication|class MoveApplication|MoveApplication\\{|candidate_multiplicity|apply_move\\(" include src . -g '*.hpp' -g '*.cpp' -g '*.h' | sed -n '1,200p'

Repository: acgetchell/CDT-plusplus

Length of output: 6853


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Read-only semantics probe for C++ rvalue/move constructor dispatch =="
python3 - <<'PY'
# We cannot compile C++ in the sandbox reliably, so this only summarizes the relevant
# C++ overload/overload resolution rules as documented facts for MoveApplication construction.
rules = [
    "std::move(candidate) converts candidate to a prvalue of MoveApplication&&, selecting MoveApplication(move).",
    "Named variable candidate is an lvalue of type MoveApplication, so MoveApplication{candidate, ...} uses the copy constructor if not explicitly moved.",
    "Unreachable copies from 'candidate = before' happen before the return when forward <= 0, unless the compiler elides the unused object entirely.",
    "Elision cannot make a dead-variable assignment disappear in the general model if side effects exist, so code should not copy before an available early return.",
]
for i, r in enumerate(rules, 1):
    print(f"{i}. {r}")
PY

Repository: acgetchell/CDT-plusplus

Length of output: 806


Minimize copies around 4D move application.

Create candidate only after a non-zero forward multiplicity, and move it into MoveApplication instead of copying the named candidate. This removes work on rejected proposals and accepted moves.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@include/Ergodic_moves_4.hpp` around lines 33 - 38, In the 4D move application
flow, check that `forward` is non-zero before creating `candidate`, so rejected
proposals avoid constructing a copy. When constructing `MoveApplication`, move
the named `candidate` into it rather than copying it, while preserving the
existing applicability validation and reverse-move handling.

Comment on lines +726 to +758
[[nodiscard]] auto time_reversed() const -> FoliatedTriangulation4
{
auto reversed = *this;
for (auto& vertex : reversed.m_vertices)
{
vertex.time = (m_timeslices - vertex.time) % m_timeslices;
}
for (auto& simplex : reversed.m_simplices)
{
switch (simplex.type)
{
case SimplexType4D::FOUR_ONE:
simplex.type = SimplexType4D::ONE_FOUR;
break;
case SimplexType4D::ONE_FOUR:
simplex.type = SimplexType4D::FOUR_ONE;
break;
case SimplexType4D::THREE_TWO:
simplex.type = SimplexType4D::TWO_THREE;
break;
case SimplexType4D::TWO_THREE:
simplex.type = SimplexType4D::THREE_TWO;
break;
}
}
std::swap(reversed.m_counts.N41, reversed.m_counts.N14);
std::swap(reversed.m_counts.N32, reversed.m_counts.N23);
reversed.m_proposal_inventory =
proposal_inventory_from_counts(reversed.m_counts);
std::reverse(reversed.m_spatial_profile.begin(),
reversed.m_spatial_profile.end());
return reversed;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The profile reversal does not match the vertex time reversal.

Line 731 maps vertex times with t -> (m_timeslices - t) % m_timeslices, so time 0 stays at 0 and time 1 moves to T-1. std::reverse at line 755 maps profile index i to T-1-i, so index 0 moves to T-1. The two mappings differ by one slice, and the reversed profile no longer aligns with the reversed vertex times.

🐛 Proposed fix for the profile mapping
-      std::reverse(reversed.m_spatial_profile.begin(),
-                   reversed.m_spatial_profile.end());
+      if (m_spatial_profile.size() ==
+          static_cast<std::size_t>(m_timeslices) && m_timeslices > 0)
+      {
+        Profile mapped(m_spatial_profile.size(), 0);
+        auto const slices = static_cast<std::size_t>(m_timeslices);
+        for (std::size_t index = 0; index < slices; ++index)
+        {
+          mapped[(slices - index) % slices] = m_spatial_profile[index];
+        }
+        reversed.m_spatial_profile = std::move(mapped);
+      }

Also consider flipping m_three_three_forward here. candidate_multiplicity selects a different proposal observable based on that flag, so a time-reversed state currently keeps the forward direction of the original state.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
[[nodiscard]] auto time_reversed() const -> FoliatedTriangulation4
{
auto reversed = *this;
for (auto& vertex : reversed.m_vertices)
{
vertex.time = (m_timeslices - vertex.time) % m_timeslices;
}
for (auto& simplex : reversed.m_simplices)
{
switch (simplex.type)
{
case SimplexType4D::FOUR_ONE:
simplex.type = SimplexType4D::ONE_FOUR;
break;
case SimplexType4D::ONE_FOUR:
simplex.type = SimplexType4D::FOUR_ONE;
break;
case SimplexType4D::THREE_TWO:
simplex.type = SimplexType4D::TWO_THREE;
break;
case SimplexType4D::TWO_THREE:
simplex.type = SimplexType4D::THREE_TWO;
break;
}
}
std::swap(reversed.m_counts.N41, reversed.m_counts.N14);
std::swap(reversed.m_counts.N32, reversed.m_counts.N23);
reversed.m_proposal_inventory =
proposal_inventory_from_counts(reversed.m_counts);
std::reverse(reversed.m_spatial_profile.begin(),
reversed.m_spatial_profile.end());
return reversed;
}
[[nodiscard]] auto time_reversed() const -> FoliatedTriangulation4
{
auto reversed = *this;
for (auto& vertex : reversed.m_vertices)
{
vertex.time = (m_timeslices - vertex.time) % m_timeslices;
}
for (auto& simplex : reversed.m_simplices)
{
switch (simplex.type)
{
case SimplexType4D::FOUR_ONE:
simplex.type = SimplexType4D::ONE_FOUR;
break;
case SimplexType4D::ONE_FOUR:
simplex.type = SimplexType4D::FOUR_ONE;
break;
case SimplexType4D::THREE_TWO:
simplex.type = SimplexType4D::TWO_THREE;
break;
case SimplexType4D::TWO_THREE:
simplex.type = SimplexType4D::THREE_TWO;
break;
}
}
std::swap(reversed.m_counts.N41, reversed.m_counts.N14);
std::swap(reversed.m_counts.N32, reversed.m_counts.N23);
reversed.m_proposal_inventory =
proposal_inventory_from_counts(reversed.m_counts);
if (m_spatial_profile.size() ==
static_cast<std::size_t>(m_timeslices) && m_timeslices > 0)
{
Profile mapped(m_spatial_profile.size(), 0);
auto const slices = static_cast<std::size_t>(m_timeslices);
for (std::size_t index = 0; index < slices; ++index)
{
mapped[(slices - index) % slices] = m_spatial_profile[index];
}
reversed.m_spatial_profile = std::move(mapped);
}
return reversed;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@include/Foliated_triangulation_4.hpp` around lines 726 - 758, Update
FoliatedTriangulation4::time_reversed so m_spatial_profile uses the same cyclic
time mapping as vertex.time: preserve the profile entry for slice 0 and shift
each other slice from i to (m_timeslices - i) % m_timeslices instead of applying
std::reverse. Also invert m_three_three_forward in the returned triangulation so
candidate_multiplicity observes the reversed proposal direction.

Comment on lines +72 to +78
struct FiniteSizeScalingReport
{
long double width_exponent{0.0L};
long double peak_exponent{0.0L};
long double collapse_error{0.0L};
bool passed{false};
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

collapse_error is always 0.0 and never computed.

FiniteSizeScalingReport::collapse_error is initialized to 0.0L in the aggregate at Lines 236-240 and never assigned any other value in analyze_finite_size_scaling. A reader of CdsValidationReport::scaling.collapse_error will see 0.0 and can mistake this for a perfect scaling collapse, even when width_exponent/peak_exponent are far off target. No test in Phase_analysis_test.cpp exercises this field, so the gap is currently silent.

Compute an actual residual (for example, the sum of squared log-log regression residuals from fit_power_law_exponent) and assign it to collapse_error, or remove the field until it is implemented.

Also applies to: 223-244

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@include/Phase_analysis.hpp` around lines 72 - 78, Implement collapse_error in
analyze_finite_size_scaling instead of leaving
FiniteSizeScalingReport::collapse_error at its default zero value. Compute an
actual residual using the regression results from fit_power_law_exponent, such
as the sum of squared log-log residuals, and assign it to the report’s
collapse_error field; otherwise remove the unused field and its exposed report
value.

Comment on lines +103 to +146
[[nodiscard]] inline auto mean(std::vector<Profile> const& profiles)
-> Profile
{
if (profiles.empty()) { return {}; }
Profile result(profiles.front().size(), 0.0L);
for (auto const& profile : profiles)
{
for (std::size_t index = 0; index < result.size(); ++index)
{
result[index] += profile[index];
}
}
for (auto& value : result)
{
value /= static_cast<long double>(profiles.size());
}
return result;
}

[[nodiscard]] inline auto covariance(std::vector<Profile> const& profiles,
Profile const& profile_mean)
-> std::vector<Profile>
{
std::vector<Profile> result(profile_mean.size(),
Profile(profile_mean.size(), 0.0L));
if (profiles.size() < 2) { return result; }
for (auto const& profile : profiles)
{
for (std::size_t i = 0; i < profile_mean.size(); ++i)
{
for (std::size_t j = 0; j < profile_mean.size(); ++j)
{
result[i][j] +=
(profile[i] - profile_mean[i]) * (profile[j] - profile_mean[j]);
}
}
}
auto const normalizer = static_cast<long double>(profiles.size() - 1);
for (auto& row : result)
{
for (auto& value : row) { value /= normalizer; }
}
return result;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Validate profile size consistency before indexing.

mean() assumes every Profile in profiles has the same size as profiles.front() and covariance() assumes every profile matches profile_mean.size(). Neither function checks this. If a caller passes profiles with mismatched sizes, profile[index] reads out of bounds with no error, causing undefined behavior instead of a clear failure.

Add a size check (throw or assert) at the start of each function.

🛡️ Proposed validation
 [[nodiscard]] inline auto mean(std::vector<Profile> const& profiles)
     -> Profile
 {
   if (profiles.empty()) { return {}; }
   Profile result(profiles.front().size(), 0.0L);
   for (auto const& profile : profiles)
   {
+    if (profile.size() != result.size())
+    {
+      throw std::invalid_argument("mean: profiles must have equal size");
+    }
     for (std::size_t index = 0; index < result.size(); ++index)
     {
       result[index] += profile[index];
     }
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@include/Phase_analysis.hpp` around lines 103 - 146, Validate profile
dimensions at the start of mean and covariance before any indexed access: ensure
every profile matches profiles.front().size() in mean, and every profile matches
profile_mean.size() in covariance. Fail clearly via the project’s established
throw or assertion mechanism when a mismatch is detected, while preserving the
existing empty-input behavior.

Comment on lines +292 to +346
[[nodiscard]] inline auto diagnose_c_ds_finite_size(
std::vector<std::pair<long double, Profile>> profiles_by_volume,
std::size_t const effective_sample_count) -> CdsValidationReport
{
CdsValidationReport report;
if (effective_sample_count < 3 || profiles_by_volume.size() < 3)
{
report.verdict = Verdict::insufficient_effective_samples;
return report;
}

report.minimum_profile_correlation =
std::numeric_limits<long double>::infinity();
for (auto const& [_, original_profile] : profiles_by_volume)
{
auto profile = centered(original_profile);
auto const total =
std::accumulate(profile.begin(), profile.end(), 0.0L);
if (total == 0.0L)
{
report.verdict = Verdict::thermalization_failed;
return report;
}
auto const peak = *std::ranges::max_element(profile) / total;
if (peak > 0.70L)
{
report.verdict = Verdict::collapsed_like;
return report;
}

auto alternating = 0.0L;
for (std::size_t index = 0; index < profile.size(); ++index)
{
alternating += (index % 2 == 0 ? 1.0L : -1.0L) * profile[index];
}
if (std::abs(alternating / total) > 0.30L)
{
report.verdict = Verdict::c_b_like;
return report;
}

auto const correlation =
profile_correlation(profile, cos3_reference(profile.size()));
report.profile_correlations.push_back(correlation);
report.minimum_profile_correlation =
std::min(report.minimum_profile_correlation, correlation);
}

report.scaling = analyze_finite_size_scaling(std::move(profiles_by_volume));
report.verdict = report.scaling.passed &&
report.minimum_profile_correlation > 0.85L
? Verdict::c_ds_supported
: Verdict::no_phase_classification;
return report;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Duplicate profile-classification logic between diagnose_c_ds_finite_size and diagnose.

The per-profile block in the loop at Lines 305-331 and the body of diagnose at Lines 384-411 both compute total, the thermalization check, the collapse-peak check (0.70L), and the alternating-slice check (0.30L) with identical logic and identical magic-number thresholds. Extracting a single helper, for example classify_profile_shape(Profile const&, long double total) -> std::optional<Verdict>, removes the duplication and prevents the two copies from drifting apart if a threshold is tuned in only one place later. Naming the thresholds (collapse_peak_ratio, alternating_ratio_limit, cos3_correlation_limit) as constexpr constants would also make the interface easier to tune and review, per the design/interface focus for header files.

As per path instructions for include/**/*.hpp: "Focus on design patterns, interfaces, and encapsulation."

Also applies to: 370-425

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@include/Phase_analysis.hpp` around lines 292 - 346, Extract the duplicated
per-profile classification from diagnose_c_ds_finite_size and diagnose into a
shared helper such as classify_profile_shape(Profile const&, long double),
preserving the thermalization, collapse, and alternating-slice verdict behavior.
Define named constexpr thresholds for the collapse peak, alternating ratio, and
cos3 correlation limits, then update both callers to reuse the helper and
constants.

Source: Path instructions

Comment on lines +416 to +421
diagnostics.held_out_likelihood = cos3_corr;
diagnostics.aic = -2.0L * cos3_corr + 2.0L * 4.0L;
diagnostics.bic =
-2.0L * cos3_corr +
std::log(static_cast<long double>(diagnostics.mean_profile.size())) *
4.0L;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Clarify that aic/bic/held_out_likelihood are heuristic proxies, not true statistics.

held_out_likelihood is set directly to the cos3 correlation coefficient, and aic/bic reuse that same correlation value in the classic -2*logL + k and -2*logL + k*ln(n) formulas. A correlation coefficient bounded in [-1, 1] is not a log-likelihood, so these values are not real Akaike/Bayesian information criteria. Naming them this way risks a future maintainer treating them as statistically rigorous model-selection scores.

Add a short comment documenting that these are heuristic scores derived from profile shape correlation, not formal AIC/BIC, or rename the fields (for example shape_correlation_score) to avoid the implication.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@include/Phase_analysis.hpp` around lines 416 - 421, Document the assignments
to diagnostics.held_out_likelihood, diagnostics.aic, and diagnostics.bic as
heuristic scores derived from the cos3 profile-shape correlation, explicitly
stating they are not formal likelihood, AIC, or BIC statistics; keep the
existing calculations and field names unchanged.

Comment on lines +56 to +92
TEST_CASE("Synthetic C_dS profiles pass finite-size scaling")
{
std::vector<std::pair<long double, Profile>> profiles;
for (auto const volume : {8000.0L, 16000.0L, 32000.0L, 64000.0L})
{
auto const width = static_cast<std::size_t>(
std::llround(4.0L * std::pow(volume, 0.25L)));
auto profile = cos3_reference(width | static_cast<std::size_t>(1));
auto const amplitude = std::pow(volume, 0.75L);
for (auto& value : profile) { value = 1.0L + amplitude * value; }
profiles.emplace_back(volume, profile);
}

auto scaling = analyze_finite_size_scaling(profiles);
CHECK(scaling.passed);
CHECK(scaling.width_exponent == doctest::Approx(0.25L).epsilon(0.10));
CHECK(scaling.peak_exponent == doctest::Approx(0.75L).epsilon(0.10));
}

TEST_CASE("Full C_dS candidate gate requires profile shape and finite-size scaling")
{
std::vector<std::pair<long double, Profile>> profiles;
for (auto const volume : {8000.0L, 16000.0L, 32000.0L, 64000.0L})
{
auto const width = static_cast<std::size_t>(
std::llround(4.0L * std::pow(volume, 0.25L)));
auto profile = cos3_reference(width | static_cast<std::size_t>(1));
auto const amplitude = std::pow(volume, 0.75L);
for (auto& value : profile) { value = 1.0L + amplitude * value; }
profiles.emplace_back(volume, profile);
}

auto validation = diagnose_c_ds_finite_size(profiles, profiles.size());
CHECK_EQ(validation.verdict, Verdict::c_ds_supported);
CHECK(validation.scaling.passed);
CHECK_GT(validation.minimum_profile_correlation, 0.85L);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the shared synthetic-profile builder used by both finite-size-scaling tests.

Lines 58-67 and Lines 77-86 construct the identical profiles_by_volume sequence (same volumes, same width/amplitude formulas). Factor this into a small helper, for example build_synthetic_c_ds_profiles(), and call it from both TEST_CASEs to remove the duplication and keep the two tests in sync if the synthetic-data recipe changes.

♻️ Proposed helper
namespace
{
  auto build_synthetic_c_ds_profiles()
      -> std::vector<std::pair<long double, Profile>>
  {
    std::vector<std::pair<long double, Profile>> profiles;
    for (auto const volume : {8000.0L, 16000.0L, 32000.0L, 64000.0L})
    {
      auto const width = static_cast<std::size_t>(
          std::llround(4.0L * std::pow(volume, 0.25L)));
      auto profile = cos3_reference(width | static_cast<std::size_t>(1));
      auto const amplitude = std::pow(volume, 0.75L);
      for (auto& value : profile) { value = 1.0L + amplitude * value; }
      profiles.emplace_back(volume, profile);
    }
    return profiles;
  }
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Phase_analysis_test.cpp` around lines 56 - 92, Extract the duplicated
synthetic profile construction from both TEST_CASEs into a shared helper named
build_synthetic_c_ds_profiles, preserving the existing volumes, width
calculation, profile generation, and amplitude scaling. Replace each local
profiles initialization loop with a call to this helper so both tests use the
same recipe.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants