Use direct autocorr inference for hosted demo

This commit is contained in:
Meta Vens 2026-04-26 19:21:01 +01:00
commit 728e08472b
12 changed files with 326 additions and 23 deletions

View file

@ -21,6 +21,12 @@ The live pipeline is:
`source image -> lattice inference -> edge analysis -> phase-field reconstruction -> cleanup -> optional palette fit -> diagnostics`
Inference now splits three ways:
- fixed lattice when the caller pins size and/or phase explicitly
- searched lattice for the full local workflow
- direct autocorr lattice for the hosted demo path: one consensus size, one cheap phase probe, no size search, no phase rerank
## What is working
- lattice size and phase inference are still shared and still CUDA-capable
@ -95,6 +101,7 @@ Current stance:
- keep the shipped searched path grounded in autocorrelation only
- let autocorr keep a small near-best lag plateau and use cross-axis consensus before collapsing to one lattice family
- the hosted demo path now skips multi-size search entirely and uses one autocorr-consensus size plus the cheap inferred phase, capped by the hosted output limit
- do not trust the old pixel-walk / change-interval spacing path; it was cut after it kept pruning the badge away from the `126` family
- do not keep the projected edge-energy spectral pass; it floor-hugged to `2px` / `626`-family nonsense on the dense landscape fixture and did not earn its keep
- treat phase rerank as optional scaffolding, not sacred machinery

View file

@ -88,15 +88,15 @@ That means the solver is not maintaining alternate per-cell candidate sets or se
The pipeline first decides whether the lattice is:
- fixed explicitly by the caller, using `infer_fixed_lattice(...)`, or
- inferred directly from autocorr consensus with one cheap phase probe, using `infer_autocorr_lattice(...)`, or
- searched automatically by `infer_lattice(...)`
`infer_lattice(...)` itself is already a small machine:
The two automatic paths split like this:
- it estimates rough cell spacing from source edge-profile autocorrelation
- it keeps a tiny near-best lag plateau long enough to find cross-axis consensus instead of collapsing instantly to one lag
- it builds candidate output sizes
- it scores a small phase grid for each size
- it keeps the best candidate plus top alternates
- both paths estimate rough cell spacing from source edge-profile autocorrelation
- both paths keep a tiny near-best lag plateau long enough to find cross-axis consensus instead of collapsing instantly to one lag
- `infer_autocorr_lattice(...)` takes one consensus size, scores one small phase grid, and stops there
- `infer_lattice(...)` still builds a small candidate size family, scores a small phase grid for each size, and keeps the best candidate plus top alternates
After that, phase rerank may still happen, but only for `phase-field`, only when confidence is low, and only when rerank is enabled. The rerank probe is a short preview solve. It calls `_run_reconstruction(...)` with a small bounded step count, capped by both the requested solver steps and `phase_rerank_preview_steps`.
@ -108,7 +108,7 @@ That part matters. The pipeline is effectively asking:
This is the survey crew laying down the ruler and arguing about where the graph paper should go.
If the user pinned the lattice, the argument is over. If not, the crew drags the ruler around, checks a bunch of possible alignments, and picks the one that seems most plausible before the real workers show up.
If the user pinned the lattice, the argument is over. If the hosted path is active, the crew makes one fast autocorr-backed call and moves on. If not, the crew drags the ruler around, checks a small pile of plausible alignments, and picks the one that seems most plausible before the real workers show up.
## Stage 1: The edge scout builds a danger map

View file

