From 7b97a93e6c82fbff76a2a898dd10f9db1e7f643d Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 23 Apr 2026 17:00:24 +0100 Subject: [PATCH] Cut cluster scout from optimizer --- README.md | 2 +- docs/implementation-plan.md | 12 ++++-- docs/optimizer-algorithm-map.md | 34 ++++++---------- src/repixelizer/analysis.py | 69 +-------------------------------- src/repixelizer/continuous.py | 24 +----------- src/repixelizer/pipeline.py | 4 +- src/repixelizer/types.py | 1 - tests/test_analysis.py | 3 +- tests/test_continuous.py | 29 ++------------ tests/test_pipeline.py | 33 ++++++++-------- 10 files changed, 47 insertions(+), 164 deletions(-) diff --git a/README.md b/README.md index c8f9174..4a3c2e7 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/implementation-plan.md b/docs/implementation-plan.md index 6762aa9..1a96508 100644 --- a/docs/implementation-plan.md +++ b/docs/implementation-plan.md @@ -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 diff --git a/docs/optimizer-algorithm-map.md b/docs/optimizer-algorithm-map.md index 3069f3a..6417948 100644 --- a/docs/optimizer-algorithm-map.md +++ b/docs/optimizer-algorithm-map.md @@ -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. diff --git a/src/repixelizer/analysis.py b/src/repixelizer/analysis.py index b992c0e..10e16f8 100644 --- a/src/repixelizer/analysis.py +++ b/src/repixelizer/analysis.py @@ -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, ) diff --git a/src/repixelizer/continuous.py b/src/repixelizer/continuous.py index 1832f6d..6bee610 100644 --- a/src/repixelizer/continuous.py +++ b/src/repixelizer/continuous.py @@ -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, diff --git a/src/repixelizer/pipeline.py b/src/repixelizer/pipeline.py index f80516a..feb10dd 100644 --- a/src/repixelizer/pipeline.py +++ b/src/repixelizer/pipeline.py @@ -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, diff --git a/src/repixelizer/types.py b/src/repixelizer/types.py index 664d0bb..e1d6c6a 100644 --- a/src/repixelizer/types.py +++ b/src/repixelizer/types.py @@ -30,7 +30,6 @@ class InferenceResult: @dataclass(slots=True) class ContinuousSourceAnalysis: edge_map: np.ndarray - cluster_map: np.ndarray @dataclass(slots=True) diff --git a/tests/test_analysis.py b/tests/test_analysis.py index 4482b30..0b86ee8 100644 --- a/tests/test_analysis.py +++ b/tests/test_analysis.py @@ -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: diff --git a/tests/test_continuous.py b/tests/test_continuous.py index c38778b..e3397bb 100644 --- a/tests/test_continuous.py +++ b/tests/test_continuous.py @@ -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), diff --git a/tests/test_pipeline.py b/tests/test_pipeline.py index 6d82b7f..827d8b2 100644 --- a/tests/test_pipeline.py +++ b/tests/test_pipeline.py @@ -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),