Cut cluster scout from optimizer
This commit is contained in:
parent
d77e2ef101
commit
7b97a93e6c
10 changed files with 47 additions and 164 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ class InferenceResult:
|
|||
@dataclass(slots=True)
|
||||
class ContinuousSourceAnalysis:
|
||||
edge_map: np.ndarray
|
||||
cluster_map: np.ndarray
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
Loading…
Reference in a new issue