@ -29,6 +29,7 @@ Do not trust this file for the exact live HEAD. Always check git.
- `phase-field` is the only live reconstruction engine.
- The live pipeline is `source -> lattice inference -> edge analysis -> phase-field reconstruction -> cleanup -> optional palette fit -> diagnostics`.
- searched lattice inference now trusts autocorrelation only; it keeps a tiny near-best lag plateau and looks for cross-axis consensus before collapsing to one lattice family. the old change-interval / pixel-walk sizing path was cut after it kept steering the badge away from the `126` family, and the projected edge-energy spectral pass was also removed after floor-hugging to 2px / 626-family aliases on the dense landscape fixture
- hosted demo inference now uses one direct autocorr-consensus size plus the cheap inferred phase, capped by the hosted output limit. it no longer silently pins unspecified jobs to `max_output_dimension`
- The low-confidence phase rerank path is a short preview solve, not a second full optimizer.
- the phase field now has a wider displacement budget, so moderate phase mistakes are allowed to get corrected by the field instead of relying purely on rerank ceremony
- Cleanup is secondary and usually a no-op; the core result is supposed to come from the solver, not cleanup cosplay.
@ -54,6 +55,7 @@ The current spine:
- lattice inference with fixed-size and searched-size paths
- searched lattice inference currently trusts autocorrelation only, not the rejected pixel-walk or spectral priors
- hosted demo inference uses the direct autocorr path and keeps phase rerank off
- low-confidence phase rerank through short preview reconstruction
- edge analysis feeding one projected displacement-field optimizer with a wider displacement leash
- nearest-source final sampling from `uv0_px + disp_t`

View file

