Cut cluster scout from optimizer

This commit is contained in:
Meta Vens 2026-04-23 17:00:24 +01:00
commit 7b97a93e6c
10 changed files with 47 additions and 164 deletions

View file

@ -65,7 +65,7 @@ What landed:
- the experimental `hybrid` path and its geometry-prior wiring have been removed
- `tile_graph.py` no longer carries geometry-prior fields through `TileGraphModel` or tile-graph unary scoring
- `tile-graph` now skips source clustering entirely and uses only edge analysis plus direct per-cell source summaries during unary scoring
- `analysis.py` now has separate prep paths: continuous gets edge plus cluster analysis, while tile-graph gets an edge-only scout report
- `analysis.py` now has separate prep paths, but both are edge-only after the first optimizer cut; the continuous k-means cluster scout was removed because its only production job was turning coarse color partitions into fake edge guidance
- `source_reference.py` was split so tile-graph could have its own lean prep contract, but the next cut removed tile-graph's need for any separate source-reference object at all
- `TileGraphModel` is now solver-only state; cache metadata and build diagnostics live in a separate `TileGraphBuildStats` sidecar
- `cli.py` now exposes only `continuous` and `tile-graph` as reconstruction engines
@ -110,11 +110,17 @@ The new map does for `continuous.py` what `docs/tile-graph-algorithm-map.md` doe
- names the real state variables the machine carries (`uv0_t`, `initial_representative_t`, `source_lattice_reference`, `source_reliability_t`, `snap_t`)
- explains the regular UV lattice, representative portrait, source lattice portrait, source-first snap, relaxed handoff, and discrete refine stages in plain language
- identifies the main contradictions still living in the optimizer, especially:
- `optimize_uv_field(...)` no longer actually optimizing the UV field
- the machine maintaining two overlapping portraits of the same lattice and spending much of its complexity mediating between them
- real edge evidence still being mixed with coarse k-means cluster boundaries
- adjacency / motif / line structure being expressed repeatedly in several slightly different dialects
The first optimizer cut landed immediately after that map:
- `optimize_uv_field(...)` was renamed to `optimize_lattice_pixels(...)`
- the unused `_exemplar_colors(...)` helper and its test were removed
- continuous analysis stopped building a k-means `cluster_map`
- `continuous.py` now uses only real source edges for guidance instead of blending in coarse cluster boundaries
- the pinned cleaned-badge continuous smoke run under `artifacts/optimizer-cut-v1-badge-126/` still behaves coherently after the cut: snap scores `0.08100`, final scores `0.07535`, and the generated preview is `diagnostics/output-preview.png`
The next optimizer simplification pass should use that map as its cutting checklist instead of guessing from scattered helper names.
## High-value regression case

View file

