Skip to content

Per-parameter bijector for gradient searches (identity / log / logit) — Phase 8B library half (autolens_profiling#162) - #1525

Merged
Jammy2211 merged 4 commits into
mainfrom
feature/bijector-log-coordinate-stepping
Aug 24, 2026
Merged

Per-parameter bijector for gradient searches (identity / log / logit) — Phase 8B library half (autolens_profiling#162)#1525
Jammy2211 merged 4 commits into
mainfrom
feature/bijector-log-coordinate-stepping

Conversation

@Jammy2211

Copy link
Copy Markdown
Collaborator

Summary

Library half of autolens_profiling #162 (W5 · Phase 8B: log-coordinate stepping). The profiling A/B driver follows once this merges (library-first gate).

  • autofit/non_linear/bijector.pyAbstractBijector (forward / inverse / log_det_jacobian / bounds_forward / kinds), per-coordinate kinds baked once in from_model so JAX traces one where chain (double-where safe surrogate, no NaN grads from the unused branch). BijectorNone (default, byte-identical step), BijectorAuto (log for LogUniform/LogGaussian, identity elsewhere), BijectorPerPath, BijectorLogit (secondary arm; the scaler-era unit-cube objections stand and are documented), BijectorDiagonal (scaler-equivalent).
  • Equivalence argument is in the docstring: per-coordinate monotone bijection relabels points without changing the objective's value set, so the MAP maps exactly and adding a Jacobian would move it; log_det_jacobian exists for φ-space samplers (multi-chain NUTS, Multi-chain, warm-started, metric-seeded BlackJAXNUTS (#1521) #1522) and is never called by MultiStart.
  • Clipper composes in transformed space (project(..., bijector=)), valid because every kind is monotone. Bug fix: ClipperPriorBox's relative inset margin·(hi−lo) ≈ 1.0 physical for LogUniform(1e-6, 1e6) — under a log coordinate that would fence off all λ < 1. Log-kind coordinates now inset in log space (margin·log(hi/lo)), pinned by a test.
  • MultiStartGradient gains bijector= beside scaler= (both non-default → ValueError); search_internal["params"] stays physical so resume across bijector on/off is safe; samples_info records class + kinds. Two flag-gated, off-by-default per-step diagnostics (record_lane_nan_history, trace_param_indices) — no-ops on the compiled step when unused.

Tests

Full suite: 2167 passed, 3 skipped. New test_bijector.py (27), test_clipper.py +6, test_multi_start_gradient.py +6. Pre-merge JAX smoke: MultiStartProdigy on a 3-param model with a LogUniform amplitude — BijectorNone and BijectorAuto reach the same MAP (centre 1.000, σ 0.800, amp 300.0). ruff clean on touched files (the 12 new __init__ re-export lines match that file's existing F401/E402 baseline).

Notes

Prodigy's global d is unchanged (one scalar over mixed units, exactly as under a scaler) — documented, to be measured by the profiling A/B's MGE byte-identity control and F4.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EDABYoH6giHXhFJUks8yd6

Jammy2211 and others added 4 commits August 24, 2026 17:57
Extends the linear diagonal AbstractScaler to a per-coordinate bijection
(identity / log / logit), vectorised over (n_starts, n_params) and built
once per model via from_model() so jax.jit traces a single jnp.where
selection tree. BijectorNone is the no-op default (byte-identical: x/1.0
== x exactly); BijectorAuto picks log for LogUniform/LogGaussian priors;
BijectorLogit is an explicitly secondary arm (the scaler's unit-cube
objections about boundary optima still apply); BijectorPerPath lets a
caller declare kinds by model path; BijectorDiagonal adapts an existing
AbstractScaler into this framework without changing scaler= behaviour.

log_det_jacobian is exposed for a future phi-space sampler (PyAutoFit
#1521/#1522) and is never needed for MAP: composing the objective through
a bijection relabels points without changing its value set, so no
Jacobian may be added without moving the MAP.

Compose AbstractClipper.project with a bijector, clipping in transformed
space against bijector.bounds_forward(lower_inset, upper_inset) -- valid
because every kind is monotone increasing. Fixes a real bug found while
wiring this up: the existing physical-relative margin
(margin * (upper - lower)) is wrong for a log-kind coordinate by orders
of magnitude -- for LogUniform(1e-6, 1e6) the default margin produces a
~1.0 PHYSICAL inset, fencing off virtually the entire support. Log-kind
coordinates now get a log-space margin (margin * log(upper/lower)) via
ClipperPriorBox._inset_from_model(kinds=...); the no-bijector
bounds_from_model path is unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EDABYoH6giHXhFJUks8yd6
Adds bijector: Optional[AbstractBijector] = None alongside scaler,
mutually exclusive (raises ValueError at construction if both are
non-default). Wherever the step loop maps between physical and stepped
coordinates for the scaler (resume load, fresh-start seeding, best/
lane-best capture, dead-lane redraw, the write-back), a parallel
bijector.forward/inverse branch is added; search_internal["params"]
stays PHYSICAL either way so resume is safe across a scaler/bijector
change. search_internal also gains "bijector": bijector.kinds, and
samples_info records the bijector's class name and (from
search_internal, not live state) its resolved kinds -- mirroring how
"scaler"/"clipper" are already reported.

AbstractClipper.project's new `bijector=` param is threaded through the
clip call and the dead-lane redraw. Everything is gated behind the same
has_scaler/has_bijector-style short-circuit already used for the
scaler/clipper/constraint checks, so the default (BijectorNone) step
loop and its jax.jit trace are unchanged.

Also adds two independently flag-gated, off-by-default diagnostics
(neither touches the compiled step when unused): record_lane_nan_history
(per-step per-lane value/grad-NaN bits, numpy.packbits'd to
(n_steps, ceil(n_starts/8))) and trace_param_indices (a physical
per-step trace for a chosen subset of parameters, (n_steps, n_starts,
k)). Both resume via the same `.get`-default discipline as the existing
lifetime counters.

Exports the Bijector* classes from autofit/__init__.py beside the
Scaler* ones.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EDABYoH6giHXhFJUks8yd6
test_bijector.py (numpy-only, mirroring test_scaler.py's discipline):
round trip and monotonicity per kind, the log-kind closed-form
log-det-Jacobian checked against a finite difference of inverse()
directly, kind-selection/fallback coverage for BijectorAuto/
BijectorLogit/BijectorPerPath (including a malformed LogUniformPrior),
BijectorDiagonal reducing exactly to ScalerPriorWidth's map,
BijectorNone's byte-identical no-op, the objective-composed-through-the-
bijector equivalence pin and its end-to-end "cannot move the MAP"
companion (L-BFGS-B in raw theta space vs. unconstrained BFGS in phi
space -- the raw run needs a box because (log(theta)-2)^2 is undefined
for theta <= 0 and unconstrained steps stray there, which is itself the
motivating case for the log bijector), and clip-commutes-with-forward.

test_clipper.py: scale+bijector together raises, ClipperNone ignores a
bijector, the LogUniform log-space inset fix pinned directly against the
bug (a LogUniform(1e-6, 1e6) physical-relative inset lands near 1.0,
fencing off virtually the whole support; the log-space inset stays
within 0.1% of the true bound), project(..., bijector=...) matching a
direct physical clip mapped through, and the no-bijector
bounds_from_model path proven byte-unchanged.

test_multi_start_gradient.py: bijector dict round-trip, the
scaler+bijector construction-time raise, bijector not offered by LBFGS,
and samples_info recording the bijector's class name and (read from
search_internal, not live resolved state) its kinds.

Full suite green: test_autofit/non_linear (673 passed, 2 skipped) and
the whole test_autofit suite (2167 passed, 3 skipped). ruff clean on
every touched file except autofit/__init__.py, where the new Bijector*
re-export lines repeat that file's pre-existing F401/E402 baseline
(258 -> 270 errors, +12 for the 12 new import lines) rather than adding
a new category of lint issue.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EDABYoH6giHXhFJUks8yd6
Extends the existing estim_lr/d progress-line comment (already noting d
is in SCALED units under a scaler, not physical ones) to cover the
bijector case explicitly: under a mixed identity/log bijector, d is a
single global scalar spanning coordinates that are not even the same
KIND of unit across parameters. Prodigy's own estimation rule (one
global d from whole-tree norms) is unmodified by either knob -- this is
existing, documented Prodigy behaviour, not something this change
touches or attempts to fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EDABYoH6giHXhFJUks8yd6
@Jammy2211
Jammy2211 merged commit 9ecf930 into main Aug 24, 2026
4 checks passed
@Jammy2211
Jammy2211 deleted the feature/bijector-log-coordinate-stepping branch August 24, 2026 22:17
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