The base-first rewrite below WAS implemented exactly as planned, validated as byte-identical to the reference on all harness states (including synthetic edge cases for free-house covers, cannibals and finned intersections) — and benchmarked 5.5× slower than the cover-first original (554s vs 100s over the same captured states; in-solver finned-fish on the 11×11 pandiagonal went from ~11s to ~70s per call). It was reverted the same day.
Why the plan's assumption failed: on house-rich grids, guarantees are
small cell-sets scattered over a large grid, so almost every f-combination of
guarantees is pairwise disjoint and coverable — the disjointness and
cover-bound prunes never bind. Worse, since relevant houses each contain at
least one full guarantee, nearly every f-subset of houses admits f disjoint
bases: the pattern space really is ~C(houses, f), and any exactly-equivalent
algorithm must visit it. The enumeration order was never the bottleneck — the
guarantee-generalised fish semantics is.
Real options (owner decision required; only option 1 changes behaviour — option 2 is exact, it just needs per-pattern bookkeeping):
- Restrict bases to textbook fish (per base house, the candidate positions of the value, requiring ≤ f positions per base house) — collapses the pattern space to the classical one; loses some exotic-but-sound eliminations that backtracking currently compensates for anyway.
- Incremental fish: only re-examine patterns whose guarantees/houses changed since the last pass (requires per-pattern bookkeeping across rounds; exact but complex).
The harness (tests/fish_rewrite_harness.py, incl. synthetic states) remains
the regression net for any future attempt.
The original (now historical) plan follows.
Live profiling (py-spy, June 2026) during the pandiagonal latin-square tests showed
10/12 stack samples inside fish/finned_fish. The cause is the enumeration order:
- For each value the implementation iterates
combinations(relevant_houses, f)(cover sets) and only then looks for disjoint guarantees inside each union. - A pandiagonal n×n latin square has ~4n houses (n rows, n cols, 2n broken diagonals). For n=11 and f=4 that is C(44,4) ≈ 135,000 cover sets per value, ≈ 1.1M for finned fish (f+1 houses) — each paying a frozenset union plus a subset-filter over all of that value's guarantees, repeated every power round.
- For 9×9 sudoku (27 houses) the same code is fine; the blow-up is specific to house-rich grids.
Grid.cached_struct (June 2026) already removed the repeated setup cost
(relevant_urs_by_val is now computed once per rule/guarantee generation instead
of 6× per round). The combinatorial core is untouched and is what this rewrite addresses.
For value v, fish size f (fish):
- Bases: f pairwise-disjoint guarantees G₁..G_f for v, each
Gᵢ ⊆ U₁∪…∪U_f. Note: a single guarantee may span multiple cover houses — the constraint is containment in the union, not in a single house. - Covers: f houses (full-size
ElementsAtMostOncegroups,unique_rule_cells). - Eliminations:
- v removed from every cover-house cell outside
all_gts = ∪Gᵢ. - "Cannibal": v removed from cells inside
all_gtsthat lie in ≥ 2 cover houses.
- v removed from every cover-house cell outside
finned_fish differs only in: f+1 cover houses, at least one nonempty pairwise
house intersection required, eliminations restricted to cells in pairwise cover
intersections (outside all_gts), and the cannibal threshold is ≥ 3 houses.
Both are order-independent within one call: pattern detection reads only guarantees/houses (never candidates), candidates are only written. So the final candidate state after a full enumeration is a pure set-difference — this is what makes exact equivalence testing possible.
Flip the loops — enumerate disjoint guarantee combinations first, then solve the tiny set-cover instance for them:
for value v:
gts = guarantees for v (sorted by len)
cell_houses = precomputed cell -> houses-containing-it (cached_struct)
recurse(prefix=[], union=∅):
prune: if lower_bound_cover(union) > f: return # see below
if len(prefix) == f:
for cover in covers(union, f): apply eliminations
return
for gt in gts after prefix's last index:
if gt ∩ union: continue # disjointness
recurse(prefix + [gt], union | gt)
covers(cells, f): # exact set cover by ≤ f houses, branching on the
# least-covered uncovered cell; houses_per_cell ≤ 4
≈ 4^f worst-case branchings — trivial vs C(4n, f)
lower_bound_cover(cells): greedy/duplicate-aware bound, monotone in cells,
so it prunes prefixes early.
- Complexity becomes O(#viable disjoint base combos × small cover search) instead of O(C(houses, f) × guarantee filter). Disjointness + the cover bound prune the base recursion hard (guarantees overlap heavily in practice).
finned_fish: same recursion; cover search targets f+1 houses with the intersection requirement; eliminations per the finned rules above.- Duplicate (bases, covers) pairs must be deduped (a base set may admit several cover sets — the old code applied each; preserve that by enumerating all minimal covers, then applying eliminations per cover set found).
tests/fish_rewrite_harness.py (not collected by pytest; run as a script):
- vendors today's
fish/finned_fishverbatim aslegacy_fish/legacy_finned_fish(frozen reference, immune to later edits ofsolve_fish.py), - captures realistic mid-solve grid states by patching
atomic_solver.fish(patch the imported binding in atomic_solver, notsolve_fish.fish— a from-import binds at import time, so patching the source module is a silent no-op) while solving a mix of examples, - asserts exact
_candidatesequality between legacy and current implementation on every captured state, for fish and finned fish at f = 2, 3, 4, - times both implementations per state and reports totals.
Workflow for the rewrite:
python tests/fish_rewrite_harness.py→ must report all-equal (trivially true before the rewrite; run once to validate the harness itself).- Implement the new enumeration in
solve_fish.pybehind the same signatures. - Re-run the harness: equality must hold on every captured state; timing section quantifies the win (expect the pandiagonal states to dominate).
- Run the full pytest suite (the lsq/pandiagonal tests are the wall-clock check).
- Delete nothing from the harness — it stays as the regression net for future fish work.
- The guarantee-based fish here is more general than textbook fish (bases are guarantees, not candidate row/col positions); equivalence must be judged against this code, not against literature — hence the vendored reference.
- The cover search must allow guarantees spanning multiple cover houses (union containment only). A per-guarantee single-house assumption would silently lose patterns.
- Keep the f=2 fast path only if the harness timing still justifies it; the base-first scheme may make it redundant.
- An explicit enumeration cap was considered and rejected (2026-06-11): owner prefers completeness; the rewrite should make caps unnecessary.