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

@ -59,7 +59,7 @@ Current tile-graph status:
- repeated fixed-lattice `tile-graph` runs in the same Python process now reuse the expensive model build; on the cleaned badge at pinned `126x126` / phase `(0.0, -0.2)`, the first CUDA run took about `10.2s` and the second cached rerun took about `2.1s`
- the latest reduce-by-key pinned badge probe under `artifacts/tile-graph-reducebykey-v1-badge-126/` finishes in about `16.1s` cold and `6.0s` warm on CUDA, versus the previous full-resolution cut at about `96.9s` cold and `5.4s` warm
- that speed pass is a real throughput win but a small quality regression: the same pinned badge case moves from `0.1814` to `0.2036` source-fidelity
- the optimizer now has its own deep-dive map in `docs/optimizer-algorithm-map.md`, which documents the current continuous path as it actually exists: a fixed-lattice, source-first discrete chooser wearing some continuous-era names
- the optimizer now has its own deep-dive map in `docs/optimizer-algorithm-map.md`, and the first cut from that map removed the old UV-optimizer name plus the continuous k-means cluster-boundary scout; the continuous path is now a fixed-lattice, source-first discrete chooser guided by real edge evidence
- after the first pruning pass, tile-graph no longer participates in pipeline phase-rerank probes and no longer carries hybrid geometry priors through its unary cost; the path is now one lattice-conditioned candidate generator plus one local discrete solver
- this fixes the core design mismatch that had allowed repeated distant labels to create big same-color patches and opaque black background blocks
- on the current `24x24` emblem smoke case, an end-to-end `tile-graph` run dropped from about `2.57s` on CPU to `0.61s` on CUDA on this machine

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.

View file

