Skip to content

feat!(cdt): add dimensional observables#119

Merged
acgetchell merged 2 commits intomainfrom
feat/58-observables
May 6, 2026
Merged

feat!(cdt): add dimensional observables#119
acgetchell merged 2 commits intomainfrom
feat/58-observables

Conversation

@acgetchell
Copy link
Copy Markdown
Owner

  • Add volume-profile, Hausdorff-dimension, and spectral-dimension observables for CDT analysis.
  • Record per-slice volume profiles in simulation measurements and expose aggregate observable summaries on simulation results.
  • Add a runnable observables example, academic references, and updated code-organization and roadmap documentation.
  • Apply saturating simplex-count conversions consistently while keeping conversion helpers crate-internal.

BREAKING CHANGE: The broad prelude now keeps only common quick-start imports; specialized foliation, move, proposal, and analysis APIs should be imported from scoped preludes or root exports. The saturating_usize_to_i32 helper is no longer public API.

Closes #58

- Add volume-profile, Hausdorff-dimension, and spectral-dimension observables for CDT analysis.
- Record per-slice volume profiles in simulation measurements and expose aggregate observable summaries on simulation results.
- Add a runnable observables example, academic references, and updated code-organization and roadmap documentation.
- Apply saturating simplex-count conversions consistently while keeping conversion helpers crate-internal.

BREAKING CHANGE: The broad prelude now keeps only common quick-start imports; specialized foliation, move, proposal, and analysis APIs should be imported from scoped preludes or root exports. The `saturating_usize_to_i32` helper is no longer public API.
@acgetchell acgetchell enabled auto-merge May 6, 2026 05:13
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

Caution

Review failed

Failed to post review comments

Walkthrough

Adds per-slice volume-profile recording, user-facing Hausdorff and spectral dimension estimators, SimulationResultsBackend and Measurement extensions, triangulation foliation/volume APIs, iterator and safety refactors, prelude/public API reorganization, examples/benchmarks/tests for observables, and broad documentation/tooling updates.

Changes

Volume profile, observables, results, and API wiring

