Skip to content

fix: compose ClipperPriorBoxJoint with a bijector/scaler instead of refusing it #1539

Description

@Jammy2211

Overview

ClipperPriorBoxJoint is refused outright whenever a scaler or bijector is set — at construction in multi_start_gradient/search.py:365-384 and again at projection time in clipper.py:522-535. The refusal is stronger than the geometry requires: a coordinate whose bijector kind is identity with linear scale s maps a disk of radius R to a disk of radius R / s, which is still a disk and still exactly projectable. Only a genuinely non-linear pair (log, logit, or two identity coordinates with different scales, which gives an ellipse) has no closed form.

The cost is concrete: any gradient arm that wants a log/logit reparameterisation anywhere in the model must drop the joint clipper entirely, so its ell_comps lanes settle at the box corner |e| = 1.414 — outside the disk, where the axis-ratio conversion saturates and the gradient is flat. Phase 8B's log_reg arms do exactly this.

Plan

  • Teach the bijector to report, per coordinate, the linear scale it applies when (and only when) that coordinate is identity-kind.
  • Replace the clipper's blanket refusal with a per-pair resolution: a ball pair whose two members are both identity-kind under a common linear scale s is projected onto radius R / s; anything else raises, now naming the offending index pair and its kinds.
  • Delete the construction-time refusal in MultiStartGradient and move the check to model-resolution time inside _fit, so a genuinely bad combination still dies before any likelihood evaluation but a good one is simply allowed.
  • Do not round-trip through the bijector inside the clipper — the logit epsilon clamps would break bit-identity on unrelated coordinates, saturate gradients, and add traced ops.
  • No new constructor arguments, so __identifier_fields__ and every stored result key are untouched.
Detailed implementation plan

Affected Repositories

  • PyAutoFit (primary)

Branch Survey

Repository Current Branch Dirty?
./PyAutoFit main (f466dce) clean

Suggested branch: feature/joint-clipper-compose-with-bijector

Implementation Steps

  1. autofit/non_linear/bijector.py — add AbstractBijector.identity_scales beside kinds (~:236-240): a list whose entry i is float(self._scale[i]) when self._kind_code[i] == _IDENTITY and None otherwise, guarded by _check_resolved(). ~12 lines.

  2. autofit/non_linear/clipper.py — replace the blanket raise at ~:522-535 with a _pairs_in_stepped_coordinates(pairs, scale, bijector) helper returning (i, j, radius / s) when both members of a pair share a common linear scale s (read from bijector.identity_scales, or from the raw scale array on the scaler path), and raising the existing ValueError — now naming the index pair and its kinds — otherwise. Add a "Composition with a scaler/bijector" docstring section carrying the R -> R/s argument.

  3. autofit/non_linear/search/mle/multi_start_gradient/search.py — delete the construction-time refusal (:365-384); call the clipper helper on model.ball_constraint_index_pairs() inside _fit, immediately after self.bijector.from_model(model=model) (:884-885) and before _vmapped is built/compiled. Update the clipper / bijector docstring paragraphs (~:148-201). bfgs/search.py is untouched — LBFGS still refuses the joint clipper wholesale, since scipy has no ball.

Tests (NumPy only — JAX stays out of the unit suite)

  • test_autofit/non_linear/test_clipper.py — rewrite test__a_scaler_or_bijector_is_refused_rather_than_applied (:700) into the composition cases: BijectorNone bit-identical to no bijector; a per-path log on a non-ell_comps path still projects the corner onto 0.999 with the angle preserved; BijectorLogit on the ball model still raises and names the pair; scale=[2,2,1] projects onto 0.999/2; scale=[2,3,1] raises; BijectorDiagonal(ScalerPriorWidth()) with equal widths is accepted. Restate the search-wiring tests (:774-784) from "refused at construction" to "refused at model resolution, before the first step", keeping the identifier assertions verbatim and adding one that a bijector leaves Identifier(...) unchanged.
  • test_autofit/non_linear/test_bijector.py — F5 pin test__round_tripping_a_per_path_map_is_bit_exact_where_it_is_identity, right after the objective-composition test (~:388-409).
  • test_autofit/non_linear/search/mle/test_multi_start_gradient.py — the joint clipper plus a per-path bijector constructs and is accepted at model resolution; a logit-on-ell_comps combination raises before the first step.

Key Files

  • autofit/non_linear/bijector.py — the per-coordinate kind/scale cache and its accessors.
  • autofit/non_linear/clipper.pyClipperPriorBoxJoint.project, the ball projection.
  • autofit/non_linear/search/mle/multi_start_gradient/search.py — where the two strategies meet.

Campaign implication (not done here)

To actually use the disk, the Phase 8B logit arm must be restated as a BijectorPerPath with logit on every path except ell_comps.*. That is a config change in autolens_profiling, not a library change, and is out of scope for this issue.

Original Prompt

Click to expand starting prompt

ClipperPriorBoxJoint is refused whenever a bijector/scaler is set (multi_start_gradient/search.py:365-384, clipper.py:522-535); Phase 8B log_reg arms therefore settle at the ell_comps box corner |e|=1.414. Compose them: resolve each ball pair against the map and refuse only genuinely non-linear pairs. Also add the in-process F5 pin test.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions