2026-04-24 00:23:32 +01:00
# Phase-Field Algorithm Map
2026-04-23 23:09:35 +01:00
2026-04-24 00:23:32 +01:00
## What this file is
2026-04-23 23:09:35 +01:00
2026-04-29 11:24:31 +01:00
This is the source-grounded map of the live `phase-field` reconstruction machine.
2026-04-23 23:09:35 +01:00
2026-04-29 11:24:31 +01:00
The current spine is:
`source image -> lattice inference -> diagnostic edge preview -> fixed lattice centers -> hierarchical flatness signal -> explicit local search + lattice relaxation -> nearest source sample -> cleanup / diagnostics`
2026-04-23 23:09:35 +01:00
2026-04-24 00:23:32 +01:00
The relevant source lives in:
2026-04-23 23:09:35 +01:00
2026-04-24 00:23:32 +01:00
- `src/repixelizer/pipeline.py`
- `src/repixelizer/inference.py`
- `src/repixelizer/analysis.py`
- `src/repixelizer/phase_field.py`
- `src/repixelizer/discrete.py`
- `src/repixelizer/diagnostics.py`
- `src/repixelizer/metrics.py`
2026-04-23 23:09:35 +01:00
2026-04-24 00:23:32 +01:00
## One-sentence machine
2026-04-23 23:09:35 +01:00
2026-04-29 11:24:31 +01:00
The solver lays a regular grid over the source, lets each cell look in a small local window for the flattest nearby source position, blends toward that local preference, relaxes the lattice back toward coherent spacing, then samples real source pixels once and goes home.
2026-04-23 23:24:28 +01:00
2026-04-24 00:23:32 +01:00
## Core state
2026-04-23 23:24:28 +01:00
2026-04-24 00:23:32 +01:00
These are the pieces that actually matter:
2026-04-23 23:24:28 +01:00
2026-04-29 11:24:31 +01:00
- `InferenceResult` : chosen lattice width, height, confidence, and optional top candidates
- `edge_map` : normalized luminance/alpha diagnostic preview from `analysis.py`
- `uv0_px` : fixed source-space center for each output cell before movement
- `signal_map` : hierarchical flatness map built in `phase_field.py`
- `pos` : live source-space sample positions, initialized from `uv0_px`
- `disp = pos - uv0_px` : reported displacement field derived from live positions
- `target_rgba` : final output grid after nearest-source sampling
2026-04-23 23:24:28 +01:00
2026-04-24 00:26:07 +01:00
Everything else is support scaffolding.
2026-04-24 00:23:32 +01:00
## Diagram
2026-04-23 23:09:35 +01:00
2026-04-24 00:23:32 +01:00
```mermaid
flowchart TD
2026-04-29 11:24:31 +01:00
A["Source RGBA"] --> B["Stage 0: Lattice inference"]
B --> C["Stage 1: Diagnostic edge preview"]
C --> D["Stage 2: Prep fixed centers + signal"]
D --> E["uv0_px fixed centers<br/>pos = uv0_px + tiny seed noise"]
E --> F["Stage 3: Local candidate search<br/>25 nearby positions per cell"]
F --> G["Stage 4: Blend toward preferred positions"]
G --> H["Stage 5: Lattice spring relaxation"]
H -->|repeat for solver steps| F
H --> I["Stage 6: Final nearest source sample"]
I --> J["Stage 7: Cleanup / palette / diagnostics"]
2026-04-24 00:23:32 +01:00
```
2026-04-23 23:09:35 +01:00
2026-04-29 11:24:31 +01:00
## Stable core
2026-04-23 23:09:35 +01:00
2026-04-29 11:24:31 +01:00
- one chosen ruler after inference
- one live position field
- one direct hierarchical flatness signal
- one local candidate choice per cell per step
- one lattice relaxation pass to keep spacing coherent
- honest final nearest-source sampling
2026-04-23 23:09:35 +01:00
2026-04-29 11:24:31 +01:00
The current live solver is not projected Adam anymore. The old differentiable loss scaffolding has been removed; `optimize_phase_field(...)` is the explicit NumPy local-search path.
2026-04-24 00:23:32 +01:00
## Stage 0: The pipeline chooses the ruler
2026-04-23 23:09:35 +01:00
2026-04-24 00:23:32 +01:00
### Source
2026-04-23 23:09:35 +01:00
2026-04-24 00:23:32 +01:00
- `run_pipeline(...)` in `src/repixelizer/pipeline.py`
- `_resolve_requested_target_dims(...)` in `src/repixelizer/pipeline.py`
2026-04-29 11:24:31 +01:00
- `infer_fixed_lattice(...)` and `infer_autocorr_lattice(...)` in `src/repixelizer/inference.py`
2026-04-28 14:55:35 +01:00
- `_select_candidate_with_reconstruction(...)` in `src/repixelizer/pipeline.py`
2026-04-23 23:09:35 +01:00
2026-04-24 00:23:32 +01:00
### Inputs
2026-04-23 23:09:35 +01:00
2026-04-24 00:23:32 +01:00
- source RGBA image
2026-04-28 14:55:35 +01:00
- optional pinned target size / width / height
2026-04-29 11:24:31 +01:00
- optional candidate rerank for low-confidence autocorr candidates
2026-04-23 23:09:35 +01:00
2026-04-24 00:23:32 +01:00
### Outputs
2026-04-23 23:09:35 +01:00
2026-04-24 00:23:32 +01:00
- `InferenceResult`
- `target_width`
- `target_height`
2026-04-29 11:24:31 +01:00
- `confidence`
2026-04-24 00:23:32 +01:00
- `top_candidates`
2026-04-23 23:09:35 +01:00
2026-04-24 00:23:32 +01:00
### What the source actually does
2026-04-23 23:09:35 +01:00
2026-04-29 11:24:31 +01:00
The pipeline decides whether the lattice is:
2026-04-23 23:09:35 +01:00
2026-04-29 11:24:31 +01:00
- fixed explicitly by the caller, using `infer_fixed_lattice(...)`
- inferred from autocorr consensus, using `infer_autocorr_lattice(...)`
2026-04-23 23:09:35 +01:00
2026-04-29 11:24:31 +01:00
The automatic path estimates rough cell spacing from source edge-profile autocorrelation, keeps a tiny near-best lag plateau long enough to find cross-axis consensus, then surfaces multiple candidate lattice sizes.
2026-04-23 23:09:35 +01:00
2026-04-29 11:24:31 +01:00
Low-confidence autocorr candidates can still be reranked through a short preview solve. There is no separate searched-inference path in the live pipeline.
2026-04-24 00:23:32 +01:00
### Metaphor
2026-04-29 11:24:31 +01:00
This is the survey crew laying graph paper over the mural. Autocorr proposes a small pile of plausible rulers; the preview reranker may make them argue before the workers show up.
2026-04-24 00:23:32 +01:00
2026-04-29 11:24:31 +01:00
## Stage 1: The diagnostic edge preview builds a gauge
2026-04-24 00:23:32 +01:00
### Source
- `analyze_phase_field_source(...)` in `src/repixelizer/analysis.py`
- `_compute_edge_map(...)` and `_compute_edge_map_torch(...)` in `src/repixelizer/analysis.py`
### Outputs
- `PhaseFieldSourceAnalysis(edge_map=...)`
### What the source actually does
2026-04-23 23:09:35 +01:00
2026-04-29 11:24:31 +01:00
The analysis stage computes one normalized `edge_map` from luminance and alpha differences. It marks where the source breaks into edges for diagnostics and GUI preview. It does not steer the solver; the active local-search solver follows the separate hierarchical flatness signal built directly from the source image.
2026-04-23 23:09:35 +01:00
### Metaphor
2026-04-29 11:24:31 +01:00
This stage walks the mural with chalk and circles cracks, seams, and paint jumps.
2026-04-24 00:23:32 +01:00
2026-04-29 11:24:31 +01:00
## Stage 2: Prep builds fixed centers and the flatness signal
2026-04-23 23:09:35 +01:00
2026-04-24 00:23:32 +01:00
### Source
2026-04-23 23:09:35 +01:00
2026-04-24 00:23:32 +01:00
- `_prepare_phase_field(...)` in `src/repixelizer/phase_field.py`
2026-04-24 00:03:05 +01:00
- `_make_regular_uv_px(...)` in `src/repixelizer/phase_field.py`
2026-04-29 11:24:31 +01:00
- `_source_signal_map_np(...)` in `src/repixelizer/phase_field.py`
2026-04-24 00:23:32 +01:00
- `premultiply(...)` in `src/repixelizer/io.py`
2026-04-23 23:09:35 +01:00
2026-04-24 00:23:32 +01:00
### What the source actually does
2026-04-29 11:24:31 +01:00
Prep:
2026-04-23 23:09:35 +01:00
2026-04-29 11:24:31 +01:00
1. Premultiplies source RGBA so transparent color does not lie.
2. Computes `cell_x` and `cell_y` from source size and target size.
3. Places canonical fixed centers in `uv0_px` .
4. Builds support tensors used by diagnostics and observer snapshots.
5. Builds `signal_map` , the live local-search attraction field.
2026-04-23 23:09:35 +01:00
2026-04-29 11:24:31 +01:00
The current signal path is intentionally simple:
2026-04-23 23:09:35 +01:00
2026-04-29 11:24:31 +01:00
- compute derivative energy from luminance gradient plus Laplacian magnitude at each pyramid level
- upsample each level back to source resolution
- accumulate the levels with `phase_field_signal_level_decay`
- normalize the accumulated derivative energy once
- invert it so low-gradient / low-curvature regions become attractive flatness
- sharpen the flatness map with `phase_field_signal_peak_power`
2026-04-23 23:09:35 +01:00
2026-04-29 11:24:31 +01:00
What no longer happens:
2026-04-24 00:23:32 +01:00
2026-04-29 11:24:31 +01:00
- no boundary-context reward
- no wall mask
- no distance transform
- no ring/context/self-penalty signal knobs
- no lattice phase concept
2026-04-23 23:09:35 +01:00
### Metaphor
2026-04-29 11:24:31 +01:00
This is the loading dock. The source gets wrapped, the ruler becomes fixed anchor points, and the solver gets a heat map where quiet paint is warm and edges are cold.
2026-04-23 23:09:35 +01:00
2026-04-29 11:24:31 +01:00
## Stage 3: The field starts from the ruler
2026-04-23 23:09:35 +01:00
2026-04-24 00:23:32 +01:00
### Source
2026-04-23 23:09:35 +01:00
2026-04-24 00:23:32 +01:00
- `optimize_phase_field(...)` in `src/repixelizer/phase_field.py`
2026-04-23 23:09:35 +01:00
2026-04-24 00:23:32 +01:00
### What the source actually does
2026-04-23 23:09:35 +01:00
2026-04-29 11:24:31 +01:00
The live position field starts as:
2026-04-23 23:09:35 +01:00
2026-04-29 11:24:31 +01:00
- `pos = uv0_px`
2026-04-24 00:23:32 +01:00
2026-04-29 11:24:31 +01:00
Diagnostics emit the zero-displacement image from those canonical centers. Then a tiny seeded noise is added to `pos` so ties in the local search do not all break in perfectly identical ways.
2026-04-23 23:09:35 +01:00
### Metaphor
2026-04-29 11:24:31 +01:00
Every worker starts with boots under shoulders, then gets the slightest shove so a perfectly symmetric room does not trap everyone in the same argument forever.
2026-04-24 00:23:32 +01:00
2026-04-29 11:24:31 +01:00
## Stage 4: Local search chooses preferred positions
2026-04-24 00:23:32 +01:00
### Source
2026-04-29 11:24:31 +01:00
- `_choose_local_preferred_positions_np(...)` in `src/repixelizer/phase_field.py`
- `_candidate_grid_score_np(...)` in `src/repixelizer/phase_field.py`
2026-04-23 23:09:35 +01:00
2026-04-24 00:23:32 +01:00
### What the source actually does
2026-04-23 23:09:35 +01:00
2026-04-29 11:24:31 +01:00
Every `phase_field_local_search_interval` steps, each sample checks a 5x5 candidate grid around its current position.
2026-04-24 00:23:32 +01:00
2026-04-29 11:24:31 +01:00
The candidate window size is:
2026-04-24 00:23:32 +01:00
2026-04-29 11:24:31 +01:00
- `radius_x = phase_field_local_search_radius_ratio * cell_x`
- `radius_y = phase_field_local_search_radius_ratio * cell_y`
2026-04-24 00:23:32 +01:00
2026-04-29 11:24:31 +01:00
Each candidate is scored as:
2026-04-24 00:23:32 +01:00
2026-04-29 11:24:31 +01:00
```text
move_score * phase_field_local_search_move_weight
+ grid_score * phase_field_local_search_grid_weight
- signal_score * phase_field_signal_weight
```
2026-04-23 23:09:35 +01:00
Meaning:
2026-04-29 11:24:31 +01:00
- `signal_score` rewards candidates in flat coherent source regions
- `move_score` discourages jumping to the edge of the search window just because it can
- `grid_score` optionally asks candidates to preserve local row/column coherence relative to immediate neighbors
2026-04-23 23:09:35 +01:00
2026-04-29 11:24:31 +01:00
The best candidate becomes that cell's preferred position.
2026-04-23 23:09:35 +01:00
### Metaphor
2026-04-29 11:24:31 +01:00
Each worker looks around a tiny voting booth, asks which nearby floor tile feels calmest, and points at that tile. If the booth is too large, workers poach each other's snacks. If it is too small, nobody reaches the useful tile.
2026-04-23 23:09:35 +01:00
2026-04-29 11:24:31 +01:00
## Stage 5: Blend and lattice relaxation move the field
2026-04-23 23:09:35 +01:00
2026-04-24 00:23:32 +01:00
### Source
2026-04-23 23:09:35 +01:00
2026-04-29 11:24:31 +01:00
- `optimize_phase_field(...)` in `src/repixelizer/phase_field.py`
- `_relax_lattice_springs_np(...)` in `src/repixelizer/phase_field.py`
- `_explicit_solver_terms_np(...)` in `src/repixelizer/phase_field.py`
2026-04-24 00:23:32 +01:00
2026-04-29 11:24:31 +01:00
### What the source actually does
2026-04-24 00:23:32 +01:00
2026-04-29 11:24:31 +01:00
After preferred positions are chosen:
2026-04-24 00:23:32 +01:00
2026-04-29 11:24:31 +01:00
```text
pos = pos * (1 - phase_field_local_search_blend) + preferred * phase_field_local_search_blend
```
2026-04-24 00:23:32 +01:00
2026-04-29 11:24:31 +01:00
Then `_relax_lattice_springs_np(...)` runs three spring iterations. Horizontal neighbors are pushed toward one `cell_x` of x-spacing and same-row y-alignment. Vertical neighbors are pushed toward one `cell_y` of y-spacing and same-column x-alignment.
2026-04-23 23:09:35 +01:00
2026-04-29 11:24:31 +01:00
The spring step is capped:
2026-04-23 23:09:35 +01:00
2026-04-29 11:24:31 +01:00
```text
spring_step = min(0.22, phase_field_grid_alignment_weight * 0.06)
```
2026-04-23 23:09:35 +01:00
2026-04-29 11:24:31 +01:00
So grid weights above the cap are equivalent for spring strength, though they still appear in diagnostic loss terms.
2026-04-23 23:09:35 +01:00
2026-04-29 11:24:31 +01:00
`_explicit_solver_terms_np(...)` records a compact diagnostic loss:
2026-04-24 00:23:32 +01:00
2026-04-29 11:24:31 +01:00
```text
grid_alignment * phase_field_grid_alignment_weight
- local_signal * phase_field_signal_weight
```
2026-04-24 00:23:32 +01:00
2026-04-29 11:24:31 +01:00
This is feedback for the GUI and logs. It is not a differentiable Adam objective.
2026-04-23 23:09:35 +01:00
### Metaphor
2026-04-29 11:24:31 +01:00
Workers take a measured step toward their preferred tile. Then the graph paper tugs everyone back into rows and columns before the next vote.
2026-04-23 23:09:35 +01:00
2026-04-29 11:24:31 +01:00
## Stage 6: Final sampling uses real source pixels
2026-04-23 23:09:35 +01:00
2026-04-24 00:23:32 +01:00
### Source
2026-04-23 23:09:35 +01:00
2026-04-24 00:23:32 +01:00
- final section of `optimize_phase_field(...)`
2026-04-29 11:24:31 +01:00
- `_nearest_source_rgba(...)` in `src/repixelizer/phase_field.py`
2026-04-24 00:23:32 +01:00
### What the source actually does
2026-04-23 23:09:35 +01:00
2026-04-24 00:23:32 +01:00
After the loop:
2026-04-23 23:09:35 +01:00
2026-04-29 11:24:31 +01:00
1. Round final `pos` to integer source pixels.
2. Index the original source RGBA.
3. Report `final_disp = pos - uv0_px` and normalized `uv_field` .
2026-04-23 23:09:35 +01:00
2026-04-29 11:24:31 +01:00
The emitted image is made of actual source pixels, not bilinear soup.
2026-04-23 23:09:35 +01:00
### Metaphor
2026-04-29 11:24:31 +01:00
During solving, workers can consider nearby tiles. At the end, everyone must plant a flag on one real brick in the wall.
2026-04-23 23:09:35 +01:00
2026-04-29 11:24:31 +01:00
## Stage 7: The pipeline does cleanup, palette, and diagnostics
2026-04-23 23:09:35 +01:00
2026-04-24 00:23:32 +01:00
### Source
2026-04-23 23:09:35 +01:00
2026-04-24 00:23:32 +01:00
- `cleanup_pixels(...)` in `src/repixelizer/discrete.py`
- palette handling in `src/repixelizer/pipeline.py` and `src/repixelizer/palette.py`
- diagnostics writing in `src/repixelizer/diagnostics.py`
### What the source actually does
2026-04-23 23:09:35 +01:00
2026-04-24 00:23:32 +01:00
After reconstruction:
2026-04-23 23:09:35 +01:00
2026-04-29 11:24:31 +01:00
1. `cleanup_pixels(...)` may run, though defaults usually leave cleanup as a no-op.
2026-04-24 00:23:32 +01:00
2. Optional palette quantization runs.
2026-04-29 11:24:31 +01:00
3. Diagnostics are written: comparisons, overlays, alpha preview, displacement previews, and `run.json` .
2026-04-24 00:23:32 +01:00
2026-04-29 11:24:31 +01:00
Cleanup is secondary. It is not the real solver.
2026-04-23 23:09:35 +01:00
2026-04-24 00:23:32 +01:00
## Stage 8: The machine judges itself
2026-04-23 23:09:35 +01:00
2026-04-24 00:23:32 +01:00
### Source
2026-04-23 23:09:35 +01:00
2026-04-24 00:23:32 +01:00
- `summarize_run(...)` in `src/repixelizer/diagnostics.py`
2026-04-23 23:09:35 +01:00
- `source_lattice_consistency_breakdown(...)` in `src/repixelizer/metrics.py`
2026-04-24 00:23:32 +01:00
- `source_structure_breakdown(...)` in `src/repixelizer/metrics.py`
- `_displacement_diagnostics(...)` in `src/repixelizer/phase_field.py`
2026-04-23 23:09:35 +01:00
2026-04-24 00:23:32 +01:00
### What the source actually does
2026-04-23 23:09:35 +01:00
2026-04-29 11:24:31 +01:00
The repo keeps two score families:
2026-04-24 00:23:32 +01:00
2026-04-29 11:24:31 +01:00
- `source_fidelity` : agreement with a lattice-derived source reference
- `source_structure` : visible structural agreement at source size
2026-04-24 00:23:32 +01:00
2026-04-29 11:24:31 +01:00
The solver also reports displacement statistics and the explicit local-search terms.
2026-04-23 23:09:35 +01:00
### Metaphor
2026-04-29 11:24:31 +01:00
The inspection bench judges both whether the output agrees with the inferred grid and whether visible linework survived the trip.
2026-04-24 00:23:32 +01:00
2026-04-29 11:24:31 +01:00
## Current tuned surface
2026-04-23 23:09:35 +01:00
2026-04-29 11:24:31 +01:00
The current shippable trial config is documented in `docs/solver-tuning-lessons-2026-04-29.md` and lives in `config/solver_params.json` .
2026-04-23 23:09:35 +01:00
2026-04-29 11:24:31 +01:00
The important tuning lesson:
2026-04-23 23:09:35 +01:00
2026-04-29 11:24:31 +01:00
- wide local-search windows caused feature poaching
- the useful radius basin is around `0.20-0.22`
- `radius=0.215` and `blend=0.24` are the current focused winner
- signal knobs still need a disciplined follow-up pass around that new motion basin
2026-04-23 23:09:35 +01:00
2026-04-24 00:26:07 +01:00
## Guardrails
2026-04-23 23:09:35 +01:00
2026-04-29 11:24:31 +01:00
These categories of complexity should not come back casually:
2026-04-26 04:57:30 +01:00
2026-04-29 11:24:31 +01:00
- lattice phase search or explicit lattice phase controls
- per-cell candidate trays that persist across stages
- extra source-derived reference rasters steering the field directly
- multiple sequential solver religions with separate objectives
- cleanup pretending to be the real reconstruction engine
2026-04-26 04:57:30 +01:00
2026-04-29 11:24:31 +01:00
The point of this machine is still one ruler, one position field, one local signal, one lattice relaxation, and one final source sample.
2026-04-26 04:57:30 +01:00
2026-04-29 11:24:31 +01:00
## Documentation boundaries
2026-04-26 04:57:30 +01:00
- live control-flow truth belongs here
2026-04-29 11:24:31 +01:00
- current shipping state and re-entry instructions belong in `notes/fresh-workspace-handoff.md`
2026-04-26 04:57:30 +01:00
- durable project truth belongs in `state/map.yaml`
2026-04-29 11:24:31 +01:00
- detailed tuning lessons belong in `docs/solver-tuning-lessons-2026-04-29.md`
- forward work belongs in `docs/implementation-plan.md`