Skip to content

refactor(atlas): migrate to public warm_solve + golden the placement decisions#35

Merged
jowch merged 2 commits into
mainfrom
atlas-warm-solve-migration
Jun 18, 2026
Merged

refactor(atlas): migrate to public warm_solve + golden the placement decisions#35
jowch merged 2 commits into
mainfrom
atlas-warm-solve-migration

Conversation

@jowch

@jowch jowch commented Jun 18, 2026

Copy link
Copy Markdown
Owner

What

Now that MakieTextRepel shipped the public warm-start primitive we filed #24 for (PR #27, exported warm_solve), this does two things:

  1. Migrate the Atlas off the internal-solver reach-through to the public API.
  2. Golden the solver's placement decisions — newly possible because #G — CairoMakie Map Feature Page (examples/map_feature/) #27 guarantees+documents solver determinism.

Migration (placement-preserving)

Verified placement-preserving: a byte-identical hero re-render (same sha256, 241,054 bytes) plus the unchanged geometric golden. So the public API produces exactly the same placement — zero behavioral change.

Placement golden (machine-stable)

The existing geometric golden deliberately avoids solver offsets because they flow through Makie's projection, which isn't bit-stable across versions. So instead of hashing raw offsets, placement_rows/atlas_placement_digest hash the solver's discrete decisions — which side each label leans (offset quadrant) + dropped — from the real cold solve:

  • Faithful to the actual map, but a quadrant only flips on a genuine re-placement, so it stays machine-independent like the geometric table.
  • Offset magnitude / frame-to-frame continuity remains covered by test_loop.jl's warm-start delta bound (<25px/frame).
  • New baseline test/golden/atlas-placement.sha256 (+ .rows.txt), UPDATE_GOLDEN=1 to regenerate.

Tests

Full suite 295/295. (Manifest stays gitignored; CI's clean instantiate fetches the new rev.)

Notes

🤖 Generated with Claude Code

…decisions

MakieTextRepel shipped the public warm-start primitive we filed #24 for (PR #27,
exported `warm_solve`), so the Atlas no longer needs to reach into unexported internals.

Migration (placement-preserving):
- Atlas.jl: `using MakieTextRepel: warm_solve` (was ProjectionSolver/RepelParams/solve_cluster).
- place.jl: drop the _PARAMS/_SOLVER objects; call `warm_solve(anchors, sizes, bounds; …,
  _SOLVE_KW...)` with the same params. warm_solve is a thin face over the same seam, so output
  is identical — verified by a BYTE-IDENTICAL hero re-render (same sha256) and the unchanged
  geometric golden.
