Incremental UMAP for growing datasets: --transform-from, growing --align, --register-to - #148
Conversation
…ign, --register-to (#142) Three features so a daily-growing dataset can keep a visually stable published map: 1. ls-umap <ds> <emb> --transform-from <umap-id> — loads the source umap's saved (--save) reducer pickle (following the reducer_id chain when the source was itself a transform output), projects only the rows appended since the source, and maps them into the source's [-1,1] frame via its stored min/max (or its registration transform if the source was registered). Old rows are copied verbatim from the source parquet so published positions never move; new points may land slightly outside [-1,1] and are left there (count printed). Meta records transformed_from + reducer_id; pickles are never copied. 2. --align now supports growing (append-only) windows: identity relations are replaced by shared-prefix relations {j: j for j in range(min(len_i, len_i+1))}, identical to the old behavior for equal-length embeddings. 3. --register-to <umap-id> — after a plain or aligned fit, registers the new layout(s) onto an existing umap with a least-squares 2D similarity transform (Umeyama; rotation + uniform scale + translation, reflection allowed) fit on the shared row prefix. The result is already in the target's [-1,1] frame, so min/max renormalization is skipped and the target's frame plus the registration transform are stored in meta (registered_to, registration) — a later --transform-from reuses that transform. The pure numpy pieces (umeyama_2d, register_layout, prefix_relations, apply_normalization) live in latentscope/scripts/registration.py and are unit-tested without running any real UMAP fits; the transform flow is tested through a fake pickled reducer. Both new flags are forwarded by the server job route, and docs/umap.md documents the daily/periodic recipe plus the seed/single-thread and pickle-size caveats. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4ea806be31
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ```bash | ||
| # AlignedUMAP across the old (shorter) and current (longer) embedding windows, | ||
| # anchored to the published umap so the layout doesn't rotate/flip/drift: | ||
| ls-umap mydataset embedding-002 25 0.1 --align embedding-001 --register-to umap-001 --save |
There was a problem hiding this comment.
Persist a reducer for the aligned refit
In the periodic refit workflow this command accepts --save, but the align branch in latentscope/scripts/umapper.py returns before the only if save: pickle.dump(...) path, so it writes the registered umap-NNN.json/parquet without a matching .pkl or reducer_id. If the user then follows the documented daily cadence and runs --transform-from on that refit, _resolve_reducer_id finds no saved reducer and the incremental workflow fails; either persist a usable reducer/reducer reference for aligned outputs or avoid documenting --save here.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 6e2ec89 — --align --save now pickles a reducer per aligned output. Two subtleties the fix handles: (1) AlignedUMAP's mappers_[i].embedding_ lives in the mapper's own frame, not the aligned frame (empirically unrelated — mean deviation ~78% of layout span), so the slice's aligned coordinates are swapped into embedding_ before pickling; UMAP.transform embeds new points relative to embedding_, which was verified to re-place training rows within ~9% of span of their aligned positions (normal transform stochasticity). (2) The meta's min/max or registration transform then maps transformed points into the published frame, exactly as --transform-from expects. Test added: align+save writes a loadable pkl per slice whose embedding_ is the aligned frame, and --transform-from on an aligned output resolves its reducer.
Review fix: the align branch returned before the --save pickle path, so the documented periodic-refit workflow produced registered umaps with no saved reducer — the next daily --transform-from failed at reducer resolution. Each aligned output now pickles its slice's fitted mapper when --save is set. Crucially, the mapper's internal embedding_ lives in the mapper's own frame, NOT the aligned frame (verified empirically: the two are unrelated — mean deviation ~78% of the layout span), so the slice's aligned coordinates are swapped into embedding_ before pickling; UMAP.transform embeds new points relative to embedding_, which puts them in the slice's raw aligned frame (verified: training rows re-transform to median ~9% of span from their aligned positions, normal transform stochasticity). The meta's min/max or registration transform then carries new points into the published frame, exactly as the daily --transform-from path expects. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-umap # Conflicts: # AGENTS.md
Closes #142.
A daily-growing dataset can now keep a visually stable published map:
ls-umap <ds> <emb> --transform-from <umap-id>— loads the source umap's saved (--save) reducer pickle (following thereducer_idchain when the source was itself a transform output), projects only the rows appended since the source, and maps them into the source's[-1,1]frame via its stored min/max — or via its registration transform if the source was registered, so the daily cadence keeps working after a registered refit. Old rows are copied verbatim (byte-identical) so published positions never move; out-of-frame new points are left in place and counted. Pickles are never copied.--align— identity relations replaced by shared-prefix relations{j: j for j in range(min(len_i, len_i+1))}; identical to old behavior for equal lengths, supports append-only windows.--register-to <umap-id>— registers a plain or aligned fit onto an existing umap with a least-squares 2D similarity transform (Umeyama; rotation + uniform scale + translation, reflections allowed) fit on the shared row prefix. Output is already in the target's frame (no renormalization); meta recordsregistered_to+ the transform.docs/umap.md— the daily/periodic recipe, plus the--seed→single-threaded-fit caveat and reducer-pickle size/pruning notes from the issue.Both new flags are forwarded by the server job route. Pure numpy pieces live in
latentscope/scripts/registration.py.Recipe:
Test plan
uv run pytest tests/ -q— 233 passed (14 new: umeyama recovery incl. reflection/degenerate cases, prefix relations for unequal windows, normalization, and the full transform flow via a fake pickled reducer — success, chain resolution, registered source, missing pkl, mismatched embedding, no-new-rows)🤖 Generated with Claude Code