@ -518,15 +518,11 @@ def _normalize_job_options(
normalized_steps = min(requested_steps, config.max_steps)
normalized_device = "cpu" if config.hosted_demo else (device.strip() if device.strip() else "auto")
normalized_strip_background = False if config.hosted_demo else bool(strip_background)
effective_target_size = normalized_target_size
if fixed_dims is None:
effective_target_size = (
config.max_output_dimension
if effective_target_size is None
else min(effective_target_size, config.max_output_dimension)
)
inference_mode = "autocorr" if config.hosted_demo and fixed_dims is None else "search"
max_inferred_target_size = config.max_output_dimension if config.hosted_demo and fixed_dims is None else None
normalized_skip_phase_rerank = True if config.hosted_demo else bool(skip_phase_rerank)
return {
"target_size": effective_target_size,
"target_size": normalized_target_size,
"target_width": normalized_target_width,
"target_height": normalized_target_height,
"phase_x": phase_x,
@ -535,7 +531,9 @@ def _normalize_job_options(
"seed": int(seed),
"device": normalized_device,
"strip_background": normalized_strip_background,
"skip_phase_rerank": bool(skip_phase_rerank),
"skip_phase_rerank": normalized_skip_phase_rerank,
"lattice_inference_mode": inference_mode,
"max_inferred_target_size": max_inferred_target_size,
}
@ -601,6 +599,8 @@ def _execute_job(job: GuiJob) -> None:
device=job.options["device"],
strip_background=job.options["strip_background"],
enable_phase_rerank=not job.options["skip_phase_rerank"],
lattice_inference_mode=job.options["lattice_inference_mode"],
max_inferred_target_size=job.options["max_inferred_target_size"],
observer=job.observe,
)
job.mark_completed()

View file

@ -558,7 +558,12 @@ function renderStatusMetrics() {
}
else if (state.stageKey === "analysis" || state.stageKey === "selection") {
if (state.inference) {
items.push({ label: "Mode", value: state.inferenceMode === "fixed" ? "pinned" : state.inferenceMode ?? "search" });
const modeLabel = state.inferenceMode === "fixed"
? "pinned"
: state.inferenceMode === "autocorr"
? "autocorr direct"
: state.inferenceMode ?? "search";
items.push({ label: "Mode", value: modeLabel });
items.push({ label: "Grid", value: `${state.inference.target_width} x ${state.inference.target_height}` });
items.push({ label: "Phase", value: `${formatNumber(state.inference.phase_x, 2)}, ${formatNumber(state.inference.phase_y, 2)}` });
items.push({ label: "Confidence", value: formatNumber(state.inference.confidence, 3) });

View file

@ -71,6 +71,10 @@ def _candidate_dims(
return dims
def _target_dims_from_major_size(width: int, height: int, target_size: int) -> tuple[int, int]:
return _candidate_dims(width, height, int(target_size))[0]
def _autocorr_size_window(
hinted_sizes: list[int],
*,
@ -117,6 +121,31 @@ def _resolve_candidate_dims_from_autocorr(
return narrowed or dims
def _direct_autocorr_target_size(
width: int,
height: int,
*,
hinted_sizes: list[int],
shared_prior: float,
prior_reliability: float,
max_target_size: int | None = None,
) -> int:
size_window = _autocorr_size_window(
hinted_sizes,
prior_reliability=prior_reliability,
)
major = max(width, height)
if size_window is not None:
target_size = int(size_window[0])
elif hinted_sizes:
target_size = int(round(float(np.median(np.asarray(hinted_sizes, dtype=np.float32)))))
else:
target_size = max(1, int(round(major / max(1e-6, shared_prior))))
if max_target_size is not None:
target_size = min(target_size, max(1, int(max_target_size)))
return max(1, target_size)
def _estimate_cell_size_details(profile: np.ndarray) -> AutocorrEstimate:
centered = profile.astype(np.float32) - float(np.mean(profile))
max_lag = min(64, max(4, profile.shape[0] // 3))
@ -612,6 +641,80 @@ def infer_lattice(
)
def infer_autocorr_lattice(
rgba: np.ndarray,
*,
max_target_size: int | None = None,
device: str = "auto",
observer: PipelineObserver | None = None,
) -> InferenceResult:
torch, _ = _require_torch()
resolved_device = _resolve_device(torch, device)
height, width = rgba.shape[:2]
autocorr_x_estimate, autocorr_y_estimate = _estimate_lattice_autocorr_details(rgba)
prior_cell_x, prior_cell_y, prior_reliability = _estimate_lattice_prior_details(rgba)
hinted_sizes = _hint_target_sizes_from_autocorr(
width,
height,
autocorr_x_estimate=autocorr_x_estimate,
autocorr_y_estimate=autocorr_y_estimate,
shared_prior=prior_cell_x,
)
target_size = _direct_autocorr_target_size(
width,
height,
hinted_sizes=hinted_sizes,
shared_prior=prior_cell_x,
prior_reliability=prior_reliability,
max_target_size=max_target_size,
)
target_width, target_height = _target_dims_from_major_size(width, height, target_size)
phase_values = np.linspace(-0.4, 0.4, num=5, dtype=np.float32)
phase_sample_count = int(phase_values.size * phase_values.size)
emit_observer(
observer,
"lattice_search_started",
candidate_count=1,
phase_sample_count=phase_sample_count,
device=resolved_device,
)
check_observer_cancelled(observer)
candidates = _score_phase_group(
rgba,
target_width=target_width,
target_height=target_height,
prior_cell_x=prior_cell_x,
prior_cell_y=prior_cell_y,
prior_reliability=prior_reliability,
phase_x_values=phase_values,
phase_y_values=phase_values,
device=resolved_device,
)
candidates.sort(key=lambda item: item.score, reverse=True)
emit_observer(
observer,
"lattice_search_progress",
completed_candidates=1,
total_candidates=1,
target_width=int(target_width),
target_height=int(target_height),
phase_sample_count=phase_sample_count,
best_score=None if not candidates else float(candidates[0].score),
)
best = candidates[0]
second = candidates[1] if len(candidates) > 1 else best
confidence = max(0.0, best.score - second.score)
return InferenceResult(
target_width=best.target_width,
target_height=best.target_height,
phase_x=best.phase_x,
phase_y=best.phase_y,
confidence=confidence,
top_candidates=candidates[:8],
)
def infer_fixed_lattice(
rgba: np.ndarray,
*,

View file

@ -18,7 +18,7 @@ from .diagnostics import (
write_lattice_overlay,
)
from .discrete import cleanup_pixels
from .inference import infer_fixed_lattice, infer_lattice, inference_to_json
from .inference import infer_autocorr_lattice, infer_fixed_lattice, infer_lattice, inference_to_json
from .io import load_rgba, nearest_resize, save_rgba
from .metrics import (
foreground_edge_concentration,
@ -51,6 +51,8 @@ def run_pipeline(
solver_params: SolverHyperParams | None = None,
strip_background: bool = False,
enable_phase_rerank: bool = True,
lattice_inference_mode: str = "search",
max_inferred_target_size: int | None = None,
observer: PipelineObserver | None = None,
) -> RunResult:
source = load_rgba(input_path)
@ -71,6 +73,8 @@ def run_pipeline(
solver_params=solver_params,
strip_background=strip_background,
enable_phase_rerank=enable_phase_rerank,
lattice_inference_mode=lattice_inference_mode,
max_inferred_target_size=max_inferred_target_size,
observer=observer,
)
@ -93,6 +97,8 @@ def run_pipeline_rgba(
solver_params: SolverHyperParams | None = None,
strip_background: bool = False,
enable_phase_rerank: bool = True,
lattice_inference_mode: str = "search",
max_inferred_target_size: int | None = None,
observer: PipelineObserver | None = None,
) -> RunResult:
started = time.perf_counter()
@ -122,15 +128,32 @@ def run_pipeline_rgba(
)
if fixed_dims is None:
check_observer_cancelled(observer)
if lattice_inference_mode == "autocorr":
inference_label = "Autocorr lattice"
inference_detail = "Taking one autocorr consensus size, probing phase once, and skipping size search."
elif lattice_inference_mode == "search":
inference_label = "Lattice search"
inference_detail = "Searching for the output size and phase that best fit the input."
else:
raise ValueError(f"Unknown lattice_inference_mode '{lattice_inference_mode}'.")
emit_observer(
observer,
"stage_started",
stage="inference",
label="Lattice search",
detail="Searching for the output size and phase that best fit the input.",
label=inference_label,
detail=inference_detail,
)
inference = infer_lattice(source, target_size=target_size, device=device, observer=observer)
inference_mode = "searched"
if lattice_inference_mode == "autocorr":
inference = infer_autocorr_lattice(
source,
max_target_size=max_inferred_target_size,
device=device,
observer=observer,
)
inference_mode = "autocorr"
else:
inference = infer_lattice(source, target_size=target_size, device=device, observer=observer)
inference_mode = "searched"
else:
check_observer_cancelled(observer)
emit_observer(
@ -273,6 +296,8 @@ def run_pipeline_rgba(
"device": device,
"strip_background": strip_background,
"enable_phase_rerank": enable_phase_rerank,
"lattice_inference_mode": lattice_inference_mode,
"max_inferred_target_size": max_inferred_target_size,
"inference_mode": inference_mode,
"solver_params": solver_params.to_dict(),
}

View file

@ -10,3 +10,4 @@
{"ts": "2026-04-26T15:46:23+00:00", "type": "research", "status": "rejected", "note": "Tried a projected edge-energy spectral lattice prior and removed it before ship. On tests/fixtures/real/dense-landscape.png it floor-hugged to a 2px rhythm and widened the candidate family toward bogus 626-size aliases, so the searched path is back to change-interval evidence plus autocorrelation."}
{"ts": "2026-04-26T17:46:30+00:00", "type": "research", "status": "ok", "note": "Removed pixel-walk / change-interval sizing from src/repixelizer/inference.py and switched searched lattice inference to autocorrelation-only priors and candidate hints. Badge auto inference no longer gets pruned into the old 115-family by spacing-spectrum guidance; it now searches inside the 120-126 neighborhood while dense landscape still lands at 418x418."}
{"ts": "2026-04-26T17:58:43+00:00", "type": "research", "status": "ok", "note": "Autocorr inference now keeps a tiny near-best lag plateau and searches for cross-axis consensus before collapsing to one lattice size. On the real badge fixture this preserves x-axis support for 10px alongside 11px, raises the shared prior to the 125-family, and restores auto inference to 125 with top candidates 125/126/124/127/123 while dense landscape remains 418x418."}
{"ts": "2026-04-26T18:19:52+00:00", "type": "design", "status": "accepted", "note": "Hosted demo jobs now use direct autocorr lattice inference with one cheap phase probe, capped by the hosted output limit. The hosted path no longer silently pins unspecified jobs to max_output_dimension, and phase rerank is forced off there while the full local workflow keeps searched autocorr inference available."}

View file

@ -65,6 +65,7 @@ accepted_design:
- `src/repixelizer/pipeline.py` owns orchestration and phase rerank selection.
- `src/repixelizer/analysis.py` provides the normalized edge scout used for loss and cleanup guidance.
- `src/repixelizer/phase_field.py` owns the one honest optimization variable: `disp_t`.
- Hosted demo jobs now use a direct autocorr lattice path: one consensus size, one cheap phase probe, capped by the hosted output limit, with phase rerank disabled.
- The optimizer starts from fixed lattice centers, uses a wider displacement budget than the earlier tighter leash, optimizes displacement with projected Adam, then rounds to real source pixels for final output.
- Cleanup is local and secondary; it does not define the core reconstruction model.
- `source_fidelity` is useful but incomplete; `source_structure` exists because visible structure can improve while lattice-only scoring complains.
@ -74,6 +75,7 @@ accepted_design:
landed_baseline:
- Lattice inference with fixed or searched target dimensions and optional low-confidence phase rerank.
- Hosted demo inference uses a direct autocorr size pick plus cheap phase probe instead of multi-size search, and it no longer silently pins unspecified jobs to `max_output_dimension`.
- Edge analysis plus one displacement-field optimizer with projected constraints.
- Compare mode, diagnostics writing, tuning, and GUI observer plumbing all working on the phase-field path.
- `source_structure` reporting alongside `source_fidelity`.
@ -90,5 +92,5 @@ rejected_paths:
reason: the projected edge-energy spectral pass floor-hugged to 2px / 626-family aliases on the dense landscape fixture and did not justify its complexity
current_status:
summary: The repo has one live reconstruction engine, `phase-field`, wired through the main pipeline with lattice inference, edge analysis, projected displacement optimization, cleanup, optional palette fitting, diagnostics, compare mode, tuning, and GUI observer events. The searched lattice path now trusts autocorrelation only; it keeps a tiny near-best lag plateau and uses cross-axis consensus before collapsing to one lattice family. The spectral prior experiment was removed after it floor-hugged to 2px / 626-family aliases on the dense landscape fixture, and the old change-interval / pixel-walk sizing path was cut after it kept pruning the badge away from the `126` family. The solver now has a wider displacement budget so moderate phase error can be recovered by the field instead of requiring as much rerank ceremony. `source_structure` now rides alongside `source_fidelity` because lattice-only scoring was lying about visible quality. The tracked weakness is still the widened dark contour near the badge sword tip on tapered shapes.
summary: The repo has one live reconstruction engine, `phase-field`, wired through the main pipeline with lattice inference, edge analysis, projected displacement optimization, cleanup, optional palette fitting, diagnostics, compare mode, tuning, and GUI observer events. The searched lattice path now trusts autocorrelation only; it keeps a tiny near-best lag plateau and uses cross-axis consensus before collapsing to one lattice family. The hosted demo path now skips multi-size lattice search and phase rerank entirely, taking one direct autocorr-consensus size plus the cheap inferred phase and respecting the hosted output cap instead of silently pinning unspecified jobs to that cap. The spectral prior experiment was removed after it floor-hugged to 2px / 626-family aliases on the dense landscape fixture, and the old change-interval / pixel-walk sizing path was cut after it kept pruning the badge away from the `126` family. The solver now has a wider displacement budget so moderate phase error can be recovered by the field instead of requiring as much rerank ceremony. `source_structure` now rides alongside `source_fidelity` because lattice-only scoring was lying about visible quality. The tracked weakness is still the widened dark contour near the badge sword tip on tapered shapes.
next_action: After rehydrate, if the user wants continued solver work, take one bounded hypothesis aimed at along-stroke versus across-stroke behavior near tapered contours in `src/repixelizer/phase_field.py`, validate it against the pinned badge case and `tests/fixtures/real/ai-badge-tip-focus.json`, and revert immediately if the visual result or `source_structure` does not clearly improve.

View file

@ -470,6 +470,46 @@ def test_gui_hosted_config_endpoint_exposes_demo_limits(monkeypatch, tmp_path: P
assert payload["ui"]["showQueuePanel"] is True
def test_hosted_job_options_use_direct_autocorr_and_skip_rerank() -> None:
from repixelizer.gui import HostedDemoConfig, _normalize_job_options
config = HostedDemoConfig(
hosted_demo=True,
show_queue_panel=True,
max_upload_bytes=1_048_576,
max_input_dimension=2048,
max_output_dimension=256,
default_steps=32,
max_steps=48,
queue_capacity=10,
heartbeat_interval_seconds=10,
stale_after_seconds=30,
phase_field_preview_stride=4,
spool_dir=Path("spool"),
)
options = _normalize_job_options(
config,
source_width=1254,
source_height=1254,
target_size=None,
target_width=None,
target_height=None,
phase_x=None,
phase_y=None,
steps=None,
seed=7,
device="auto",
strip_background=False,
skip_phase_rerank=False,
)
assert options["target_size"] is None
assert options["lattice_inference_mode"] == "autocorr"
assert options["max_inferred_target_size"] == 256
assert options["skip_phase_rerank"] is True
def test_gui_queue_panel_defaults_off_for_local_runs_and_can_be_forced(monkeypatch, tmp_path: Path) -> None:
from repixelizer.gui import create_app
@ -554,6 +594,51 @@ def test_gui_queue_rejects_eleventh_waiting_job(monkeypatch, tmp_path: Path) ->
assert "Queue is full" in getattr(rejected, "detail", str(rejected))
def test_gui_hosted_jobs_dispatch_direct_autocorr_pipeline(monkeypatch, tmp_path: Path) -> None:
from repixelizer.gui import create_app
release = threading.Event()
calls: list[dict[str, object]] = []
def fake_run_pipeline_rgba(*args, **kwargs):
calls.append(kwargs)
release.wait(timeout=2.0)
monkeypatch.setenv("REPIXELIZER_HOSTED_DEMO", "1")
monkeypatch.setenv("REPIXELIZER_SPOOL_DIR", str(tmp_path / "spool"))
monkeypatch.setattr("repixelizer.gui.run_pipeline_rgba", fake_run_pipeline_rgba)
upload = _png_bytes()
app = create_app()
create_job = _route_endpoint(app, "/api/jobs", "POST")
created = asyncio.run(
create_job(
image=UploadFile(filename="tiny.png", file=io.BytesIO(upload)),
target_size=None,
target_width=None,
target_height=None,
phase_x=None,
phase_y=None,
steps=None,
seed=7,
device="auto",
strip_background=False,
skip_phase_rerank=False,
)
)
deadline = time.time() + 2.0
while not calls and time.time() < deadline:
time.sleep(0.05)
release.set()
assert created.status_code == 200
assert calls
assert calls[0]["lattice_inference_mode"] == "autocorr"
assert calls[0]["max_inferred_target_size"] == 256
assert calls[0]["enable_phase_rerank"] is False
assert calls[0]["target_size"] is None
def test_gui_canceling_queued_job_cleans_spool_file(monkeypatch, tmp_path: Path) -> None:
from repixelizer.gui import create_app

View file

@ -15,6 +15,7 @@ from repixelizer.inference import (
_hint_target_sizes_from_autocorr,
_resolve_candidate_dims_from_autocorr,
_top_candidates_by_size,
infer_autocorr_lattice,
infer_fixed_lattice,
infer_lattice,
)
@ -135,6 +136,14 @@ def test_badge_autocorr_consensus_stays_in_canonical_family() -> None:
assert result.top_candidates[0].target_width in {125, 126}
def test_direct_badge_autocorr_consensus_stays_in_canonical_family() -> None:
rgba = load_rgba(Path("tests/fixtures/real/ai-badge-cleaned.png"))
result = infer_autocorr_lattice(rgba, device="cpu")
assert result.target_width in {125, 126}
assert result.top_candidates
assert all(candidate.target_width == result.target_width for candidate in result.top_candidates)
def test_top_candidates_are_diversified_by_size() -> None:
candidates = [
InferenceCandidate(target_width=113, target_height=113, phase_x=0.0, phase_y=0.0, score=0.90, breakdown={}),

View file

@ -470,6 +470,70 @@ def test_pipeline_fixed_target_and_phase_skip_search_inference(tmp_path: Path, m
assert result.inference.phase_y == -0.2
def test_pipeline_autocorr_mode_skips_size_search(tmp_path: Path, monkeypatch) -> None:
source = np.zeros((6, 6, 4), dtype=np.float32)
source[..., 3] = 1.0
input_path = tmp_path / "input.png"
output_path = tmp_path / "output.png"
from repixelizer.io import save_rgba
save_rgba(input_path, source)
inference = InferenceResult(
target_width=3,
target_height=3,
phase_x=0.2,
phase_y=-0.2,
confidence=1.0,
top_candidates=[
InferenceCandidate(target_width=3, target_height=3, phase_x=0.2, phase_y=-0.2, score=0.9, breakdown={})
],
)
class DummyAnalysis:
edge_map = np.zeros((6, 6), dtype=np.float32)
def fail_infer(*args, **kwargs):
raise AssertionError("searched lattice inference should not run in autocorr mode")
def fake_infer_autocorr(*args, **kwargs):
assert kwargs["max_target_size"] == 5
return inference
def fake_optimize_phase_field(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(
target_rgba=rgba,
uv_field=np.zeros((inference.target_height, inference.target_width, 2), dtype=np.float32),
guidance_strength=np.zeros((inference.target_height, inference.target_width), dtype=np.float32),
initial_rgba=rgba.copy(),
loss_history=[],
)
monkeypatch.setattr("repixelizer.pipeline.infer_lattice", fail_infer)
monkeypatch.setattr("repixelizer.pipeline.infer_autocorr_lattice", fake_infer_autocorr)
monkeypatch.setattr("repixelizer.pipeline.analyze_phase_field_source", lambda *args, **kwargs: DummyAnalysis())
monkeypatch.setattr("repixelizer.pipeline.optimize_phase_field", fake_optimize_phase_field)
result = run_pipeline(
input_path,
output_path,
enable_phase_rerank=False,
lattice_inference_mode="autocorr",
max_inferred_target_size=5,
steps=0,
device="cpu",
)
assert output_path.exists()
assert result.inference.target_width == 3
assert result.inference.target_height == 3
assert result.inference.phase_x == 0.2
assert result.inference.phase_y == -0.2
def test_phase_rerank_can_accept_low_confidence_size_jump_with_strong_support(monkeypatch) -> None:
source = np.zeros((16, 16, 4), dtype=np.float32)
candidate_a = InferenceCandidate(target_width=16, target_height=16, phase_x=0.0, phase_y=0.0, score=0.91, breakdown={})