@ -16,7 +16,7 @@ Where did the machine start negotiating with itself too much?
The current optimizer machine is:
`source image -> edge/cluster scout -> regular UV grid -> soft representative portrait + hard source lattice portrait -> source-first snap -> relaxed candidate field -> greedy discrete refine -> keep snap if refine made things worse`
`source image -> edge scout -> regular UV grid -> soft representative portrait + hard source lattice portrait -> source-first snap -> relaxed candidate field -> greedy discrete refine -> keep snap if refine made things worse`
Or, in pictures:
@ -67,37 +67,32 @@ This fixes:
Every later judgment is conditioned on that ruler. If it is wrong, the optimizer is making careful decisions against the wrong graph paper.
## Stage 1: The scouts walk the mural
## Stage 1: The edge scout walks the mural
Functions:
- `analyze_continuous_source(...)` in `src/repixelizer/analysis.py`
- `_compute_edge_map(...)`
- `_kmeans(...)`
Output:
- `ContinuousSourceAnalysis.edge_map`
- `ContinuousSourceAnalysis.cluster_map`
Meaning:
The optimizer sends out two scouts:
The optimizer sends out one scout:
- one chalks the cliff faces where luminance or alpha changes sharply
- one throws a coarse paint-by-number veil over the opaque pixels
- it chalks the cliff faces where luminance or alpha changes sharply
Important variables:
- `edge_map[y, x]`
- `cluster_map[y, x]`
Natural-language picture:
The edge scout is looking for cracks and ridges.
The cluster scout is painting broad neighborhoods in different colors so the optimizer can later tell when it crossed from one country into another.
This is already one place where the machine speaks in two voices: one scout sees real local edges, the other sees a rough k-means abstraction.
The old k-means paint-by-number scout has been cut. It was letting coarse color partitions masquerade as geometry, and that did not fit the optimizer's core story.
## Stage 2: The optimizer lays down a regular UV grid
@ -128,7 +123,7 @@ Then `_make_patch_offsets(...)` builds a little stencil of nearby offsets around
Important truth:
Despite the name `optimize_uv_field(...)`, the current path never actually moves the UV field. It lays down a regular grid and keeps it.
Despite the old name `optimize_uv_field(...)`, the current path never actually moves the UV field. It lays down a regular grid and keeps it.
## Stage 3: The machine paints a soft portrait of the lattice
@ -136,7 +131,6 @@ Functions:
- `_sample_cell_patches(...)`
- `_representative_colors(...)`
- `_exemplar_colors(...)` (helper only; not used in the current main path)
Key variables:
@ -176,7 +170,6 @@ Functions:
- `_build_source_detail_reference(...)`
- `_build_source_reliability(...)`
- `_edge_gradient_maps(...)`
- `_cluster_boundary_map(...)`
Key outputs:
@ -432,7 +425,7 @@ Then it hardens alpha again before producing the final `target_rgba`.
Functions:
- `source_lattice_consistency_breakdown(...)`
- final guardrail in `optimize_uv_field(...)`
- final guardrail in `optimize_lattice_pixels(...)`
Key variables:
@ -472,11 +465,10 @@ Those are coherent. They all serve the same broad goal: make one global lattice
These are the places where the optimizer still speaks in an overly complicated or self-contradictory voice:
- `optimize_uv_field(...)` no longer optimizes the UV field at all. The UV grid is regular and fixed. The name is a historical fossil.
- the machine still maintains two overlapping portraits of the same lattice and spends much of its complexity mediating the argument between them
- `guide_edge` mixes the real `edge_map` with `_cluster_boundary_map(cluster_map)`, which means a coarse k-means partition is still allowed to masquerade as geometric structure
- snap, relax, and refine all carry slightly different versions of adjacency, motif, line, and delta agreement; the same idea is being said three times with different accents
- the candidate generator is local and the solver is discrete, but the vocabulary around it still sounds like a continuous deformation engine
- the main entry point is now honestly named `optimize_lattice_pixels(...)`, and the k-means boundary scout is gone; those cuts removed two contradictions from the previous map
This is probably the real cutting checklist for the next optimizer pass.
@ -484,7 +476,7 @@ This is probably the real cutting checklist for the next optimizer pass.
If we strip the optimizer down to what it is really doing today, the machine is:
`source image -> edge/cluster scout -> fixed lattice centers -> soft portrait + hard portrait -> source-first snap -> soft neighborhood relax -> greedy discrete refine -> keep snap if refine lied`
`source image -> edge scout -> fixed lattice centers -> soft portrait + hard portrait -> source-first snap -> soft neighborhood relax -> greedy discrete refine -> keep snap if refine lied`
That is much simpler than the surrounding names make it sound.
@ -492,13 +484,11 @@ That is much simpler than the surrounding names make it sound.
If we keep pruning, the highest-value cuts look like this:
1. Rename `optimize_uv_field(...)` to match reality.
It is a lattice-conditioned discrete pixel chooser now, not a UV optimizer.
2. Separate preparation from decision-making.
1. Separate preparation from decision-making.
The edge scout, portraits, reliability maps, and delta maps want to be a clear prep stage instead of being braided through one giant function.
3. Decide whether the representative portrait still earns its keep everywhere.
2. Decide whether the representative portrait still earns its keep everywhere.
If the source-first path is now strong enough, some of that arbitration machinery may be removable.
4. Collapse duplicate structure voices.
3. Collapse duplicate structure voices.
Adjacency, motif, and line are important, but they should not need three near-parallel dialects unless they are truly doing different work.
That is the next place to swing the axe.