@ -55,91 +55,24 @@ def _compute_edge_map_torch(torch, rgba_t):
return edge.to(dtype=torch.float32)
def _kmeans(data: np.ndarray, k: int, seed: int, iterations: int = 12) -> tuple[np.ndarray, np.ndarray]:
rng = np.random.default_rng(seed)
if data.shape[0] <= k:
centers = data.copy()
labels = np.arange(data.shape[0], dtype=np.int32)
return centers, labels
picks = rng.choice(data.shape[0], size=k, replace=False)
centers = data[picks].copy()
for _ in range(iterations):
distances = np.linalg.norm(data[:, None, :] - centers[None, :, :], axis=-1)
labels = np.argmin(distances, axis=1)
new_centers = []
for idx in range(k):
members = data[labels == idx]
if members.size == 0:
new_centers.append(centers[idx])
else:
new_centers.append(np.mean(members, axis=0))
centers = np.asarray(new_centers, dtype=np.float32)
distances = np.linalg.norm(data[:, None, :] - centers[None, :, :], axis=-1)
labels = np.argmin(distances, axis=1)
return centers.astype(np.float32), labels.astype(np.int32)
def _kmeans_torch(torch, data_t, k: int, seed: int, iterations: int = 12):
if data_t.shape[0] <= k:
centers_t = data_t.clone()
labels_t = torch.arange(data_t.shape[0], device=data_t.device, dtype=torch.long)
return centers_t, labels_t
generator = torch.Generator(device=data_t.device)
generator.manual_seed(seed)
picks = torch.randperm(data_t.shape[0], device=data_t.device, generator=generator)[:k]
centers_t = data_t[picks].clone()
for _ in range(iterations):
distances = torch.linalg.vector_norm(data_t[:, None, :] - centers_t[None, :, :], dim=-1)
labels_t = torch.argmin(distances, dim=1)
new_centers = []
for idx in range(k):
members = data_t[labels_t == idx]
if members.numel() == 0:
new_centers.append(centers_t[idx])
else:
new_centers.append(members.mean(dim=0))
centers_t = torch.stack(new_centers, dim=0).to(dtype=torch.float32)
distances = torch.linalg.vector_norm(data_t[:, None, :] - centers_t[None, :, :], dim=-1)
labels_t = torch.argmin(distances, dim=1)
return centers_t, labels_t
def analyze_continuous_source(
rgba: np.ndarray,
seed: int,
cluster_count: int = 6,
device: str | None = None,
) -> ContinuousSourceAnalysis:
del seed
if device is not None:
torch = _require_torch()
resolved_device = _resolve_device(torch, device)
rgba_t = torch.from_numpy(rgba).to(device=resolved_device, dtype=torch.float32)
edge_map_t = _compute_edge_map_torch(torch, rgba_t)
alpha_t = rgba_t[..., 3]
cluster_map_t = torch.full(alpha_t.shape, -1, device=resolved_device, dtype=torch.long)
opaque_t = alpha_t > 0.05
if bool(torch.any(opaque_t).item()):
samples_t = rgba_t[opaque_t]
k = min(cluster_count, max(2, int(np.sqrt(int(samples_t.shape[0]) // 64 + 1))))
_centers_t, labels_t = _kmeans_torch(torch, samples_t, k=k, seed=seed)
cluster_map_t[opaque_t] = labels_t
return ContinuousSourceAnalysis(
edge_map=edge_map_t.detach().cpu().numpy().astype(np.float32),
cluster_map=cluster_map_t.detach().cpu().numpy().astype(np.int32),
)
edge_map = _compute_edge_map(rgba)
alpha = rgba[..., 3]
cluster_map = np.full(alpha.shape, -1, dtype=np.int32)
opaque = alpha > 0.05
if np.any(opaque):
samples = rgba[opaque]
k = min(cluster_count, max(2, int(np.sqrt(samples.shape[0] // 64 + 1))))
_centers, labels = _kmeans(samples, k=k, seed=seed)
cluster_map[opaque] = labels
return ContinuousSourceAnalysis(
edge_map=edge_map,
cluster_map=cluster_map,
)

View file

@ -45,15 +45,6 @@ def _make_regular_uv(height: int, width: int, target_height: int, target_width:
return uv.astype(np.float32)
def _cluster_boundary_map(cluster_map: np.ndarray) -> np.ndarray:
if cluster_map.size == 0:
return np.zeros_like(cluster_map, dtype=np.float32)
boundary = np.zeros(cluster_map.shape, dtype=np.float32)
boundary[:, 1:] = np.maximum(boundary[:, 1:], (cluster_map[:, 1:] != cluster_map[:, :-1]).astype(np.float32))
boundary[1:, :] = np.maximum(boundary[1:, :], (cluster_map[1:, :] != cluster_map[:-1, :]).astype(np.float32))
return boundary
def _edge_gradient_maps(edge_map: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
grad_x = np.zeros_like(edge_map, dtype=np.float32)
grad_y = np.zeros_like(edge_map, dtype=np.float32)
@ -109,16 +100,6 @@ def _representative_colors(patches, solver_params: SolverHyperParams):
return representative, coherence
def _exemplar_colors(patches):
patch_mean = patches.mean(dim=3, keepdim=True)
distances = (patches - patch_mean).abs().mean(dim=-1)
selected = distances.argmin(dim=3)
return patches.gather(
dim=3,
index=selected[..., None, None].expand(*selected.shape, 1, patches.shape[-1]),
).squeeze(3)
def _source_boundary_deltas(F, source_t, uv, axis: str, probe_scale: float = 0.22):
if axis == "x":
if uv.shape[2] < 2:
@ -1355,7 +1336,7 @@ def _discrete_refine_output(
return _finalize_output_rgba(best_colors, use_opaque, use_transparent), loss_history
def optimize_uv_field(
def optimize_lattice_pixels(
rgba: np.ndarray,
inference: InferenceResult,
analysis: ContinuousSourceAnalysis,
@ -1377,8 +1358,7 @@ def optimize_uv_field(
source_t = torch.from_numpy(source.transpose(2, 0, 1)[None, ...]).to(device=device, dtype=torch.float32)
source_edge = analysis.edge_map.astype(np.float32)
edge_grad_x, edge_grad_y = _edge_gradient_maps(source_edge)
guide_edge = np.maximum(source_edge, _cluster_boundary_map(analysis.cluster_map))
edge_t = torch.from_numpy(guide_edge[None, None, ...]).to(device=device, dtype=torch.float32)
edge_t = torch.from_numpy(source_edge[None, None, ...]).to(device=device, dtype=torch.float32)
uv0 = _make_regular_uv(
height=height,
width=width,

View file

@ -7,7 +7,7 @@ from typing import Any
import numpy as np
from .analysis import analyze_continuous_source, analyze_tile_graph_source
from .continuous import optimize_uv_field
from .continuous import optimize_lattice_pixels
from .diagnostics import (
summarize_run,
write_alpha_preview,
@ -421,7 +421,7 @@ def _run_reconstruction(
device=device,
solver_params=solver_params,
)
solver = optimize_uv_field(
solver = optimize_lattice_pixels(
source,
inference=inference,
analysis=analysis,

View file

@ -30,7 +30,6 @@ class InferenceResult:
@dataclass(slots=True)
class ContinuousSourceAnalysis:
edge_map: np.ndarray
cluster_map: np.ndarray
@dataclass(slots=True)

View file

@ -14,8 +14,7 @@ def test_continuous_source_analysis_cpu_device_matches_default_path() -> None:
accelerated = analyze_continuous_source(source, seed=7, device="cpu")
assert np.allclose(accelerated.edge_map, default.edge_map, atol=1e-5)
assert accelerated.cluster_map.shape == default.cluster_map.shape
assert np.array_equal(accelerated.cluster_map, default.cluster_map)
assert not hasattr(accelerated, "cluster_map")
def test_tile_graph_source_analysis_is_edge_only() -> None:

View file

@ -7,10 +7,9 @@ from repixelizer.analysis import analyze_continuous_source
from repixelizer.continuous import (
_build_candidate_positions,
_build_source_reliability,
_exemplar_colors,
_line_pattern_loss,
_make_regular_uv,
optimize_uv_field,
optimize_lattice_pixels,
)
from repixelizer.metrics import source_lattice_consistency_breakdown
from repixelizer.params import SolverHyperParams
@ -35,28 +34,6 @@ def test_line_pattern_loss_penalizes_wobbling_line() -> None:
assert bad_loss > good_loss + 0.01
def test_exemplar_colors_selects_an_actual_patch_sample() -> None:
patches = torch.tensor(
[
[
[
[
[0.0, 0.0, 0.0, 1.0],
[0.8, 0.7, 0.1, 1.0],
[0.2, 0.3, 0.9, 1.0],
]
]
]
],
dtype=torch.float32,
)
exemplar = _exemplar_colors(patches)[0, 0, 0]
options = patches[0, 0, 0]
assert any(torch.allclose(exemplar, option) for option in options)
def test_source_reliability_stays_high_for_high_contrast_edge_cells() -> None:
source = np.zeros((8, 8, 4), dtype=np.float32)
source[..., 3] = 1.0
@ -125,7 +102,7 @@ def test_candidate_positions_include_sharp_and_edge_guided_samples() -> None:
assert len(cell_candidates) > 2
def test_optimize_uv_field_does_not_regress_from_snap_on_thin_feature_case() -> None:
def test_optimize_lattice_pixels_does_not_regress_from_snap_on_thin_feature_case() -> None:
source = np.zeros((16, 16, 4), dtype=np.float32)
source[4:12, 8] = np.asarray([0.95, 0.95, 0.95, 1.0], dtype=np.float32)
source[4, 7:10] = np.asarray([0.95, 0.95, 0.95, 1.0], dtype=np.float32)
@ -151,7 +128,7 @@ def test_optimize_uv_field_does_not_regress_from_snap_on_thin_feature_case() ->
confidence=1.0,
top_candidates=[],
)
artifacts = optimize_uv_field(
artifacts = optimize_lattice_pixels(
fake,
inference=inference,
analysis=analyze_continuous_source(fake, seed=7),

View file

@ -7,7 +7,7 @@ import numpy as np
from repixelizer.analysis import analyze_continuous_source
from repixelizer.baselines import naive_resize_baseline
from repixelizer.continuous import optimize_uv_field
from repixelizer.continuous import optimize_lattice_pixels
from repixelizer.metrics import coherence_breakdown, foreground_reconstruction_error, source_lattice_consistency_breakdown
from repixelizer.params import SolverHyperParams
from repixelizer.pipeline import _run_reconstruction, _select_phase_candidate, run_pipeline
@ -214,11 +214,11 @@ def test_phase_rerank_can_override_low_confidence_inference_pick(monkeypatch) ->
def __init__(self, rgba: np.ndarray) -> None:
self.target_rgba = rgba
def fake_optimize_uv_field(source_rgba, inference, analysis, steps, seed, device, solver_params=None):
def fake_optimize_lattice_pixels(source_rgba, inference, analysis, steps, seed, device, solver_params=None):
assert steps == 0
return DummyArtifacts(outputs[(inference.phase_x, inference.phase_y)])
monkeypatch.setattr("repixelizer.pipeline.optimize_uv_field", fake_optimize_uv_field)
monkeypatch.setattr("repixelizer.pipeline.optimize_lattice_pixels", fake_optimize_lattice_pixels)
selected = _select_phase_candidate(source, inference, analysis=object(), seed=7, device="cpu")
assert selected.phase_x == candidate_b.phase_x
@ -242,7 +242,7 @@ def test_phase_rerank_can_override_to_better_size_candidate(monkeypatch) -> None
def __init__(self, rgba: np.ndarray) -> None:
self.target_rgba = rgba
def fake_optimize_uv_field(source_rgba, inference, analysis, steps, seed, device, solver_params=None):
def fake_optimize_lattice_pixels(source_rgba, inference, analysis, steps, seed, device, solver_params=None):
rgba = np.zeros((inference.target_height, inference.target_width, 4), dtype=np.float32)
return DummyArtifacts(rgba)
@ -251,7 +251,7 @@ def test_phase_rerank_can_override_to_better_size_candidate(monkeypatch) -> None
return {"score": 0.10}
return {"score": 0.01}
monkeypatch.setattr("repixelizer.pipeline.optimize_uv_field", fake_optimize_uv_field)
monkeypatch.setattr("repixelizer.pipeline.optimize_lattice_pixels", fake_optimize_lattice_pixels)
monkeypatch.setattr("repixelizer.pipeline.source_lattice_consistency_breakdown", fake_support)
selected = _select_phase_candidate(source, inference, analysis=object(), seed=7, device="cpu")
@ -276,7 +276,7 @@ def test_phase_rerank_rejects_large_size_jump(monkeypatch) -> None:
def __init__(self, rgba: np.ndarray) -> None:
self.target_rgba = rgba
def fake_optimize_uv_field(source_rgba, inference, analysis, steps, seed, device, solver_params=None):
def fake_optimize_lattice_pixels(source_rgba, inference, analysis, steps, seed, device, solver_params=None):
rgba = np.zeros((inference.target_height, inference.target_width, 4), dtype=np.float32)
return DummyArtifacts(rgba)
@ -285,7 +285,7 @@ def test_phase_rerank_rejects_large_size_jump(monkeypatch) -> None:
return {"score": 0.10}
return {"score": 0.01}
monkeypatch.setattr("repixelizer.pipeline.optimize_uv_field", fake_optimize_uv_field)
monkeypatch.setattr("repixelizer.pipeline.optimize_lattice_pixels", fake_optimize_lattice_pixels)
monkeypatch.setattr("repixelizer.pipeline.source_lattice_consistency_breakdown", fake_support)
selected = _select_phase_candidate(source, inference, analysis=object(), seed=7, device="cpu")
@ -309,7 +309,7 @@ def test_select_phase_candidate_can_skip_phase_rerank(monkeypatch) -> None:
def fail(*args, **kwargs):
raise AssertionError("phase rerank probe should not run when disabled")
monkeypatch.setattr("repixelizer.pipeline.optimize_uv_field", fail)
monkeypatch.setattr("repixelizer.pipeline.optimize_lattice_pixels", fail)
selected = _select_phase_candidate(
source,
inference,
@ -449,7 +449,6 @@ def test_pipeline_fixed_target_and_phase_skip_search_inference(tmp_path: Path, m
class DummyAnalysis:
edge_map = np.zeros((6, 6), dtype=np.float32)
cluster_map = np.zeros((6, 6), dtype=np.int32)
def fail_infer(*args, **kwargs):
raise AssertionError("full lattice search should not run for fixed target/phase")
@ -457,7 +456,7 @@ def test_pipeline_fixed_target_and_phase_skip_search_inference(tmp_path: Path, m
def fake_infer_fixed(*args, **kwargs):
return inference
def fake_optimize_uv_field(source_rgba, inference, analysis, steps, seed, device, solver_params=None):
def fake_optimize_lattice_pixels(source_rgba, inference, analysis, steps, seed, device, solver_params=None):
rgba = np.zeros((inference.target_height, inference.target_width, 4), dtype=np.float32)
rgba[..., 3] = 1.0
return SolverArtifacts(
@ -471,7 +470,7 @@ def test_pipeline_fixed_target_and_phase_skip_search_inference(tmp_path: Path, m
monkeypatch.setattr("repixelizer.pipeline.infer_lattice", fail_infer)
monkeypatch.setattr("repixelizer.pipeline.infer_fixed_lattice", fake_infer_fixed)
monkeypatch.setattr("repixelizer.pipeline.analyze_continuous_source", lambda *args, **kwargs: DummyAnalysis())
monkeypatch.setattr("repixelizer.pipeline.optimize_uv_field", fake_optimize_uv_field)
monkeypatch.setattr("repixelizer.pipeline.optimize_lattice_pixels", fake_optimize_lattice_pixels)
result = run_pipeline(
input_path,
@ -514,7 +513,7 @@ def test_phase_rerank_can_accept_low_confidence_size_jump_with_strong_support(mo
candidate_b.target_width: np.ones((candidate_b.target_height, candidate_b.target_width, 4), dtype=np.float32),
}
def fake_optimize_uv_field(source_rgba, inference, analysis, steps, seed, device, solver_params=None):
def fake_optimize_lattice_pixels(source_rgba, inference, analysis, steps, seed, device, solver_params=None):
return DummyArtifacts(outputs[inference.target_width])
def fake_support(source_rgba, output_rgba, *, target_width, target_height, phase_x, phase_y):
@ -529,7 +528,7 @@ def test_phase_rerank_can_accept_low_confidence_size_jump_with_strong_support(mo
def fake_concentration(rgba):
return 0.18 if np.mean(rgba) < 0.1 else 0.31
monkeypatch.setattr("repixelizer.pipeline.optimize_uv_field", fake_optimize_uv_field)
monkeypatch.setattr("repixelizer.pipeline.optimize_lattice_pixels", fake_optimize_lattice_pixels)
monkeypatch.setattr("repixelizer.pipeline.source_lattice_consistency_breakdown", fake_support)
monkeypatch.setattr("repixelizer.pipeline.foreground_edge_position_error", fake_edge_position)
monkeypatch.setattr("repixelizer.pipeline.foreground_stroke_wobble_error", fake_wobble)
@ -556,7 +555,7 @@ def test_phase_rerank_keeps_high_confidence_candidate_without_probe(monkeypatch)
def fail(*args, **kwargs):
raise AssertionError("rerank probe should not run for high-confidence inference")
monkeypatch.setattr("repixelizer.pipeline.optimize_uv_field", fail)
monkeypatch.setattr("repixelizer.pipeline.optimize_lattice_pixels", fail)
selected = _select_phase_candidate(source, inference, analysis=object(), seed=7, device="cpu")
assert selected.target_width == candidate_a.target_width
assert selected.target_height == candidate_a.target_height
@ -596,10 +595,10 @@ def test_phase_rerank_can_prefer_better_line_metrics(monkeypatch) -> None:
candidate_b.phase_x: np.ones((2, 2, 4), dtype=np.float32),
}
def fake_optimize_uv_field(source_rgba, inference, analysis, steps, seed, device, solver_params=None):
def fake_optimize_lattice_pixels(source_rgba, inference, analysis, steps, seed, device, solver_params=None):
return DummyArtifacts(outputs[inference.phase_x])
monkeypatch.setattr("repixelizer.pipeline.optimize_uv_field", fake_optimize_uv_field)
monkeypatch.setattr("repixelizer.pipeline.optimize_lattice_pixels", fake_optimize_lattice_pixels)
monkeypatch.setattr("repixelizer.pipeline.source_lattice_consistency_breakdown", fake_support)
monkeypatch.setattr("repixelizer.pipeline.foreground_edge_position_error", fake_edge_position)
monkeypatch.setattr("repixelizer.pipeline.foreground_stroke_wobble_error", fake_wobble)
@ -620,7 +619,7 @@ def test_badge_fixture_candidate_beats_previous_source_fidelity_baselines() -> N
confidence=0.0,
top_candidates=[],
)
artifacts = optimize_uv_field(
artifacts = optimize_lattice_pixels(
source,
inference=inference,
analysis=analyze_continuous_source(source, seed=7),