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`
2026-05-30 23:53:04 +01:00
- `src/repixelizer/spritesheet.py`
2026-04-24 00:23:32 +01:00
- `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-29 11:43:17 +01:00
The remaining helpers load inputs, write diagnostics, or carry this state across
2026-05-30 23:53:04 +01:00
the CLI, GUI, spritesheet wrapper, and comparison harness.
## Spritesheet wrapper
### Source
- `run_spritesheet(...)` and `detect_sprite_regions(...)` in `src/repixelizer/spritesheet.py`
- CLI dispatch in `src/repixelizer/cli.py`
### Ownership
2026-05-31 02:14:03 +01:00
Spritesheet mode owns sprite-region identity, sheet-level texel density
selection, optional individual exports, and output packing only. It does not own
reconstruction, cleanup, palette fitting, or metrics.
2026-05-30 23:53:04 +01:00
### What the source actually does
Spritesheet mode:
1. Loads one source sheet.
2026-05-31 02:14:03 +01:00
2. If `--sheet-columns` and `--sheet-rows` are supplied, slices the sheet into
full grid cells in reading order. This is the conservative path for
character sheets where limbs, weapons, shadows, and effects can be
disconnected.
3. Otherwise builds a foreground mask from alpha, or from edge-connected background
2026-05-30 23:53:04 +01:00
stripping when `--strip-background` is requested. Fully opaque sheets use a
fast border-color foreground guess by default.
2026-05-31 02:14:03 +01:00
4. Detects connected foreground regions, optionally selecting the largest
2026-05-30 23:53:04 +01:00
requested `--sprite-count` .
2026-05-31 02:14:03 +01:00
5. Sorts regions in reading order.
6. When no explicit target size is supplied, probes each crop with autocorr
2026-05-31 00:41:18 +01:00
inference, chooses the median source-pixels-per-texel density for the whole
sheet, and pins each crop to dimensions derived from that shared density.
2026-05-31 02:14:03 +01:00
7. Crops each region and calls `run_pipeline_rgba(...)` on that crop.
8. Writes individual sprite PNGs when `--export-sprites-dir` is supplied.
9. Packs per-sprite outputs back into source-row order and writes a
2026-05-30 23:53:04 +01:00
`spritesheet.json` diagnostic summary when diagnostics are enabled.
2026-05-31 00:41:18 +01:00
Pinned target width, height, or size applies to each sprite crop and disables
shared-density sizing. The whole sheet never receives one shared lattice, but
automatic sheet mode does use one shared texel density so sub-sprites do not
silently converge on half- or double-scale rulers.
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:43:17 +01:00
`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:43:17 +01:00
The live signal is the flatness map described above; diagnostic edge preview remains separate from solver steering.
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:43:17 +01:00
This is feedback for the GUI and logs.
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:43:17 +01:00
The emitted image is made of actual source pixels.
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:43:17 +01:00
Future solver changes should preserve the current model unless the design map is updated first: 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`