Layer / File(s) Summary
Data Shape
src/cdt/metropolis.rs, src/cdt/results.rs, src/cdt/triangulation.rs
Measurement gains volume_profile: Vec<u32>; new Measurement/SimulationResultsBackend types introduced in src/cdt/results.rs; DelaunayBackend2D triangulation exposes volume_profile() and face_time_slice() helpers.
Core Algorithms / Estimators
src/cdt/observables.rs, src/cdt/triangulation/foliation.rs
New estimate_hausdorff_dimension and estimate_spectral_dimension compute dimensions via dual adjacency, BFS ball growth, and random-walk return probabilities; foliation module provides classification, time-label helpers, and integrates face time-slice logic used by estimators.
Simulation Integration
src/cdt/metropolis.rs, src/lib.rs
measurement_for() populates volume_profile; simplex_counts uses saturating conversions; CdtTarget::new becomes fallible and validates temperature; crate exposes observables and results modules and adds run_simulation(config) -> CdtResult<SimulationResultsBackend>.
Utilities / Safety
src/util.rs, src/config.rs, src/cdt/action.rs, src/geometry/operations.rs
Added pub(crate) saturating_usize_to_u32 and made existing saturating i32 helper crate-private; multiple modules add #![forbid(unsafe_code)]; operations trait bounds and signatures adjusted (Sized handling).
Geometry API / Backend Changes
src/geometry/traits.rs, src/geometry/backends/*.rs, src/geometry/generators.rs
Switched TriangulationQuery iteration APIs from boxed trait objects to impl Iterator returns; Delaunay backend mutation paths hardened with state-capture/restore; centralized generation parameter validation added.
Triangulation Builders, Moves, Validation
src/cdt/triangulation/builders.rs, src/cdt/triangulation/moves.rs, src/cdt/triangulation/validation.rs
Added builders for strips/toroidal CDT, move-support hooks (flip/subdivide/remove/set_vertex_data) with modification-count and cache invalidation, and validation routines for foliation/causality/cell classification.
Tests, Examples, Benchmarks
examples/observables.rs, benches/cdt_benchmarks.rs, src/cdt/metropolis.rs tests, src/cdt/observables.rs tests
New example that runs Metropolis and prints volume/profile and dimensions; benchmarks updated to include volume_profile; unit tests added/extended across observables, foliation, builders, validation, and Metropolis behavior.
Docs, Tooling, CI
docs/code_organization.md, docs/dev/*, AGENTS.md, CONTRIBUTING.md, README.md, docs/roadmap.md, docs/testing.md, docs/PERFORMANCE_TESTING.md, Cargo.toml, justfile, scripts/run_all_examples.sh, semgrep.yaml
Removed/replace docs/project.md content with docs/code_organization.md; added prelude guidance and geometry-backend isolation; added example validation workflow (validate-examples) and CI/script updates; updated semgrep rules to ban bare unwrap/panic in src; Cargo.toml documentation field added; references/roadmap updated for volume-profile and observables.

Sequence Diagram

sequenceDiagram
    participant Client as CLI / Example
    participant Sim as Metropolis Simulation
    participant Tri as Triangulation (CdtTriangulation2D)
    participant Res as SimulationResultsBackend
    participant Obs as Observable Estimators

    Client->>Sim: run_simulation(config)
    Sim->>Tri: build initial triangulation / validate foliation
    loop each measurement
      Sim->>Tri: propose/mutate (moves)
      Tri-->>Sim: updated triangulation
      Sim->>Tri: triangulation.volume_profile()
      Tri-->>Sim: Vec<u32> per-slice volumes
      Sim->>Res: record Measurement{..., volume_profile}
    end
    Sim->>Res: collect equilibrium measurements
    Sim->>Obs: estimate_hausdorff_dimension(&final_triangulation)
    Obs->>Tri: dual_adjacency(), BFS ball growth
    Obs-->>Sim: Option<f64> d_H
    Sim->>Obs: estimate_spectral_dimension(&final_triangulation)
    Obs->>Tri: dual_adjacency(), random-walk return probs
    Obs-->>Sim: Option<f64> d_S
    Sim->>Res: Res.average_volume_profile()
    Res-->>Client: results + estimates
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 Hop, hop — I count each triangular cheer,

Slices stack like carrots, near and dear.
Dual hops measure how returns align,
Hausdorff hums, spectra softly shine.
Observables baked — a crunchy CDT treat!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat!(cdt): add dimensional observables' clearly and concisely summarizes the main change: adding Hausdorff and spectral dimension observables to the CDT module.
Description check ✅ Passed The description comprehensively relates to the changeset, detailing volume-profile, Hausdorff-dimension, spectral-dimension observables, simulation measurement updates, example additions, and breaking changes to the prelude API.
Linked Issues check ✅ Passed All primary objectives from #58 are met: volume_profile field added to Measurement (#58 req 1), average_volume_profile and volume_fluctuations methods implemented (#58 req 2), Hausdorff dimension estimator implemented via estimate_hausdorff_dimension (#58 req 3), and spectral dimension implemented as stretch goal (#58 req 4).
Out of Scope Changes check ✅ Passed All changes directly support the core observables objectives: dimensional estimators, measurement infrastructure, example code, documentation updates, validation hardening, and consistent saturating conversions are all scope-aligned with #58 requirements.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/58-observables

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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 and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 6, 2026

Codecov Report

❌ Patch coverage is 86.03089% with 416 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.50%. Comparing base (85547fa) to head (aa08007).

Files with missing lines Patch % Lines
src/cdt/triangulation/foliation.rs 80.16% 164 Missing ⚠️
src/cdt/triangulation/validation.rs 64.96% 96 Missing ⚠️
src/cdt/triangulation/builders.rs 89.16% 87 Missing ⚠️
src/geometry/backends/delaunay.rs 61.31% 53 Missing ⚠️
src/cdt/metropolis.rs 92.38% 8 Missing ⚠️
src/cdt/observables.rs 97.83% 6 Missing ⚠️
src/cdt/results.rs 99.20% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #119      +/-   ##
==========================================
- Coverage   92.53%   92.50%   -0.03%     
==========================================
  Files          13       19       +6     
  Lines        7794     8557     +763     
==========================================
+ Hits         7212     7916     +704     
- Misses        582      641      +59     
Flag Coverage Δ
unittests 92.50% <86.03%> (-0.03%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
CONTRIBUTING.md (1)

202-203: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Align branch naming example with repository convention.

Line 202 currently uses feature/your-feature-name, which conflicts with the repo’s preferred {type}/{issue}-descriptor-or-two pattern. Updating this example avoids mixed guidance for contributors.

Based on learnings: “When suggesting branch names, prefer {type}/{issue}-descriptor-or-two format (e.g., fix/307-topology-validation, perf/315-bench-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 `@CONTRIBUTING.md` around lines 202 - 203, Replace the branch naming example
"git checkout -b feature/your-feature-name" with the repository convention using
the {type}/{issue}-descriptor-or-two pattern; update the example line to show
the new format (referencing the literal string "git checkout -b
feature/your-feature-name" to locate it) and provide example branch names such
as "fix/307-topology-validation" and "perf/315-bench-profile" to demonstrate the
preferred convention.
🧹 Nitpick comments (8)
src/cdt/triangulation.rs (1)

2005-2017: ⚡ Quick win

Avoid O(T) slab scan per face in toroidal face_time_slice

Line 2009 introduces a full 0..time_slices scan for every face. Since volume_profile() calls this for all faces, this becomes O(F·T). You can derive the slab in O(1) from the two distinct labels after cell_type() succeeds.

Suggested direction
         match self.metadata.topology {
             CdtTopology::OpenBoundary => Some(labels[0].min(labels[1]).min(labels[2])),
             CdtTopology::Toroidal => {
                 let total = self.metadata.time_slices;
-                for slice in 0..total {
-                    let next = (slice + 1) % total;
-                    let spans_slab = labels.iter().all(|&label| label == slice || label == next)
-                        && labels.contains(&slice)
-                        && labels.contains(&next);
-                    if spans_slab {
-                        return Some(slice);
-                    }
-                }
-                None
+                let mut uniq = [labels[0], labels[1], labels[2]];
+                uniq.sort_unstable();
+                uniq.dedup();
+                let (a, b) = match uniq {
+                    [a, b, ..] if a != b => (a, b),
+                    _ => return None,
+                };
+                if (a + 1) % total == b {
+                    Some(a)
+                } else if (b + 1) % total == a {
+                    Some(b)
+                } else {
+                    None
+                }
             }
         }
🤖 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 `@src/cdt/triangulation.rs` around lines 2005 - 2017, The toroidal branch
currently scans all time_slices per face (O(T)), make it O(1) by deriving the
slab from the two distinct labels instead of iterating: in the toroidal arm of
face_time_slice (called from volume_profile() after cell_type() succeeds),
collect the distinct values from labels (e.g., via a small set or compare
values), ensure there are exactly two distinct labels l0 and l1, then check
adjacency modulo total = self.metadata.time_slices (if l1 == (l0+1)%total return
Some(l0), else if l0 == (l1+1)%total return Some(l1), else return None); leave
other branches unchanged.
src/cdt/metropolis.rs (3)

192-198: ⚡ Quick win

Adding a public field is a breaking change for direct struct construction.

Measurement is pub with all public fields, so adding volume_profile breaks any downstream code that constructs Measurement { … } via struct literal (no #[non_exhaustive]). This is acceptable here because the PR is feat! and the change is captured in the breaking-change footer, but consider marking Measurement #[non_exhaustive] going forward to absorb similar future additions without a major bump.

🤖 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 `@src/cdt/metropolis.rs` around lines 192 - 198, The Measurement struct was
made extensible by adding the new public field volume_profile which is a
breaking change for direct struct literal construction; to avoid future breaking
changes, mark the Measurement struct with #[non_exhaustive] so downstream crates
cannot exhaustively construct it and will not break when new fields like
volume_profile are added — locate the Measurement declaration in
src/cdt/metropolis.rs and add the #[non_exhaustive] attribute above the struct
definition (ensuring any existing public visibility remains unchanged) and
update any internal construction sites to use the struct literal within the same
crate or provide a constructor if needed.

1267-1304: 💤 Low value

Dimension estimates only use the final triangulation, not the equilibrium ensemble.

hausdorff_dimension_estimate and spectral_dimension_estimate evaluate the estimator solely on self.triangulation (the final post-run state). For CDT observables an ensemble average over equilibrium measurements is the more physically meaningful quantity, but reproducing intermediate triangulations would require keeping snapshots in Measurement (or rerunning the chain). The current single-state approach is reasonable as a first cut and is consistent with issue #58's "simple log-log fit", but consider clarifying in the doc that the estimate is not averaged over the equilibrium ensemble so users don't conflate it with average_volume_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 `@src/cdt/metropolis.rs` around lines 1267 - 1304, Update the docstrings for
hausdorff_dimension_estimate and spectral_dimension_estimate to explicitly state
that these methods compute the estimator only on the final triangulation
(self.triangulation) and do not perform an ensemble average over equilibrium
measurements; mention that reproducing an equilibrium average would require
storing snapshots in Measurement or rerunning the chain and optionally point
users to average_volume_profile for ensemble-style information so they don't
conflate the single-state estimate with an equilibrium ensemble average.

1207-1234: 💤 Low value

Population standard deviation used for volume_fluctuations.

volume_fluctuations divides variance by measurements.len() rather than len() - 1, i.e. it returns the population (biased) standard deviation rather than the sample (unbiased) one. For CDT-style equilibrium observables this is a defensible convention, but it's worth either documenting the convention explicitly or switching to the sample form (and guarding against len() < 2) so consumers don't misinterpret error bars.

🤖 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 `@src/cdt/metropolis.rs` around lines 1207 - 1234, The function
volume_fluctuations currently computes the population stddev by dividing by
measurements.len(); change it to compute the sample (unbiased) stddev: obtain n
= measurements.len(), if n < 2 return an empty Vec (or Vec of zeros if
preferred), otherwise use denominator (n - 1) when normalizing the accumulated
variances (convert n-1 to f64 via NumCast like NumCast::from(n -
1).unwrap_or(1.0)); update the normalization in the map to use (variance /
denom).sqrt(). Reference equilibrium_measurements() and average_volume_profile()
when locating the accumulation and the count logic to add the guard.
src/cdt/observables.rs (2)

268-317: ⚖️ Poor tradeoff

Reasonable random-walk implementation; bipartite oscillation may limit usable samples in practice.

For nearly bipartite dual graphs (e.g. small toroidal CDTs at low refinement) the return probability oscillates between ~1 and ~0 across consecutive steps. fit_spectral_dimension filters values outside (0, 1), so usable points can drop below the 2-sample threshold and the estimator will return None even though there is real diffusion data. This is acceptable for a first implementation (and is consistent with the stretch-goal status of spectral dimension in #58), but it's worth either noting the limitation in the doc or considering a lazy-walk variant (e.g. self-loop probability 1/2) in a follow-up to break the parity.

🤖 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 `@src/cdt/observables.rs` around lines 268 - 317, average_return_probabilities
currently does a simple random walk that causes parity oscillation on
nearly-bipartite graphs; change it to a lazy random walk by giving each node a
self-loop probability (e.g. self_loop = 0.5) so a fraction of probability stays
at the node each step and the rest is distributed to neighbors. Concretely,
inside average_return_probabilities (the inner loop over current probabilities)
compute stay = probability * self_loop, add stay to next[index], then distribute
probability * (1.0 - self_loop) equally among live neighbors (using the same
neighbor_count/NumCast logic); keep sums and swapping logic the same so return
probabilities are averaged as before. Optionally make self_loop a constant or
parameter for future tuning.

235-250: 💤 Low value

Document the volume > 1.0 filter rationale.

The filter drops samples where ball volume is exactly 1.0 (i.e. only the root face is reachable at that radius), which produces a ln(volume) = 0 point that biases the slope toward zero. Worth a one-line comment explaining that intent — otherwise it reads as an arbitrary threshold and could be "fixed" later to > 0.0 or >= 1.0, silently regressing the fit on small/disconnected components.

Suggested clarifying comment
 fn fit_log_log_slope(ball_volumes: &[f64]) -> Option<f64> {
     fit_linear_slope(
         ball_volumes
             .iter()
             .enumerate()
             .skip(1)
             .filter_map(|(radius, &volume)| {
+                // Drop volume == 1.0 (root only): ln(1) = 0 anchors the
+                // slope at the origin and biases the fit on disconnected
+                // or trivially small dual graphs.
                 if volume > 1.0 && volume.is_finite() {
🤖 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 `@src/cdt/observables.rs` around lines 235 - 250, Add a one-line comment next
to the volume filter in fit_log_log_slope explaining why we use volume > 1.0
(i.e., to exclude samples where volume == 1.0 because ln(1.0) == 0 and such
“root-only” reachable points would bias the log-log slope toward zero); place
the comment adjacent to the filter_map closure (referencing the volume > 1.0
check) so future readers won’t change it to > 0.0 or >= 1.0 by mistake.
src/lib.rs (1)

244-269: 💤 Low value

Document why prelude::observables re-exports CdtTriangulation types.

CdtTriangulation and CdtTriangulation2D are already available in prelude::triangulation and prelude::geometry. Re-exporting them here is reasonable for ergonomics (the doctest needs from_cdt_strip in scope), but the duplication should be called out in the module doc so it doesn't drift over time. As per coding guidelines/learnings: "Keep scoped preludes minimal and orthogonal; do not duplicate specialized APIs across scoped preludes unless the overlap is intentionally documented."

Suggested doc clarification
     /// Focused exports for CDT observables and post-simulation analysis.
     ///
     /// This prelude is intended for measuring triangulations without importing
     /// simulation runner, telemetry, proposal, or move APIs.
+    ///
+    /// `CdtTriangulation` and `CdtTriangulation2D` are intentionally re-exported
+    /// from this prelude (also available via `prelude::triangulation` /
+    /// `prelude::geometry`) so a single `use prelude::observables::*;` is
+    /// sufficient for typical analysis workflows.

Based on learnings: "Keep scoped preludes minimal and orthogonal; do not duplicate specialized APIs across scoped preludes unless the overlap is intentionally documented."

🤖 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 `@src/lib.rs` around lines 244 - 269, Add a short module-level doc comment to
prelude::observables explaining why it re-exports CdtTriangulation and
CdtTriangulation2D (even though those types are exported by
prelude::triangulation and prelude::geometry) to document the intentional
overlap for ergonomics (e.g., to make methods like
CdtTriangulation::from_cdt_strip available to the doctest) and to signal this
duplication is deliberate and should be maintained; update the docstring at the
top of the observables mod (referencing prelude::observables, CdtTriangulation,
CdtTriangulation2D, and the doctest use of from_cdt_strip) with one or two
sentences describing this rationale.
benches/cdt_benchmarks.rs (1)

341-358: 💤 Low value

Consider benchmarking the new observable APIs.

The bench_simulation_analysis group already exercises acceptance_rate, average_action, and equilibrium_measurements. Adding bench points for average_volume_profile, volume_fluctuations, and the dimension estimators would catch performance regressions in the new analysis surface — hausdorff_dimension_estimate in particular is O(F·(F+E_d)) and worth keeping an eye on.

🤖 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 `@benches/cdt_benchmarks.rs` around lines 341 - 358, The benchmark group
bench_simulation_analysis currently measures acceptance_rate, average_action,
and equilibrium_measurements but omits the new observable APIs; update
benches/cdt_benchmarks.rs to add benchmark points that call
average_volume_profile, volume_fluctuations, and the dimension estimators (e.g.,
hausdorff_dimension_estimate and any other estimator functions) inside the same
bench_simulation_analysis harness using the existing Measurement/test dataset so
they run alongside acceptance_rate/average_action; when adding
hausdorff_dimension_estimate ensure the benchmark input size is reasonable (it
is O(F*(F+E_d))) to avoid excessive runtime and label the benches clearly so
regressions for those specific functions are tracked.
🤖 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 `@src/util.rs`:
- Around line 146-148: The test test_saturating_usize_to_u32_overflow uses
`u32::MAX as usize + 1` which can overflow on 32-bit targets; update the test to
be pointer-width safe by guarding the overflow assertion with a cfg so it only
runs on platforms where usize is wider than u32 (e.g. add
#[cfg(target_pointer_width = "64")] around the
`assert_eq!(saturating_usize_to_u32(u32::MAX as usize + 1), u32::MAX);` line or
otherwise compute the overflow value in a way that doesn't overflow on 32-bit,
keeping the second assertion for usize::MAX intact and referencing the test
function name `test_saturating_usize_to_u32_overflow` and the helper
`saturating_usize_to_u32`.

---

Outside diff comments:
In `@CONTRIBUTING.md`:
- Around line 202-203: Replace the branch naming example "git checkout -b
feature/your-feature-name" with the repository convention using the
{type}/{issue}-descriptor-or-two pattern; update the example line to show the
new format (referencing the literal string "git checkout -b
feature/your-feature-name" to locate it) and provide example branch names such
as "fix/307-topology-validation" and "perf/315-bench-profile" to demonstrate the
preferred convention.

---

Nitpick comments:
In `@benches/cdt_benchmarks.rs`:
- Around line 341-358: The benchmark group bench_simulation_analysis currently
measures acceptance_rate, average_action, and equilibrium_measurements but omits
the new observable APIs; update benches/cdt_benchmarks.rs to add benchmark
points that call average_volume_profile, volume_fluctuations, and the dimension
estimators (e.g., hausdorff_dimension_estimate and any other estimator
functions) inside the same bench_simulation_analysis harness using the existing
Measurement/test dataset so they run alongside acceptance_rate/average_action;
when adding hausdorff_dimension_estimate ensure the benchmark input size is
reasonable (it is O(F*(F+E_d))) to avoid excessive runtime and label the benches
clearly so regressions for those specific functions are tracked.

In `@src/cdt/metropolis.rs`:
- Around line 192-198: The Measurement struct was made extensible by adding the
new public field volume_profile which is a breaking change for direct struct
literal construction; to avoid future breaking changes, mark the Measurement
struct with #[non_exhaustive] so downstream crates cannot exhaustively construct
it and will not break when new fields like volume_profile are added — locate the
Measurement declaration in src/cdt/metropolis.rs and add the #[non_exhaustive]
attribute above the struct definition (ensuring any existing public visibility
remains unchanged) and update any internal construction sites to use the struct
literal within the same crate or provide a constructor if needed.
- Around line 1267-1304: Update the docstrings for hausdorff_dimension_estimate
and spectral_dimension_estimate to explicitly state that these methods compute
the estimator only on the final triangulation (self.triangulation) and do not
perform an ensemble average over equilibrium measurements; mention that
reproducing an equilibrium average would require storing snapshots in
Measurement or rerunning the chain and optionally point users to
average_volume_profile for ensemble-style information so they don't conflate the
single-state estimate with an equilibrium ensemble average.
- Around line 1207-1234: The function volume_fluctuations currently computes the
population stddev by dividing by measurements.len(); change it to compute the
sample (unbiased) stddev: obtain n = measurements.len(), if n < 2 return an
empty Vec (or Vec of zeros if preferred), otherwise use denominator (n - 1) when
normalizing the accumulated variances (convert n-1 to f64 via NumCast like
NumCast::from(n - 1).unwrap_or(1.0)); update the normalization in the map to use
(variance / denom).sqrt(). Reference equilibrium_measurements() and
average_volume_profile() when locating the accumulation and the count logic to
add the guard.

In `@src/cdt/observables.rs`:
- Around line 268-317: average_return_probabilities currently does a simple
random walk that causes parity oscillation on nearly-bipartite graphs; change it
to a lazy random walk by giving each node a self-loop probability (e.g.
self_loop = 0.5) so a fraction of probability stays at the node each step and
the rest is distributed to neighbors. Concretely, inside
average_return_probabilities (the inner loop over current probabilities) compute
stay = probability * self_loop, add stay to next[index], then distribute
probability * (1.0 - self_loop) equally among live neighbors (using the same
neighbor_count/NumCast logic); keep sums and swapping logic the same so return
probabilities are averaged as before. Optionally make self_loop a constant or
parameter for future tuning.
- Around line 235-250: Add a one-line comment next to the volume filter in
fit_log_log_slope explaining why we use volume > 1.0 (i.e., to exclude samples
where volume == 1.0 because ln(1.0) == 0 and such “root-only” reachable points
would bias the log-log slope toward zero); place the comment adjacent to the
filter_map closure (referencing the volume > 1.0 check) so future readers won’t
change it to > 0.0 or >= 1.0 by mistake.

In `@src/cdt/triangulation.rs`:
- Around line 2005-2017: The toroidal branch currently scans all time_slices per
face (O(T)), make it O(1) by deriving the slab from the two distinct labels
instead of iterating: in the toroidal arm of face_time_slice (called from
volume_profile() after cell_type() succeeds), collect the distinct values from
labels (e.g., via a small set or compare values), ensure there are exactly two
distinct labels l0 and l1, then check adjacency modulo total =
self.metadata.time_slices (if l1 == (l0+1)%total return Some(l0), else if l0 ==
(l1+1)%total return Some(l1), else return None); leave other branches unchanged.

In `@src/lib.rs`:
- Around line 244-269: Add a short module-level doc comment to
prelude::observables explaining why it re-exports CdtTriangulation and
CdtTriangulation2D (even though those types are exported by
prelude::triangulation and prelude::geometry) to document the intentional
overlap for ergonomics (e.g., to make methods like
CdtTriangulation::from_cdt_strip available to the doctest) and to signal this
duplication is deliberate and should be maintained; update the docstring at the
top of the observables mod (referencing prelude::observables, CdtTriangulation,
CdtTriangulation2D, and the doctest use of from_cdt_strip) with one or two
sentences describing this rationale.
🪄 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.yml

Review profile: CHILL

Plan: Pro

Run ID: 50ffda73-171a-4d8f-9a3a-24ec003d058c

📥 Commits

Reviewing files that changed from the base of the PR and between 85547fa and bf3ffb1.

📒 Files selected for processing (21)
  • AGENTS.md
  • CONTRIBUTING.md
  • Cargo.toml
  • README.md
  • REFERENCES.md
  • benches/cdt_benchmarks.rs
  • docs/PERFORMANCE_TESTING.md
  • docs/code_organization.md
  • docs/dev/rust.md
  • docs/project.md
  • docs/roadmap.md
  • docs/testing.md
  • examples/observables.rs
  • src/cdt/action.rs
  • src/cdt/metropolis.rs
  • src/cdt/observables.rs
  • src/cdt/triangulation.rs
  • src/config.rs
  • src/geometry/operations.rs
  • src/lib.rs
  • src/util.rs
💤 Files with no reviewable changes (1)
  • docs/project.md

Comment thread src/util.rs
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

🔴 Performance Regression Detected

Performance Analysis Report

CDT Performance Analysis Report

Generated: 2026-05-06T05:21:16+00:00

Summary

  • Total benchmarks: 40
  • Regressions: 39
  • Improvements: 0
  • Stable: 0
  • New benchmarks: 1
  • Average change: 29.7%
  • Median change: 29.1%

🔴 Performance Regressions

Benchmark Change Current Baseline Ratio
ergodic_moves/move/Move13Add +43.1% 9.6µs 6.7µs 1.43x
ergodic_moves/move/EdgeFlip +40.1% 9.8µs 7.0µs 1.40x
ergodic_moves/move/Move22 +40.0% 9.8µs 7.0µs 1.40x
ergodic_moves/random_move_attempt +38.7% 9.6µs 6.9µs 1.39x
ergodic_moves/move/Move31Remove +38.5% 8.5µs 6.1µs 1.38x
metropolis_simulation/metropolis_steps/100 +34.6% 12.8ms 9.5ms 1.35x
edge_counting/uncached/25 +34.1% 1.0µs 755.2ns 1.34x
metropolis_simulation/metropolis_steps/50 +33.7% 5.0ms 3.7ms 1.34x
ergodic_moves/random_move_selection +32.7% 91.5ns 69.0ns 1.33x
edge_counting/uncached/50 +32.0% 2.1µs 1.6µs 1.32x
metropolis_simulation/metropolis_steps/10 +31.7% 2.1ms 1.6ms 1.32x
edge_counting/cached/200 +30.9% 0.7ns 0.5ns 1.31x
edge_counting/uncached/100 +30.4% 4.4µs 3.4µs 1.30x
triangulation_creation/delaunay_backend/100 +30.2% 37.0ms 28.4ms 1.30x
geometry_queries/is_valid +30.2% 323.3µs 248.3µs 1.30x
geometry_queries/iterate_edges +29.8% 3.4µs 2.7µs 1.30x
triangulation_creation/delaunay_backend/10 +29.5% 415.0µs 320.4µs 1.30x
triangulation_creation/delaunay_backend/50 +29.3% 10.0ms 7.7ms 1.29x
edge_counting/uncached/200 +29.3% 9.1µs 7.1µs 1.29x
cache_operations/refresh_cache +29.1% 10.0ms 7.8ms 1.29x
geometry_queries/vertex_count +29.0% 0.7ns 0.5ns 1.29x
edge_counting/cached/100 +28.9% 0.7ns 0.5ns 1.29x
geometry_queries/face_count +28.9% 0.7ns 0.5ns 1.29x
edge_counting/cached/50 +28.9% 0.7ns 0.5ns 1.29x
edge_counting/cached/10 +28.9% 0.7ns 0.5ns 1.29x
edge_counting/cached/25 +28.9% 0.7ns 0.5ns 1.29x
action_calculations/calculate_action/50 +28.9% 6.3ns 4.9ns 1.29x
triangulation_creation/delaunay_backend/20 +28.6% 1.7ms 1.3ms 1.29x
validation/validate +28.5% 419.3µs 326.4µs 1.28x
geometry_queries/euler_characteristic +28.4% 2.1µs 1.7µs 1.28x
triangulation_creation/delaunay_backend/5 +28.4% 73.5µs 57.3µs 1.28x
action_calculations/calculate_action/100 +28.2% 6.3ns 4.9ns 1.28x
action_calculations/calculate_action/10 +28.1% 6.3ns 4.9ns 1.28x
simulation_analysis/equilibrium_measurements +28.0% 34.1ns 26.6ns 1.28x
edge_counting/uncached/10 +22.7% 352.8ns 287.6ns 1.23x
simulation_analysis/average_action +19.7% 2.9ns 2.5ns 1.20x
geometry_queries/iterate_faces +16.1% 223.3ns 192.4ns 1.16x
simulation_analysis/acceptance_rate +15.8% 3.2ns 2.7ns 1.16x
geometry_queries/iterate_vertices +14.0% 138.9ns 121.8ns 1.14x

🆕 New Benchmarks

  • cache_operations/metadata_cache_invalidation: 214.0ns

⚠️ This PR introduces performance regressions that exceed the threshold. Please review the changes.


Performance analysis powered by Criterion.rs

- Move CDT triangulation builders, foliation logic, mutation hooks, validation, and simulation result summaries into focused modules.
- Validate Metropolis target and proposal parameters before constructing reusable MCMC components.
- Reject non-finite geometry generator inputs and invalid toroidal domains before they reach Delaunay predicates.
- Make Delaunay backend mutations restore prior state on upstream errors or malformed flip outputs.
- Return concrete topology iterators from TriangulationQuery to avoid boxed iterator dispatch in repeated scans.
- Add validated example output markers and production Rust Semgrep rules for panic-prone error paths.

BREAKING CHANGE: CdtTarget::new, CdtProposal::new, and CdtProposal::with_seed now return CdtResult, and TriangulationQuery iterator methods now return concrete impl Iterator values instead of boxed trait objects.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

🔴 Performance Regression Detected

Performance Analysis Report

CDT Performance Analysis Report

Generated: 2026-05-06T18:23:28+00:00

Summary

  • Total benchmarks: 44
  • Regressions: 19
  • Improvements: 10
  • Stable: 10
  • New benchmarks: 5
  • Average change: 2.4%
  • Median change: 5.9%

🔴 Performance Regressions

Benchmark Change Current Baseline Ratio
simulation_analysis/acceptance_rate +142.4% 6.6ns 2.7ns 2.42x
simulation_analysis/average_action +40.8% 3.5ns 2.5ns 1.41x
edge_counting/uncached/50 +39.7% 2.2µs 1.6µs 1.40x
edge_counting/uncached/100 +31.0% 4.4µs 3.4µs 1.31x
geometry_queries/euler_characteristic +28.4% 2.1µs 1.7µs 1.28x
edge_counting/uncached/200 +28.4% 9.1µs 7.1µs 1.28x
edge_counting/uncached/25 +27.4% 962.4ns 755.2ns 1.27x
ergodic_moves/random_move_selection +23.4% 85.1ns 69.0ns 1.23x
ergodic_moves/move/Move31Remove +22.8% 7.5µs 6.1µs 1.23x
ergodic_moves/random_move_attempt +22.4% 8.5µs 6.9µs 1.22x
ergodic_moves/move/Move22 +21.0% 8.5µs 7.0µs 1.21x
ergodic_moves/move/EdgeFlip +20.9% 8.4µs 7.0µs 1.21x
metropolis_simulation/metropolis_steps/100 +20.5% 11.4ms 9.5ms 1.21x
ergodic_moves/move/Move13Add +19.3% 8.0µs 6.7µs 1.19x
metropolis_simulation/metropolis_steps/50 +16.8% 4.3ms 3.7ms 1.17x
edge_counting/uncached/10 +13.5% 326.5ns 287.6ns 1.14x
action_calculations/calculate_action/10 +13.0% 5.6ns 4.9ns 1.13x
action_calculations/calculate_action/50 +13.0% 5.6ns 4.9ns 1.13x
action_calculations/calculate_action/100 +12.7% 5.6ns 4.9ns 1.13x

🟢 Performance Improvements

Benchmark Change Current Baseline Ratio
geometry_queries/iterate_faces -52.0% 92.4ns 192.4ns 2.08x
geometry_queries/iterate_vertices -49.7% 61.2ns 121.8ns 1.99x
geometry_queries/face_count -42.1% 0.3ns 0.5ns 1.73x
edge_counting/cached/200 -42.0% 0.3ns 0.5ns 1.73x
edge_counting/cached/10 -42.0% 0.3ns 0.5ns 1.73x
edge_counting/cached/50 -42.0% 0.3ns 0.5ns 1.72x
geometry_queries/vertex_count -42.0% 0.3ns 0.5ns 1.72x
edge_counting/cached/100 -42.0% 0.3ns 0.5ns 1.72x
edge_counting/cached/25 -41.9% 0.3ns 0.5ns 1.72x
simulation_analysis/equilibrium_measurements -21.6% 20.9ns 26.6ns 1.27x

🆕 New Benchmarks

  • cache_operations/metadata_cache_invalidation: 154.2ns
  • simulation_analysis/hausdorff_dimension_estimate: 7.3µs
  • simulation_analysis/average_volume_profile: 77.5ns
  • simulation_analysis/volume_fluctuations: 144.3ns
  • simulation_analysis/spectral_dimension_estimate: 38.3µs

✅ Stable Benchmarks

No significant changes detected in 10 benchmarks.

⚠️ This PR introduces performance regressions that exceed the threshold. Please review the changes.


Performance analysis powered by Criterion.rs

@acgetchell acgetchell disabled auto-merge May 6, 2026 18:28
@acgetchell acgetchell merged commit 73e1b28 into main May 6, 2026
16 checks passed
@acgetchell acgetchell deleted the feat/58-observables branch May 6, 2026 18:28
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.

Add volume profile and per-slice observables (N₂(t), Hausdorff dimension)

1 participant