Skip to content

Replace scalar Dijkstra with measured Pareto label-setting search (#1076)#1084

Merged
isPANN merged 1 commit into
1075-growth-domainfrom
1076-pareto-search
Jul 13, 2026
Merged

Replace scalar Dijkstra with measured Pareto label-setting search (#1076)#1084
isPANN merged 1 commit into
1075-growth-domainfrom
1076-pareto-search

Conversation

@isPANN

@isPANN isPANN commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Implements M3 (F3b) of the Symbolic Growth Domain & Pareto Search milestone. Independent of the growth domain (measured labels are numeric; the pre-flight guard uses plain Expr::eval). References #1076.

Closes #788.

The bug

ReductionGraph's scalar Dijkstra evaluated each edge's cost at the size accumulated along the path — a path-dependent cost, which violates Dijkstra's assumptions. When two paths reached the same node, only the cheaper-so-far label's size was retained, so a cheaper-but-larger intermediate could poison downstream choices and miss the path with the smallest final target.

Design

Pareto label-setting kernel (pareto_search, generic over a new PathLabel trait):

  • Per-node bag = antichain of non-dominated labels, each with a predecessor pointer for path reconstruction.
  • Frontier explored in ascending cost() order (early branch-and-bound bound).
  • Deterministic safety caps: hop cap 16, per-node bag cap 32 with a deterministic (cost, hops, node-name path) tie-break (never iteration-order truncation). Edges visited in deterministic (target-name, target-variant) order.
  • Returns a deterministically ordered Pareto front; find_cheapest_path* keep their signatures and return the front's best element.

Two label domains:

  • CostLabel — scalar formula label that reproduces Dijkstra behavior for the existing PathCostFn functions (used by find_cheapest_path*).
  • MeasuredLabel — the concrete-instance label. Its extend runs a four-part pruning stack, in order:
    1. Symbolic pre-flight guard — evaluate the edge's overhead formula at the measured current size (computed in f64, so a 2^num_vertices prediction is flagged rather than overflowing usize); over budget ⇒ None without executing. This makes OOM structurally impossible during selection.
    2. Execute + measure the real constructed target size; over budget ⇒ None. A caught panic from a reduction whose preconditions the instance violates prunes that edge.
    3. Branch-and-bound against the best completed path's final size.
    4. Componentwise measured-size dominance — heuristic; the exhaustive flag disables only this guard (1–3 stay sound).

MeasuredPath carries the constructed reduction chain (Rc), so the winner's downstream solve/witness extraction reuses it with no re-execution. Default size budget 10^7, overridable.

Behavioral changes

  • ILPSolver::best_path_to_ilp now calls ReductionGraph::find_measured_best_path_to_name, ranking ILP variants by real measured size instead of step count + formula. It returns a MeasuredPath, and try_solve_via_reduction solves/extracts from the already-built chain (no re-reduction). Aggregate-path detection (for the WitnessPathRequired error) still uses a formula existence check.
  • find_cheapest_path / find_cheapest_path_mode now run the generic kernel with CostLabel (old dijkstra deleted); signatures and observed behavior unchanged.
  • find_all_paths / find_paths_up_to* untouched. Growth domain / canonical.rs untouched.

Verification (exact results)

  1. [Enhancement] ILP path selector over-prioritizes step count over final output size #788 known-answer (test_hamiltoniancircuit_to_ilp_measured_optimum_788): HamiltonianCircuit on the prism graph (6 vertices, 9 edges). The literal chain quoted in [Enhancement] ILP path selector over-prioritizes step count over final output size #788 (HC → HP → ConsecutiveOnesSubmatrix → ILP = 60) no longer exists on the current reduction graph. The measured search now selects the current measured optimum HC → LongestCircuit → ILP<bool>, total 232 (num_constraints=127, num_vars=105), ahead of RuralPostman → ILP<i32> (366) and TravelingSalesman → ILP<bool> (768). The test pins this, proving ranking by measured final size. Note: on today's graph the old formula-selector coincidentally also lands on 232 (the LongestCircuit overhead formula is tight), so the genuine behavioral proof of the path-dependent-cost fix is the diamond negative control below.
  2. OOM pre-flight guard (test_oom_preflight_guard_highlyconnecteddeletion): a dense 64-vertex HighlyConnectedDeletion routed at the 2^num_vertices edge (highlyconnecteddeletion_ilp). Guard 1 evaluates 2^64 ≫ 10^7 and skips; the search returns None in ~50 µs (asserted < 1s), never starting the exponential cluster enumeration. Without the guard this OOMs.
  3. Existing behavior preserved: cargo test fully green — lib 5265 passed, integration 75 passed, CLI 495 passed — including the closed-loop reduce tests routing through find_cheapest_path.
  4. Negative control (test_negative_control_diamond_pareto_beats_scalar): a hand-built 4-node diamond (test-only ReductionGraph::from_test_edges). P1 (S→M→T) has the lower first-edge cost but a larger measured intermediate at M; P2 (S→P→M→T) has a higher first-edge cost but a strictly smaller final measured size. Scalar-cost selection (find_cheapest_path) returns P1; the Pareto search keeps both incomparable routes into M and returns P2 (final size 6). This is the behavioral proof the old Dijkstra could not pass.

cargo clippy --all-targets --features ilp-highs -- -D warnings clean, CLI clippy clean, cargo fmt --check clean.

Notes for reviewers

  • The MeasuredLabel executes reductions during exploration; a reduction that panics on an infeasible intermediate is treated as an infeasible edge (caught, pruned). A thread-local silencer keeps these expected/recovered panics off stderr without affecting other threads.
  • The bag-cap (32) truncation branch is defensive and not exercised by tests (constructing >32 pairwise-incomparable labels at one node is contrived); the core guards, dominance, B&B, both label domains, and the measured search are all covered.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EgxSbn5gwizTBkC22eyWXR

)

Implements M3 (F3b) of the Symbolic Growth Domain & Pareto Search milestone.

The scalar Dijkstra in `ReductionGraph` had a path-dependent-cost hole: when
two paths reached the same node, only the cheaper-so-far label's size was kept,
so a cheaper-but-larger intermediate state could poison downstream choices
(issue #788). This replaces it with a generic multi-label Pareto search plus a
measured concrete-instance label domain.

- New `PathLabel` trait (`extend` + `dominates` + `cost`) and a generic
  `pareto_search` kernel over per-node antichain bags with predecessor pointers,
  branch-and-bound, deterministic safety caps (hop cap 16, bag cap 32 with a
  deterministic tie-break), and a deterministically ordered Pareto front.
- `CostLabel`: scalar formula label reproducing Dijkstra behavior for the
  existing `PathCostFn` cost functions; `find_cheapest_path*` keep their
  signatures and now run the kernel with it.
- `MeasuredLabel` (`src/rules/pareto.rs`): the concrete-instance label. Its
  `extend` runs a four-part pruning stack in order — (1) symbolic pre-flight
  guard (evaluated in f64 so a `2^num_vertices` prediction is refused without
  executing, making OOM structurally impossible), (2) execute + measure the real
  target size, (3) branch-and-bound, (4) componentwise measured-size dominance
  (disabled by the `exhaustive` flag; guards 1-3 stay sound). A caught panic from
  a reduction whose preconditions the instance violates prunes that edge.
- `MeasuredPath` carries the constructed reduction chain (via `Rc`) so downstream
  solve/witness extraction reuses it without re-executing.
- `ILPSolver::best_path_to_ilp` now uses `find_measured_best_path_to_name`,
  ranking ILP variants by real measured size instead of step count / formula.

Verification (all green): #788 known-answer test (HC on the prism graph selects
the measured optimum), OOM pre-flight guard test (64-vertex HighlyConnectedDeletion
refused in <1ms, exponential construction never started), and a hand-built diamond
negative control where scalar cost selection commits to P1 while the Pareto search
returns the strictly-better-final-size P2.

Closes #788.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EgxSbn5gwizTBkC22eyWXR
@isPANN isPANN changed the base branch from main to 1075-growth-domain July 13, 2026 08:33
@codecov

codecov Bot commented Jul 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.24460% with 32 lines in your changes missing coverage. Please review.
✅ Project coverage is 98.00%. Comparing base (b198ddc) to head (b4f0753).
⚠️ Report is 1 commits behind head on 1075-growth-domain.

Files with missing lines Patch % Lines
src/rules/graph.rs 88.46% 30 Missing ⚠️
src/rules/pareto.rs 99.01% 1 Missing ⚠️
src/solvers/ilp/solver.rs 97.14% 1 Missing ⚠️
Additional details and impacted files
@@                 Coverage Diff                  @@
##           1075-growth-domain    #1084    +/-   ##
====================================================
  Coverage               98.00%   98.00%            
====================================================
  Files                    1044     1046     +2     
  Lines                  107195   107670   +475     
====================================================
+ Hits                   105053   105525   +472     
- Misses                   2142     2145     +3     

☔ View full report in Codecov by Harness.
📢 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.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@isPANN isPANN merged commit b4f0753 into 1075-growth-domain Jul 13, 2026
7 checks passed
@isPANN isPANN deleted the 1076-pareto-search branch July 13, 2026 08:43
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.

[Enhancement] ILP path selector over-prioritizes step count over final output size

1 participant