Skip to content

Incremental UMAP for growing datasets: --transform-from, growing --align, --register-to - #148

Merged
enjalot merged 3 commits into
mainfrom
issue-142-incremental-umap
Jul 11, 2026
Merged

Incremental UMAP for growing datasets: --transform-from, growing --align, --register-to#148
enjalot merged 3 commits into
mainfrom
issue-142-incremental-umap

Conversation

@enjalot

@enjalot enjalot commented Jul 10, 2026

Copy link
Copy Markdown
Owner

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 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 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.
  • Growing-window --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 records registered_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:

ls-umap mydata embedding-001 25 0.1 --save                       # once: fit + save reducer
ls-umap mydata embedding-001 --transform-from umap-001           # daily: project new rows
ls-umap mydata embedding-002 25 0.1 --align embedding-001 --register-to umap-001 --save  # periodic refit

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)
  • Verified end-to-end with real UMAP/AlignedUMAP fits on a 120-row dataset: daily transform kept 100 old rows byte-stable and appended 20; growing align (100→120) + register wrote both slices anchored to the published frame
  • Behavior with none of the new flags is unchanged

🤖 Generated with Claude Code

…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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment thread docs/umap.md
```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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

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.

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.

enjalot and others added 2 commits July 10, 2026 02:13
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>
@enjalot
enjalot merged commit 4e9246d into main Jul 11, 2026
4 checks passed
@enjalot
enjalot deleted the issue-142-incremental-umap branch July 11, 2026 14:30
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.

Incremental map updates: transform-from-saved-reducer, growing-window align, layout registration

1 participant