Solve-pipeline refactor (integration branch — do not merge yet) - #69
Draft
ciaranm wants to merge 29 commits into
Draft
Solve-pipeline refactor (integration branch — do not merge yet)#69ciaranm wants to merge 29 commits into
ciaranm wants to merge 29 commits into
Conversation
Baseline instrumentation for the solve-pipeline refactor described in dev_docs/preprocessor-refactor.md. Behaviour-neutral; no solver code changes. - test-instances/proof_metrics.bash: deterministic fixed-matrix harness recording, per configuration, the supplemental-graph count and OPB/PBP line counts (plus solution count and nodes). Needs no VeriPB, so it is registered as the proof_metrics ctest unconditionally and runs in every lane -- under the sanitizers it doubles as a proof-emission smoke test across many feature combinations. - random_proof_sweep.bash: additionally emits a per-instance proof-size TSV (at $RPS_METRICS, default <workdir>/proof_sweep_metrics.tsv) alongside its verify pass. - dev_docs/preprocessor-refactor.md: the agreed refactor plan (unified SolveStep pipeline; OPB stays complete; S2a = static inertness elimination; phases 0-6). These give the before/after numbers that Phases 1-3 must hold byte-identical and that Phase 4 onward should shrink. 49/49 ctest; clean under ASan/UBSan. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pure refactor of solve_homomorphism_problem into a pipeline of SolveStep objects over a shared SolveContext, run by a driver that stops at the first step to conclude the problem. The top-level branches become steps -- PatternBiggerThanTargetStep, TargetLoopShortcutStep, CliqueShortcutStep -- and the model build + search becomes the terminal MainSolveStep, so "the search is just a step" is now structural. The OPB model emission stays inline as setup for now (it becomes its own step in Phase 3, when proof emission is co-located per step). The HomomorphismModel/searcher boundary and the would-be unified SolveState are untouched -- that is Phase 1b. No behaviour change: across the proof_metrics matrix and the 128-instance random sweep, all 274 emitted proof files (.opb + .pbp) are byte-identical to before; 49/49 ctest; clean under ASan/UBSan. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace calculate_n_shape_graphs and the parallel if(supports_*) build sequence in prepare() with a single ShapeGraphSpec plan (make_shape_graph_plan): the ordered list of supplemental graphs the model has, with each entry's slot count. max_graphs is now derived from the plan (number_of_shape_graphs) and prepare() builds by iterating it, so the count, the build order, and per-graph applicability live in one place and can no longer drift apart -- adding a supplemental graph is an entry in the plan plus a case in the build switch, not edits to a separate count function kept in step by a runtime consistency check. The per-graph proof emission stays inline in the build switch for now; co-locating it with each build (and splitting it out of this file) is Phase 3. The slot-index check is kept as a cheap defensive invariant. No behaviour change: across the proof_metrics matrix and the 128-instance random sweep, all 274 emitted proof files are byte-identical; 49/49 ctest; clean under ASan/UBSan. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The recoded bitset graphs and everything derived from them -- the per-graph adjacency rows, degrees, loops, labels, compressed pattern adjacencies, the directed flag, and the supplemental-graph names -- move out of the catch-all HomomorphismModel Imp into a cohesive ProcessedGraphsData struct (new processed_graphs_data.hh). This is the data the supplemental builders, the domain filters and the searcher all read; Imp keeps the genuinely model-private remainder (the Proof, vertex proof names, the clique-size caches, the less-than flags). This is the enabler for moving the supplemental build + proof code into its own translation unit (Phase 3b): that code will take ProcessedGraphsData rather than needing the whole Imp. The dimensions (max_graphs / pattern_size / target_size) stay on the model for now -- folding them in would mean non-inline accessors or churn in the searcher's hot loop, which rides the later searcher-side restructuring; PGD-taking code receives the stride as a parameter. No behaviour change: a field reorganisation only. 274/274 proof files byte-identical; 49/49 ctest; clean under ASan/UBSan. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ee functions
Introduces the "solver-proofs" middle layer (solver core -> HomomorphismProofs ->
generic Proof writer):
- HomomorphismProofs (homomorphism_proofs.{hh,cc}) owns the vertex index->name mapping
(moved out of the model's Imp) and the supplemental-graph proof derivations
(exact-path / distance-3 / extra-shape), reading ProcessedGraphsData directly. For
now it still calls today's generic Proof methods, so the NamedVertex marshalling
lives here rather than in the model; leaning the Proof API so these emit directly
(deleting the marshalling) is the next step.
- The supplemental graph *builds* become free functions over ProcessedGraphsData with
no proof dependency (supplemental_graphs.{hh,cc}).
- prepare()'s plan switch is now build-then-prove one-liners. model.pattern_vertex_for_proof
/ target_vertex_for_proof forward to HomomorphismProofs, so the searcher's many call
sites don't churn.
homomorphism_model.cc drops from ~1300 to ~1020 lines. No behaviour change: 274/274
emitted proof files byte-identical; 49/49 ctest; clean ASan/UBSan; new files are
clang-format clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The OPB model emission (CP variables, (local-)injectivity, adjacency / induced non-edge constraints, the preserved set, finalise, loop-fix) was the last big block of proof code living outside the middle layer -- it sat inline in solve_homomorphism_problem because it runs before the model is built. It now becomes HomomorphismProofs::emit_model, invoked by a new EmitProofModelStep (the pipeline's first step). To let the one HomomorphismProofs span the whole solve, it is now created and owned by the pipeline (a unique_ptr in solve_homomorphism_problem) rather than by the model: the emit-model step and the model's later supplemental / filter proofs share it. The model holds a non-owning pointer (passed to its constructor); HomomorphismProofs now holds a shared_ptr<Proof> (also sets up the clique-proof extension in 3c). With this, the solver core no longer emits any proof model directly -- HomomorphismProofs is the model's proof interface for everything bar the per-prune filter calls (which still go straight to Proof, to be routed later). No behaviour change: 274/274 proof files byte-identical; 49/49 ctest; clean under ASan/UBSan; clang-format clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- new clique_size_constraints.{hh,cc}: a CliqueSizeData struct (the lazily-computed
clique-size caches, carved out of the model's Imp) plus the clique-size *computation*
as free functions over CliqueSizeData + ProcessedGraphsData -- init_clique_size_data
and check_clique_compatibility, with find_clique as a file-local helper. No proof
dependency.
- the no-clique *proof* becomes HomomorphismProofs::prove_no_clique (it drives the
clique solver with proof extension, which is why the middle layer now holds the
shared_ptr<Proof> introduced in 3d). check_clique_compatibility calls it on a bound
violation.
- HomomorphismModel::_check_clique_compatibility is now a one-line forward; Imp drops
the clique cache fields in favour of `mutable CliqueSizeData clique_data;`, and the
constructor's cache setup becomes init_clique_size_data.
homomorphism_model.cc is down to ~830 lines (from ~1300 at the start of Phase 3). No
behaviour change: 274/274 proof files byte-identical (incl. proof_cliques /
proof_hom_clique); 49/49 ctest; clean under ASan/UBSan.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…yout prove_exact_path_graphs assumed exact-path graph g lived at slot g, and read slot 1 for the shared 2-path structure that justifies every exact-path graph. It now takes explicit (index, slot) pairs plus the exact-path-1 slot, so the supplementals can be renumbered when inert ones are eliminated (Phase 4b). The index (the path-count threshold) and the slot are now distinct; the derivation is keyed on the slot (what the searcher references). Byte-identical prep: with no elimination the pairs are (1,1),(2,2),... and the exact-path-1 slot is 1, so the emitted proof is unchanged -- 274/274 proof files byte-identical; 49/49 ctest; clean under ASan/UBSan. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The supplemental adjacency constraints the proof emits are nested: for a fixed head (p -> t, with neighbour q) the permitted target set shrinks as the path-count threshold grows (exact-path-k+1 subset of exact-path-k, both subset of distance3). A constraint with the same head but a smaller set syntactically subsumes the wider ones, so emitting only the strongest leaves the rest derivable -- and they are provably RUP-redundant, unlike the merely unused-in-this-search constraints, so this is sound regardless of instance. So emit only the set-minimal constraint per head: the exact-path prover keeps just the highest index that holds for each pattern edge and reports the edges it covered, and the distance-3 prover skips those (distance3 is always the widest). The degree/NDS pigeonhole still needs the per-graph constraint for the graph it reasons about; where that was elided, re-derive it on demand as a one-step weakening of the kept stronger constraint (cited by label), then delete it again -- cheaper than the original multi-line derivation even when it is needed. The original-graph and distance-2/k4 graphs (which carry no adjacency lines) are left untouched, matching the no-elision behaviour. A hidden --no-proof-supplemental-subsumption flag turns the whole thing off, emitting every constraint as before, for studying its effect (e.g. on proof trimming). Solution counts are unchanged and every proof still verifies (128-instance random sweep + 49 ctests + sanitizers); PBP shrinks ~25-39% on preprocessing-heavy configurations. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…helved) Update the preprocessor-refactor plan to match what shipped: Phase 4 became constraint-level subsumption of supplemental adjacency constraints (emit only the strongest per head; degree/NDS re-derives elided ones on demand), with the originally-planned graph-level inert/duplicate elimination shelved as unsound (the deadness is dynamic / RUP-load-bearing). Notes the hidden --no-proof-supplemental-subsumption toggle. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fold the constraint model, the (root) domains and the nogood store into a
single SolveState carried through the solve pipeline, rather than as locals
scattered across the model build and the two solvers. This is the prerequisite
for staged solving (Phase 6): builder / filter steps can grow the carried model
and bounded-search steps can accumulate nogoods between rounds.
- New gss/innards/solve_state.hh holding { model, domains, watches }.
- SolveContext carries a SolveState; the main solve step builds the model into
it (still lazily, after the cheap shortcut steps, so the max_graphs guard does
not fire before a shortcut can conclude).
- The searcher no longer owns its nogood store: it takes a Watches & supplied by
the caller. The sequential path passes state.watches; the threaded (terminal,
unbounded) path keeps a per-thread vector<Watches>, since threaded staging is
deferred.
Behaviour-neutral: 274/274 emitted proof files byte-identical, 49/49 ctest and
clean under ASan/UBSan (release and sanitize), and threaded SAT / counting /
triggered-restarts runs match the sequential results under the sanitizers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Run cheap preprocessing first and only build the expensive supplemental graphs if a bounded first search round does not conclude. New --staged flag (sequential, decision search): Stage 1 filters on the original graph only (degree + Hall, no NDS, no supplemental graphs) and searches under a small backtrack budget; if that does not conclude, the driver builds the supplemental graphs + NDS, re-filters, and continues unbounded. This skips the supplemental build/propagation cost -- and, under proof, their derivations -- on instances that crack cheaply. - HomomorphismModel::prepare() split: Stage-1 work stays in prepare(); the supplemental build+derive+degrees moves to a new build_supplemental_graphs(), which prepare() still calls itself when not staged (unstaged byte-identical). initialise_domains() gains a stage1 flag (g=0 only, no NDS); new tighten_domains_with_supplementals() applies the deferred supplemental degree + NDS filtering to the existing domains, emitting only the new prunings. - SequentialSolver drives the two stages over the carried SolveState (Phase 1b): Stage 1 under its own fixed-budget schedule; at the first restart it builds the supplementals (derived mid-proof at the level-0 restart boundary) and tightens, then switches to the user's schedule. Nogoods persist in state.watches. - Full proof support: staged proofs verify under VeriPB; instances that conclude in Stage 1 emit no supplemental derivations (trident decision PBP 2168 -> 136). - Guards: --staged is sequential-only, and not yet combined with counting (a mid-enumeration restart would recount explored subtrees). Hidden --staged-first-round-backtracks tunes the budget (default 100; tests use a tiny value to force the transition). Validation: unstaged proofs byte-identical (274/274); staged proof sweep 160/160 verify; the random correctness sweep checks staging is satisfiability-neutral vs unstaged; 49/49 ctest, release + ASan/UBSan. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The cheap concluding steps (pattern-too-big, target-loop, clique) already run before the supplemental builders, but the loop-cancelled adjacency derivations (loop_fix_adjacencies) were emitted at the tail of emit_model -- the very first step -- so even a trivial refutation carried them. Under --induced every non-edge constraint has the target in its permitted set, so all of them get rewritten: the induced c3 -> trident refutation (pattern bigger than target, a one-line injectivity pigeonhole that cites no adjacency constraint) emitted 2430 dead @adj rup lines before concluding. Move the derivation out of emit_model into a new HomomorphismProofs::derive_loop_fixed_adjacencies(), called at the start of MainSolveStep -- after the cheap steps, before prepare()'s first @adj-citing pol. Nothing is emitted to the proof between the two points for an instance that reaches search, so its proof is byte-identical; an instance that concludes early omits the derivation. The OPB is untouched. induced_unsat (c3 -> trident, --induced) PBP 2439 -> 9, still verifies; 273/274 proof-metrics + sweep files byte-identical; all 49 ctests pass (release + ASan/UBSan), including the random proof sweep and the cake pipeline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Staged counting double-counted: a 22-solution instance reported up to 41, re-counting in Stage 2 the solutions already counted in Stage 1. The cause was not a missing mechanism but a disabled one. might_have_watches(), which gates the whole nogood/watch machinery (post_nogood, the watch-table allocation, and the watch propagation in propagate()), returned params.restarts_schedule->might_restart() -- keyed on the *user's* schedule. Counting defaults to --restarts none, so might_have_watches was false and every post_nogood at the Stage-1 -> Stage-2 transition was a no-op. With no resumption nogoods, Stage 2 re-explored the whole tree from the top, and since sequential counting does no per-solution dedup, it counted every solution again. Staging always performs at least one restart (the transition), independently of the user's schedule, so it must enable watches: might_have_watches() now also returns true when params.staged. The restart-resumption nogoods then cover the Stage-1 region exactly, as they already do for ordinary restarts. This only affects staged counting (staged decision defaults to Luby, so might_have_watches was already true there; non-staged is unchanged), so all 274 existing proofs stay byte-identical. The staged-count guard is relaxed from "staged + count" to "staged + count + proof" -- the non-proof case now works; under proof it stays blocked (logging an enumeration across a restart is not yet handled, same restriction as counting with restarts). random_homomorphism_test now checks staged counting against the brute-force oracle over the 1500-instance sweep (tiny first-round budget forces the transition mid-enumeration): the distinct-set check catches a missed solution, the solution_count check catches a double-counted one. 49/49 ctest release + ASan/UBSan; 274/274 proofs byte-identical. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The homomorphism adjacency / supplemental-graph derivations in Proof share three maps -- the adjacency-line labels, their numeric line ids, and their permitted target sets -- all keyed by (g, p, q, t) index. Group them into an AdjacencyProofLines struct and expose it via Proof::adjacency_proof_lines(). This is a pure, byte-identical refactor (274/274 proofs unchanged; 49/49 ctest). Its point is to decouple the cluster: with the shared state behind one accessor, the derivation methods that read and write it can migrate out of Proof into the HomomorphismProofs middle layer one at a time, each sharing this object, rather than the whole ~600-line cluster having to move in a single atomic step. The end state is a generic Proof that emits PB constraints without knowing about pattern or target vertices (see dev_docs/preprocessor-refactor.md, Phase 3 Option 2). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ency managers
Add the generic low-level proof-emission primitives to Proof -- emit_proof_line
(write a constraint line, bump and return the line number), emit_proof_directive
(write a non-constraint line: comment, setlvl/wiplvl, del id; no counter),
current_proof_line, variable_name(p, t), and is_locally_injective -- so the
homomorphism derivations can emit through Proof rather than reaching into its
internals.
Move the first four adjacency-management methods out of Proof into the
HomomorphismProofs middle layer, each emitting via the new primitives over the
shared AdjacencyProofLines cache:
- loop_fix_adjacencies -> derive_loop_fixed_adjacencies (already the caller)
- weaken_supplemental_adjacency -> inlined into ensure_supplemental_adjacency,
which now builds the line straight from indices and the target row, deleting
the vector<NamedVertex> target_set marshalling
- forget_supplemental_adjacency -> inlined into forget_transient_supplemental_adjacencies
- adjacency_line_exists -> a direct cache lookup in the middle
Proof no longer knows about loop-cancellation, subsumption weakening, or transient
deletion; these now live where the vertex names and graph data already are.
Byte-identical: 274/274 proofs unchanged; the migrated transient-weaken path
(@g..adj.. ia .. : @kept + del id) verifies under VeriPB on dense instances;
49/49 ctest release + ASan/UBSan.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…iddle Move create_exact_path_graphs -- the largest supplemental derivation, the intricate setlvl / pol s+ / ia chain that proves a G^[gx2] adjacency constraint -- out of Proof and into HomomorphismProofs::emit_exact_path_graph. The middle's prove_exact_path_graphs now builds plain index vectors (between_p_and_q, n_t, d_n_t, two_away_from_t) instead of the vector<NamedVertex> / vector<pair<NamedVertex, vector<NamedVertex>>> it used to marshal for the Proof call, and emits through the generic primitives. To support it, Proof gains read accessors for the model labels the derivation cites -- injectivity_label, locally_injective_label, at_most_one_value_label -- and a generic dedup cache (cached_proof_line / cache_proof_line). Proof no longer contains any exact-path-specific logic or the NamedVertex-vector signature. Byte-identical: 274/274 proofs unchanged; the supplemental / distance3 / staged configs (which derive these) verify under VeriPB via the sweep and cake tests; 49/49 ctest release + ASan/UBSan; clang-format clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…middle Move the three distance-3 (G^3) adjacency derivations -- the direct-edge, single-intermediate, and two-vertex-path variants -- out of Proof into HomomorphismProofs (emit_distance3_graph_distance_1 / _distance_2 / emit_distance3_graph). prove_distance3_graphs now finds the pattern path and builds the d1/d2/d3-from-t target sets as plain index containers (set<int> gives the same order as the old set<NamedVertex>, since the name is a function of the index) instead of marshalling NamedVertex, and emits through the generic primitives. These variants needed no new Proof API beyond what the exact-path migration already added. With this, every homomorphism-exclusive supplemental derivation lives in the middle layer; Proof keeps only the cross-solver / model-driven proof methods (create_adjacency_constraint is shared with MCS; the degree/NDS/Hall filter proofs are model-driven). Byte-identical: 274/274 proofs unchanged; the reachable distance-3 variants (direct edge and the full two-vertex path) verify under VeriPB on dense instances; 49/49 ctest release + ASan/UBSan; clang-format clean. (The single-intermediate variant is unreachable while exact-path graphs are on -- a distance-2 pair always has a 2-path, so exact-path covers it and distance3 skips it -- so it is a faithful translation rather than separately exercised.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
create_adjacency_constraint and start_adjacency_constraints_for are shared with the common-subgraph (MCS) solver, so they cannot simply move out of Proof. To get the subgraph solver clean without disturbing the others, give the homomorphism solver its own path -- HomomorphismProofs::emit_adjacency_constraint, which builds the @adj OPB line and records it (with its permitted set) in the shared adjacency cache via the new generic emit_model_constraint / emit_model_comment primitives -- and leave Proof::create_adjacency_constraint / start_adjacency_constraints_for in place for MCS (and clique) to keep using. emit_model no longer marshals NamedVertex for its adjacency constraints. The homomorphism solver now drives all of its supplemental and adjacency proof emission through the middle layer; the only Proof methods it still calls are the generic model-building / searcher-logging API shared by every solver. Byte-identical: 274/274 proofs unchanged; the MCS and clique proof tests (proof_mcs, proof_mcs_connected, proof_mcs_clique, proof_clique) still pass via the retained API; 49/49 ctest release + ASan/UBSan; clang-format clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…middle Move hack_in_shape_graph (the assert-rule adjacency line for an --shape extra shape graph) out of Proof into HomomorphismProofs::emit_shape_graph; prove_extra_shape now builds an index vector and emits through the generic primitives. It is homomorphism-only, so nothing is left behind. Byte-identical: 274/274 proofs unchanged; extra_shapes_test passes and an --shape proof still emits and verifies; 49/49 ctest release + ASan/UBSan. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Owner
Author
|
@ArthurGontierPro Give this branch a go, in particular using the "--staged" command line option. It restructures some of how we do the preprocessing and seems to produce less noisy proofs. |
Building on the subsumption elision (Phase 4) and transient ensure/forget (Phase 5) economy, defer the kept-strongest supplemental adjacency derivations until first use rather than emitting them all during model build. A supplemental for antecedent (p,t) is needed only when x_p_t's value is decided by adjacency during search -- assigned true (forward-checking use) or removed from dom(p) by forward-checking (the branch-consequence a later Hall-set confinement cites) -- plus the root degree/NDS reads, so a head never touched in search is never emitted. This is orthogonal to subsumption (which picks the strongest per head) and composes with it. The prove_*_graphs builders keep their analysis (the subsumption covered set and kept-slot map) but register a pending closure per kept constraint instead of emitting; materialise_adjacency_for runs them on first use, driven from the searcher's guessing / unit_propagating (assignment) and propagate_adjacency_ constraints (per removed value, gated on has_pending_supplementals). The transient ensure_supplemental_adjacency now materialises the kept constraint it weakens from, since it may still be pending when a degree/NDS check reads it. The OPB is byte-identical and the search tree is unchanged. Mid-search materialisation scratches one level above the search (active_level+1, since wiplvl N wipes every level >= N), emits the persistent @Label at level 0, and restores the active level once per batch. Verified: ctest 49/49 (release and sanitize, incl proof_*, cake_* and random_proof_sweep); a 300-instance distance-3/locally-injective/counting sweep all verify; OPB byte-identical. On top of subsumption it roughly halves the supplemental count and PBP line count again (e.g. seed-31 distance-3: 279 -> 117 supplementals, 3479 -> 1423 lines). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… middle Route Proof::initial_domain_is_empty through HomomorphismProofs so the domain-initialisation filter conclusions log via the middle layer rather than calling the generic Proof directly. It is a comment-only line, so the six callers in homomorphism_model.cc now go through _imp->proofs and Proof loses the method -- a first step towards moving the remaining model filter proofs (incompatible_by_degrees/_nds/_loops, the Hall set/violator) off the generic bottom, per dev_docs/preprocessor-refactor.md Phase 3 Option 2. Byte-identical: unstaged corpus (137 opb / 137 pbp) unchanged, 49/49 ctest. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Route Proof::emit_hall_set_or_violator through HomomorphismProofs. The middle method takes pattern/target vertex indices (not pre-built NamedVertex vectors) and builds the comment and the pol over each lhs vertex's at-least-one-value constraint and each rhs target's injectivity constraint via the generic primitives, so the homomorphism naming stays in the middle layer. Both callers move across: the root global-degree Hall filter and the post-initialisation nonshrinking check in homomorphism_model.cc, and the search-time all-different filter in cheap_all_different.cc, which now reaches the middle via model->proofs() (so its proof shared_ptr is only the on/off switch and drops out of the templated worker). Adds the at-least-one-value label accessor to Proof (the sibling of at-most-one-value); Proof loses emit_hall_set_or_violator and its last NamedVertex-vector filter method. Byte-identical: unstaged corpus (137 opb / 137 pbp) unchanged, 49/49 ctest release + sanitize (the search-time Hall path is exercised by the 128-instance proof_random_sweep, including counting). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… middle Move the last homomorphism-specific filter derivations off the generic Proof: incompatible_by_loops, incompatible_by_degrees and incompatible_by_nds (with their need_elimination helper) now live in HomomorphismProofs, taking vertex indices and emitting through the generic primitives over the shared adjacency cache and the (local-)injectivity / at-least-one-value labels. The eliminations map -- which records, per already-refuted (p,t), the line that refuted it, for the NDS pigeonhole to cite -- moves into the middle with them. It was homomorphism-only bookkeeping: the sole other writer, Proof::create_forbidden_assignment_constraint, is called only by the common-subgraph solver, which never runs an NDS check and so never reads the map back -- that write was dead, and is dropped. A solve is either homomorphism or common-subgraph, never both, so nothing shares the map across the split. With this, every reader of the adjacency-line cache is in the middle layer (the degree/NDS pigeonholes were the last bottom consumers), setting up the eventual move of AdjacencyProofLines ownership out of Proof. Byte-identical: unstaged corpus (137 opb / 137 pbp) unchanged, 49/49 ctest release + sanitize (the nds, induced-unsat and loopy-supplemental configs exercise all three; proof_mcs* covers the forbidden-assignment change). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the AdjacencyProofLines struct and its instance out of the generic Proof and into HomomorphismProofs, which now owns it outright: the middle's model emission and supplemental / degree / NDS derivations read and write a private _adjacency member instead of reaching through Proof::adjacency_proof_lines(). This is the payoff of the step-3 filter migrations: with every reader of the cache now in the middle, the only remaining bottom writer was the common-subgraph solver's create_adjacency_constraint, whose writes nothing in the MCS path ever read (MCS runs no degree/NDS pigeonhole) -- so those writes are dropped as dead, and Proof loses the struct, the member and the adjacency_proof_lines() accessor. The generic Proof no longer carries any homomorphism adjacency state. Byte-identical: unstaged corpus (137 opb / 137 pbp) unchanged, 49/49 ctest release + sanitize (proof_mcs* covers the create_adjacency_constraint change). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Long-lived integration branch for the solve-pipeline refactor. Plan and rationale in
dev_docs/preprocessor-refactor.md.Not for merging phase-by-phase. Phases accumulate as commits here; the branch merges to
masteronly once the refactor (or a meaningful chunk) is complete. Kept as a draft PR so CI runs on every push (the workflow only triggers on PRs tomaster). Head6a5d7a4, all 6 CI lanes green.Motivation (the three problems this targets)
Status — the canonical phases are complete
proof_metrics).SolveSteppipeline; search becomes the terminal step.SolveState(model + root domains + nogood store carried through the pipeline).ShapeGraphSpecplan as the single source of truth for the supplemental graphs.ProcessedGraphsData; 3bHomomorphismProofsmiddle layer; 3c clique-size split; 3d OPB emission into the middle layer).homomorphism_model.cc~1300 → ~830 lines.distance3 ⊇ exact-path-1 ⊇ exact-path-2 …, same head), so emit only the strongest per head; a degree/NDS check that needs an elided one re-derives it as a one-step weakening of the kept constraint and deletes it again. Hidden--no-proof-supplemental-subsumptionrestores full emission. ~25–39% PBP shrink on preprocessing-heavy configs, counts unchanged.loop_fix_adjacencies) were emitted at the head ofemit_model, so even a trivial refutation carried them. Deferred to the start of the search step, after the cheap concluding steps. Byte-identical for any instance that reaches search; an early conclusion now emits none (e.g. inducedc3 → tridentPBP 2439 → 9).--staged). A cheap first round (original graph only: degree + Hall, no NDS, no supplementals); only if a bounded search round does not conclude does it build the supplementals + NDS, re-filter the domains, and continue unbounded. Full proof support — the supplementals are derived mid-proof at the level-0 restart boundary. Sequential; decision and (see below) counting.Follow-on work, also on this branch
might_have_watches()was keyed on the user's restart schedule, so under the--restarts nonethat counting defaults to, the transition's resumption nogoods were never posted and Stage 2 re-explored the tree. One-line fix (params.staged || …), validated against the brute-force oracle over the random sweep. Under proof it is still gated (logging an enumeration across a restart — the same restriction as counting with restarts).ProofAPI (Phase 3 Option 2). The middle layer used to marshalvector<NamedVertex>to call hom-specificProofmethods.Proofnow exposes generic emit primitives (emit_proof_line/emit_proof_directive,emit_model_constraint/emit_model_comment,variable_name, injectivity/at-most-one label and dedup-cache accessors) plus a sharedAdjacencyProofLinescache, and every homomorphism-exclusive derivation (adjacency, exact-path, distance-3, extra-shape, loop-fix, transient weaken/forget) is emitted fromHomomorphismProofsusing plain indices.create_adjacency_constraint/start_adjacency_constraints_forare kept inProoffor the common-subgraph and clique solvers, which still use them; the homomorphism solver drives its own clean path.Deferred / not done (a reviewer's map of the edges)
core idmonotonicity bug (the same blocker as re-adding solution-blocking-nogood deletion, issue Enumeration proofs are linear in #solutions; re-add blocking-constraint deletion once upstream VeriPB bug is fixed #59).--stagedthe default (wants budget tuning first).incompatible_by_degrees/_nds,emit_hall) and the clique-size proof still use the oldProofAPI — the latter is entangled with the shared clique-solver proof path (its translation state lives inProofbecause the clique solver's own proof methods read it), so it is left on the old API deliberately.Guardrail
Phases 1–3 are byte-identical (verified by the random correctness + proof sweeps, the cake
gss → veripb → cakepipeline, and theproof_metricsmatrix — 274 emitted.opb/.pbpfiles). Phases 4–6 change proofs deliberately, verified by re-running the sweeps under VeriPB with solution counts unchanged. Each Phase-3-Option-2 step is again byte-identical — it moves where a proof line is emitted, not what. Every push is green on all 6 CI lanes (ubuntu 24.04 / 26.04, macOS 15 / 26, Sanitize, Proof coverage).🤖 Generated with Claude Code