Skip to content

perf: repair insertion neighbors locally (#335)#366

Merged
acgetchell merged 2 commits intomainfrom
perf/335-local-neighbor-repair
May 8, 2026
Merged

perf: repair insertion neighbors locally (#335)#366
acgetchell merged 2 commits intomainfrom
perf/335-local-neighbor-repair

Conversation

@acgetchell
Copy link
Copy Markdown
Owner

  • Replace post-insertion global neighbor rebuilds with seeded local repair over new cells, removed-cell frontiers, and facet-issue survivors.
  • Preserve facet-compatible existing neighbor pointers while repairing empty, dangling, or stale local slots.
  • Add a release-mode escape hatch for forcing global neighbor rebuilds during A/B isolation.
  • Expose the low-level local repair helper through the insertion prelude and preserve typed neighbor-repair error sources.

Closes #335

- Replace post-insertion global neighbor rebuilds with seeded local repair
  over new cells, removed-cell frontiers, and facet-issue survivors.
- Preserve facet-compatible existing neighbor pointers while repairing
  empty, dangling, or stale local slots.
- Add a release-mode escape hatch for forcing global neighbor rebuilds
  during A/B isolation.
- Expose the low-level local repair helper through the insertion prelude
  and preserve typed neighbor-repair error sources.

Closes #335
@acgetchell acgetchell enabled auto-merge (squash) May 8, 2026 19:40
@codacy-production
Copy link
Copy Markdown

codacy-production Bot commented May 8, 2026

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 17 complexity

Metric Results
Complexity 17

View in Codacy

🟢 Coverage 85.43% diff coverage · +0.00% coverage variation

Metric Results
Coverage variation +0.00% coverage variation (-1.00%)
Diff coverage 85.43% diff coverage

View coverage diff in Codacy

Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (83d9619) 52924 47454 89.66%
Head commit (3af15be) 53526 (+602) 47995 (+541) 89.67% (+0.00%)

Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch: <coverage of head commit> - <coverage of common ancestor commit>

Diff coverage details
Coverable lines Covered lines Diff coverage
Pull request (#366) 645 551 85.43%

Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: <covered lines added or modified>/<coverable lines added or modified> * 100%

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 8, 2026

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: c42522e7-07ea-4523-a90b-a4807d5fd3b9

📥 Commits

Reviewing files that changed from the base of the PR and between 5b9ee3b and 3af15be.

📒 Files selected for processing (3)
  • src/core/algorithms/incremental_insertion.rs
  • src/core/triangulation.rs
  • tests/delaunay_incremental_insertion.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/delaunay_incremental_insertion.rs
  • src/core/algorithms/incremental_insertion.rs
  • src/core/triangulation.rs

Walkthrough

This pull request implements Phase 2/3 of local neighbor-pointer repair for triangulation incremental insertion: it adds a scoped repair API seeded by removed-cell frontiers, threads frontier results through local facet repair and post-removal orchestration, introduces an env-gated global-rebuild fallback, adjusts error shapes, and expands unit and integration tests.

Changes

Local Neighbor-Pointer Repair for Removed-Cell Insertion Path

Layer / File(s) Summary
Error Type Contracts
src/core/algorithms/incremental_insertion.rs
NeighborRebuildError::Unexpected changes from message: String to source: Box<InsertionError>. New NeighborWiringError::AsymmetricNeighbor variant added.
Local Neighbor Repair API & Helpers
src/core/algorithms/incremental_insertion.rs
New repair_neighbor_pointers_local plus private facet-identity and incidence helpers: affected set = seeds ∪ 1-hop, compute local facet incidence, selectively rewrite empty/incompatible neighbor slots, return changed-slot count.
Unit Tests: repair_neighbor_pointers_local
src/core/algorithms/incremental_insertion.rs
Mutual-neighbor finder, boxed-InsertionError preservation test, and tests covering missing-slot reconstruction, non-manifold rejection, stale/wrong-slot replacement, and scoped (no unseeded) repairs.
Frontier & Repair Orchestration
src/core/triangulation.rs
Adds LocalFacetRepairOutcome, repair_local_facet_issues_with_frontier, cells_for_local_facet_issue_repair, frontier helpers, and repair_neighbors_after_local_cell_removal that prefers local repair or uses env-gated global rebuild fallback.
Cavity & Hull Integration
src/core/triangulation.rs
Cavity insertion and hull extension now accumulate neighbor_repair_frontier from local facet repair outcomes, guard facet-sharing validity, and call centralized post-removal repair orchestration when removals occur.
Public API / Docs / Prelude
src/lib.rs, tests/prelude_exports.rs, docs/dev/debug_env_vars.md
Crate docs reference insertion prelude; prelude::triangulation::insertion re-exports TdsMutationError; DELAUNAY_FORCE_GLOBAL_NEIGHBOR_REBUILD documented; prelude tests exercise repair_neighbor_pointers_local.
Integration Tests & Guardrails
tests/delaunay_incremental_insertion.rs, src/core/triangulation.rs
Macro-driven multi-dimensional (2D–5D) guardrail tests asserting neighbor validity/symmetry, locate (with/without hint), conflict-region traversal, plus a regression test verifying frontier-seeded rewiring across formerly over-shared facets.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

bug

🐰 I hop where broken pointers lay,
I stitch the frontier, seed by way,
Local fixes, tested wide,
Env-gated paths for A/B to guide,
Small paws, precise repair — hooray!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'perf: repair insertion neighbors locally (#335)' directly summarizes the main change: replacing global neighbor rebuilds with local repair.
Description check ✅ Passed The description is directly related to the changeset, covering local repair implementation, edge case handling, env-var toggle, and prelude exposure.
Linked Issues check ✅ Passed All Phase 2/3 objectives from issue #335 are satisfied: local repair function implemented with affected-set logic, frontier handling, debug validations, env-var escape hatch, and integration tests.
Out of Scope Changes check ✅ Passed All changes align with issue #335 Phase 2/3 goals: local repair implementation, neighbor-error refactoring, frontier preservation, and prelude re-exports are all within scope.
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 perf/335-local-neighbor-repair

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.

@coderabbitai coderabbitai Bot added enhancement New feature or request rust Pull requests that update rust code breaking change geometry Geometry-related issues api topology labels May 8, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented May 8, 2026

Codecov Report

❌ Patch coverage is 85.42636% with 94 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.64%. Comparing base (83d9619) to head (3af15be).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/core/triangulation.rs 77.44% 53 Missing ⚠️
src/core/algorithms/incremental_insertion.rs 90.00% 41 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##             main     #366    +/-   ##
========================================
  Coverage   89.64%   89.64%            
========================================
  Files          58       58            
  Lines       52733    53335   +602     
========================================
+ Hits        47270    47811   +541     
- Misses       5463     5524    +61     
Flag Coverage Δ
unittests 89.64% <85.42%> (+<0.01%) ⬆️

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.

Copy link
Copy Markdown
Contributor

@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

🧹 Nitpick comments (4)
tests/delaunay_incremental_insertion.rs (1)

316-371: ⚡ Quick win

Add at least one adversarial near-boundary insertion per dimension in these new guardrail cases.

The new guardrail suite is useful, but it currently uses only well-conditioned interior points.

Proposed minimal extension (example pattern)
 test_local_neighbor_repair_guardrails!(
     2,
     [[0.0, 0.0], [4.0, 0.0], [0.0, 4.0]],
-    [[0.8, 0.8], [1.6, 0.7], [0.7, 1.5], [1.2, 1.2]]
+    [[0.8, 0.8], [1.6, 0.7], [0.7, 1.5], [1.2, 1.2], [1.0e-9, 1.3]]
 );

Apply the same idea for 3D–5D with one point near a facet/hyperfacet (tiny coordinate on one axis).

As per coding guidelines tests/**/*.rs: “Include adversarial inputs (near-boundary points, cospherical sets, degenerate simplices, large coordinates) alongside well-conditioned inputs in both tests and benchmarks”.

🤖 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/delaunay_incremental_insertion.rs` around lines 316 - 371, The test
suite invocations of the macro test_local_neighbor_repair_guardrails! only use
well-conditioned interior points; add at least one adversarial near-boundary
insertion for each dimension case (2,3,4,5) by adding a point with a very small
coordinate on one axis (e.g., epsilon on one coordinate to place it near a
facet/hyperfacet) to the inserted-points list passed into
test_local_neighbor_repair_guardrails! so each test includes one near-boundary
case alongside the existing interior points; update the parameter arrays for the
2D, 3D, 4D, and 5D macro calls accordingly.
src/core/triangulation.rs (1)

248-251: ⚡ Quick win

Cache the force-global-rebuild flag like the other env-gated switches.

Line 250 does a fresh env::var_os lookup on every repair-path call, while the nearby debug toggles already use OnceLock<bool>. Since this sits on the insertion hot path in a perf-focused change, caching it would remove repeated environment lookups for free.

♻️ Suggested change
+static FORCE_GLOBAL_NEIGHBOR_REBUILD_ENABLED: OnceLock<bool> = OnceLock::new();
+
 /// Returns whether local neighbor repair should be bypassed for regression isolation.
 fn force_global_neighbor_rebuild_enabled() -> bool {
-    env::var_os("DELAUNAY_FORCE_GLOBAL_NEIGHBOR_REBUILD").is_some()
+    *FORCE_GLOBAL_NEIGHBOR_REBUILD_ENABLED
+        .get_or_init(|| env::var_os("DELAUNAY_FORCE_GLOBAL_NEIGHBOR_REBUILD").is_some())
 }
🤖 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/core/triangulation.rs` around lines 248 - 251, The helper
force_global_neighbor_rebuild_enabled currently calls
env::var_os("DELAUNAY_FORCE_GLOBAL_NEIGHBOR_REBUILD") on every call; change it
to cache the result in a static OnceLock<bool> (like the other env-gated
toggles) so subsequent calls return the stored bool instead of performing
repeated env lookups; implement initialization that sets the OnceLock from the
env var (is_some()) and have force_global_neighbor_rebuild_enabled return the
locked value.
src/core/algorithms/incremental_insertion.rs (2)

2671-2730: ⚡ Quick win

Extract a shared BFS helper to eliminate duplication between the two validate_no_neighbor_cycles* functions.

validate_no_neighbor_cycles_for_cells (lines 2671–2730) replicates the entire BFS body of validate_no_neighbor_cycles (lines 2611–2669) verbatim; the only difference is how the sample set is sourced. A bug fix or improvement (e.g., the missing tracing::trace! at the end of the local variant that the global one has) would need to be applied in both places.

♻️ Proposed refactor — shared BFS helper
+#[cfg(debug_assertions)]
+fn validate_no_neighbor_cycles_impl<T, U, V, const D: usize>(
+    tds: &Tds<T, U, V, D>,
+    sample_cells: impl Iterator<Item = CellKey>,
+) -> Result<(), InsertionError>
+where
+    U: DataType,
+    V: DataType,
+{
+    let max_cells = tds.number_of_cells();
+    for start_cell in sample_cells.take(10) {
+        let mut visited: FastHashSet<CellKey> = FastHashSet::default();
+        let mut to_visit = vec![start_cell];
+        visited.insert(start_cell);
+        while let Some(current) = to_visit.pop() {
+            let cell = tds
+                .cell(current)
+                .ok_or(NeighborWiringError::MissingCell { cell_key: current })?;
+            let Some(neighbors) = cell.neighbors() else { continue; };
+            for &neighbor_opt in neighbors {
+                let Some(neighbor_key) = neighbor_opt else { continue; };
+                if neighbor_key == current {
+                    return Err(NeighborWiringError::SelfNeighbor { cell_key: current }.into());
+                }
+                if !tds.contains_cell(neighbor_key) {
+                    return Err(NeighborWiringError::MissingNeighborTarget {
+                        cell_key: current, neighbor_key,
+                    }.into());
+                }
+                if visited.insert(neighbor_key) {
+                    to_visit.push(neighbor_key);
+                    if visited.len() > max_cells {
+                        return Err(NeighborWiringError::NeighborWalkExceededCellCount {
+                            visited: visited.len(), total: max_cells,
+                        }.into());
+                    }
+                }
+            }
+        }
+    }
+    Ok(())
+}

 #[cfg(debug_assertions)]
 fn validate_no_neighbor_cycles<T, U, V, const D: usize>(
     tds: &Tds<T, U, V, D>,
 ) -> Result<(), InsertionError>
 where U: DataType, V: DataType,
 {
-    // ... (entire existing BFS body)
+    let result = validate_no_neighbor_cycles_impl(tds, tds.cells().map(|(k, _)| k));
+    tracing::trace!("validate_no_neighbor_cycles: neighbor walk terminated");
+    result
 }

 #[cfg(debug_assertions)]
 fn validate_no_neighbor_cycles_for_cells<T, U, V, const D: usize>(
     tds: &Tds<T, U, V, D>,
     sample_cells: &FastHashSet<CellKey>,
 ) -> Result<(), InsertionError>
 where U: DataType, V: DataType,
 {
-    // ... (entire existing BFS body)
+    validate_no_neighbor_cycles_impl(tds, sample_cells.iter().copied())
 }

As per coding guidelines: "Keep code simple and maintainable when multiple correct solutions exist."


4798-4968: ⚡ Quick win

Add dimension-generic tests for repair_neighbor_pointers_local covering D=3 through D=5.

All five new tests use D=2 exclusively. The existing test_repair_neighbors! macro demonstrates the pastey-based pattern for the global rebuild; the same approach should be applied to at least the basic local-repair case (reconstructs_missing_slot and reports_non_manifold_incidence).

♻️ Sketch of a dimension-generic macro
macro_rules! test_repair_neighbor_pointers_local {
    ($dim:literal, $initial_vertices:expr) => {
        pastey::paste! {
            #[test]
            fn [<test_repair_neighbor_pointers_local_reconstructs_missing_slot_ $dim d>]() {
                let vertices = $initial_vertices;
                let mut dt = DelaunayTriangulation::<_, (), (), $dim>::new(&vertices).unwrap();
                let tds = dt.tds_mut();
                let (cell_key, facet_idx, neighbor_key, _) =
                    first_neighbor_pair(tds).expect("adjacent cells");
                set_neighbor(tds, cell_key, facet_idx, None).unwrap();
                let repaired =
                    repair_neighbor_pointers_local(tds, &[cell_key], Some(&[neighbor_key]))
                        .expect("local repair");
                assert_eq!(repaired, 1);
                assert!(tds.is_valid().is_ok());
            }
        }
    };
}

test_repair_neighbor_pointers_local!(3, vec![
    vertex!([0.0,0.0,0.0]), vertex!([1.0,0.0,0.0]),
    vertex!([0.0,1.0,0.0]), vertex!([0.0,0.0,1.0]),
    vertex!([0.25,0.25,0.25]),
]);
// ... D=4, D=5 analogues

As per coding guidelines: "Dimension-generic tests MUST cover D=2 through D=5 whenever feasible; use pastey macros for per-dimension tests."

🤖 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/core/algorithms/incremental_insertion.rs` around lines 4798 - 4968, The
tests under repair_neighbor_pointers_local currently only cover D=2; add
dimension-generic variants for D=3..=5 by converting the core tests
(test_repair_neighbor_pointers_local_reconstructs_missing_slot and
test_repair_neighbor_pointers_local_reports_non_manifold_incidence) into a
pastey-powered macro that instantiates per-dimension tests, e.g. a macro named
test_repair_neighbor_pointers_local that takes $dim and an initial vertex list
and emits pasted test function names, and inside each generated test use
DelaunayTriangulation::<_,(),(),$dim>, first_neighbor_pair, set_neighbor, and
repair_neighbor_pointers_local exactly as the D=2 tests do; ensure you supply
appropriate minimal vertex sets for D=3..=5 so first_neighbor_pair returns
adjacent cells and keep the same assertions (repaired count and is_valid /
NonManifoldTopology match).
🤖 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/core/triangulation.rs`:
- Around line 4152-4177: The helper repair_neighbors_after_local_cell_removal
currently re-runs a full triangulation facet-sharing validation via
self.tds.validate_facet_sharing(), causing an unnecessary O(N·D) scan; remove
the duplicate validation and the facet_valid variable and associated debug
field/conditional check (including the early Err return of
CavityFillingError::InvalidFacetSharingAfterRepair { stage }) so this function
relies on the callers' prior global validation; keep only the localized debug
information that doesn't call validate_facet_sharing() and ensure any logic that
depended on facet_valid is removed or handled by the callers instead.

---

Nitpick comments:
In `@src/core/algorithms/incremental_insertion.rs`:
- Around line 4798-4968: The tests under repair_neighbor_pointers_local
currently only cover D=2; add dimension-generic variants for D=3..=5 by
converting the core tests
(test_repair_neighbor_pointers_local_reconstructs_missing_slot and
test_repair_neighbor_pointers_local_reports_non_manifold_incidence) into a
pastey-powered macro that instantiates per-dimension tests, e.g. a macro named
test_repair_neighbor_pointers_local that takes $dim and an initial vertex list
and emits pasted test function names, and inside each generated test use
DelaunayTriangulation::<_,(),(),$dim>, first_neighbor_pair, set_neighbor, and
repair_neighbor_pointers_local exactly as the D=2 tests do; ensure you supply
appropriate minimal vertex sets for D=3..=5 so first_neighbor_pair returns
adjacent cells and keep the same assertions (repaired count and is_valid /
NonManifoldTopology match).

In `@src/core/triangulation.rs`:
- Around line 248-251: The helper force_global_neighbor_rebuild_enabled
currently calls env::var_os("DELAUNAY_FORCE_GLOBAL_NEIGHBOR_REBUILD") on every
call; change it to cache the result in a static OnceLock<bool> (like the other
env-gated toggles) so subsequent calls return the stored bool instead of
performing repeated env lookups; implement initialization that sets the OnceLock
from the env var (is_some()) and have force_global_neighbor_rebuild_enabled
return the locked value.

In `@tests/delaunay_incremental_insertion.rs`:
- Around line 316-371: The test suite invocations of the macro
test_local_neighbor_repair_guardrails! only use well-conditioned interior
points; add at least one adversarial near-boundary insertion for each dimension
case (2,3,4,5) by adding a point with a very small coordinate on one axis (e.g.,
epsilon on one coordinate to place it near a facet/hyperfacet) to the
inserted-points list passed into test_local_neighbor_repair_guardrails! so each
test includes one near-boundary case alongside the existing interior points;
update the parameter arrays for the 2D, 3D, 4D, and 5D macro calls accordingly.
🪄 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: d13e8809-342e-4181-bbc9-ad450c720fc3

📥 Commits

Reviewing files that changed from the base of the PR and between 83d9619 and 5b9ee3b.

📒 Files selected for processing (6)
  • docs/dev/debug_env_vars.md
  • src/core/algorithms/incremental_insertion.rs
  • src/core/triangulation.rs
  • src/lib.rs
  • tests/delaunay_incremental_insertion.rs
  • tests/prelude_exports.rs

Comment thread src/core/triangulation.rs Outdated
- Avoid duplicate facet-sharing scans inside local neighbor repair after
  cell removal while preserving stage-specific failure when repair loops do
  not restore valid facet sharing.
- Cache the force-global-neighbor-rebuild diagnostic toggle after the first
  environment lookup.
- Extend local neighbor repair coverage across dimensions 2 through 5,
  including non-manifold incidence and near-boundary insertion cases.

Closes #335
@coderabbitai coderabbitai Bot added the bug Something isn't working label May 8, 2026
@acgetchell acgetchell merged commit f09d698 into main May 8, 2026
18 checks passed
@acgetchell acgetchell deleted the perf/335-local-neighbor-repair branch May 8, 2026 22:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api breaking change bug Something isn't working enhancement New feature or request geometry Geometry-related issues rust Pull requests that update rust code topology

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf: local neighbor-pointer repair for cells-removed insertion path (Phase 2/3)

1 participant