- Project.toml: pin rev → 90f87ed3 (past #27/#28). Comments across src updated solve_cluster→warm_solve.

Placement golden (now possible — #27 guarantees+documents solver determinism):
- golden.jl: `placement_rows`/`atlas_placement_digest` hash the solver's DISCRETE decisions
  (which side each label leans = offset quadrant, + dropped) from the real COLD solve. Hashing
  the side, not the pixel offset, keeps it machine-stable (a quadrant only flips on a genuine
  re-placement) — same machine-independence as the geometric golden. Magnitude/continuity stays
  covered by test_loop's warm-start delta bound.
- New baseline test/golden/atlas-placement.sha256 (+ .rows.txt).

Full suite 295/295.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@jowch jowch left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — round 1

Clean, behavior-preserving refactor plus a well-constructed placement golden. The solve_cluster(_SOLVER, …)warm_solve(…) migration is exact: warm_solve is a thin pass-through to the same solve_cluster(ProjectionSolver(params), anchors, sizes, bounds; init_state, pin_mask, pinned_offsets, obstacles) seam, and place.jl passes its solver params (box_padding=5.0, point_padding=5.5, min_segment_length=2.0, only_move=:both) explicitly, so it's insulated from any drift in warm_solve's own defaults. The new golden hashes discrete side/dropped decisions (sign-quadrant + Int(dropped)), not pixels — deterministic and meaningful. I independently reproduced the committed 475dfc52… sha256 from atlas-placement.rows.txt and ran the full atlas suite (295/295 pass) against the committed sha. Measure-once/layout-many and the no-kerning rule are untouched. No blocking findings.

Nits

  • examples/atlas/src/golden.jl:115placement_rows' determinism silently depends on the mutable global _PIN_SLO[] (render.jl:106) staying true. Nothing in the test path flips it, but passing the pin flag explicitly (or a one-line comment noting the dependency) would harden the golden against a future test that mutates the Ref.
  • examples/atlas/src/golden.jl:14-17 / examples/atlas/test/test_golden.jl:43-45 — the "machine-stable" framing leans on the same "needs no Makie" argument as the geometric atlas_digest, but placement_rows routes anchors through the live Makie camera (assemble_frame_data_to_pxMakie.project). The digest is robust to sub-pixel projection drift, not independent of Makie — a label near a quadrant boundary could flip under a projection change. Wording should say "robust to sub-pixel projection drift" rather than implying full Makie-independence.
  • examples/atlas/README.md:28,55,68 — still reference solve_cluster; now contradicts place.jl's public-warm_solve reality. Minor doc drift (the archival prep/spike_timing.jl keeping the old name is fine).

- golden.jl: harden placement_rows against the mutable _PIN_SLO[] global it depends on —
  explicitly pin it to the production value (true) so a future test mutating the Ref can't
  perturb the golden. No-op on the digest (it was already true; baseline unchanged).
- golden.jl + test_golden.jl: fix the "machine-stable / machine-independent like the geometric
  golden" overclaim. placement_rows DOES route anchors through Makie's projection (via
  assemble_frame), unlike the fully-affine geometric table — so the accurate claim is "robust
  to sub-pixel projection drift" (a quadrant only flips on a real re-placement), not Makie-
  independent. Reworded the header, docstrings, and the testset name.
- README.md: solve_cluster → warm_solve at the three remaining mentions (now matches place.jl).

Atlas suite 295/295; placement digest unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jowch

jowch commented Jun 18, 2026

Copy link
Copy Markdown
Owner Author

Round 1 — addressing the nits

Pushed 0f33077. All three valid; fixed all three. Atlas suite 295/295, placement digest unchanged.

Fixed

  • _PIN_SLO[] dependency (golden.jl) — placement_rows now explicitly pins _PIN_SLO[] = true (the production value assemble_frame reads) before solving, so a future test that leaves the global Ref flipped can't perturb the golden. It was already true, so this is a no-op on the digest (baseline 475dfc52… unchanged) — pure hardening.
  • "machine-stable" overclaim (golden.jl header + docstrings, test_golden.jl comment + testset name) — you're right: placement_rows routes anchors through Makie.project via assemble_frame, unlike the fully-affine geometric table. Corrected throughout to "robust to sub-pixel projection drift" (a quadrant only flips on a real re-placement), and made explicit that it is not Makie-independent like the geometric one. The robustness argument stands; the wording no longer overclaims the mechanism.
  • README solve_cluster drift — updated lines 28/55/68 to warm_solve, matching place.jl. (Left prep/spike_timing.jl as the archival record of the original spike, per your note.)

@jowch jowch left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — round 2

All three round-1 nits addressed in 0f33077, no regressions. Scoped post-approval re-review of the incremental diff (golden.jl, test_golden.jl, README.md).

The only functional change is _PIN_SLO[] = true at the top of placement_rows (golden.jl:113). I verified it's inert: _PIN_SLO defaults to Ref(true) (render.jl:106), golden.jl:113 is its only writer (and writes true), and it's read only at render.jl:581 — so there's no ordering hazard and the digest is unchanged. The golden data files (atlas-placement.rows.txt/.sha256) are untouched in this diff, so the 475dfc52… digest already verified in round 1 still holds by construction. The remaining changes are comment/docstring/testset-name wording, now correctly stating "robust to sub-pixel projection drift, not Makie-independent."

Resolved since last round

  • _PIN_SLO[] dependency — now pinned explicitly in placement_rows; confirmed inert (sole writer, default already true). Fixed.
  • "machine-stable" overclaim — corrected throughout golden.jl header/docstrings and test_golden.jl to "robust to sub-pixel projection drift," with the Makie-routing caveat made explicit. Fixed.
  • README solve_cluster drift — lines 28/55/68 updated to warm_solve; archival prep/spike_timing.jl left as-is per the note. Fixed.

@jowch

jowch commented Jun 18, 2026

Copy link
Copy Markdown
Owner Author

Converged at round 2 — zero blocking throughout; the three round-1 nits (the _PIN_SLO hardening, the "robust to sub-pixel projection drift" wording correction, and the README solve_cluster→warm_solve drift) are fixed and re-approved. Migration is verified placement-preserving (byte-identical hero, unchanged geometric golden); the new placement golden holds at 475dfc52…; suite 295/295. No further changes intended.

@jowch
jowch merged commit 2316e74 into main Jun 18, 2026
11 checks passed
@jowch
jowch deleted the atlas-warm-solve-migration branch June 18, 2026 22:00
jowch added a commit that referenced this pull request Jun 18, 2026
The greenfield gallery has landed (#30/#32/#33/#34/#35) and the engine is stable, so the
v0.1.0 deferral is lifted.

- CHANGELOG: promote [Unreleased] → [0.1.0] (2026-06-18), drop the stale "deferred until
  #30" note, fix the Atlas line to public `warm_solve` (post-#35), fix link refs.
- Add TagBot workflow (canonical JuliaRegistries example, v1.25.8 pinned by SHA; no explicit
  permissions block per current TagBot guidance) so the v0.1.0 git tag + GitHub release are
  cut automatically once the General-registry version merges.

Not adding CompatHelper: it's in maintenance mode and .github/dependabot.yml already covers
the Julia ecosystem (root + /test) plus github-actions.

Registration readiness (verified): [compat] is complete (Unicode + the three weakdeps +
julia, all upper-bounded), top-level MIT LICENSE, src/TextMeasure.jl, green test suite, and
the name "TextMeasure" is free in General. The registration trigger itself is a manual step.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant