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
Branch Survey
| Repository |
Current Branch |
Dirty? |
| ./PyAutoFit |
main (f466dce) |
clean |
Suggested branch: feature/joint-clipper-compose-with-bijector
Implementation Steps
-
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.
-
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.
-
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.py — ClipperPriorBoxJoint.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.
Overview
ClipperPriorBoxJointis refused outright whenever ascalerorbijectoris set — at construction inmulti_start_gradient/search.py:365-384and again at projection time inclipper.py:522-535. The refusal is stronger than the geometry requires: a coordinate whose bijector kind isidentitywith linear scalesmaps a disk of radiusRto a disk of radiusR / 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/logitreparameterisation anywhere in the model must drop the joint clipper entirely, so itsell_compslanes settle at the box corner|e| = 1.414— outside the disk, where the axis-ratio conversion saturates and the gradient is flat. Phase 8B'slog_regarms do exactly this.Plan
identity-kind.sis projected onto radiusR / s; anything else raises, now naming the offending index pair and its kinds.MultiStartGradientand 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.logitepsilon clamps would break bit-identity on unrelated coordinates, saturate gradients, and add traced ops.__identifier_fields__and every stored result key are untouched.Detailed implementation plan
Affected Repositories
Branch Survey
Suggested branch:
feature/joint-clipper-compose-with-bijectorImplementation Steps
autofit/non_linear/bijector.py— addAbstractBijector.identity_scalesbesidekinds(~:236-240): a list whose entryiisfloat(self._scale[i])whenself._kind_code[i] == _IDENTITYandNoneotherwise, guarded by_check_resolved(). ~12 lines.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 scales(read frombijector.identity_scales, or from the rawscalearray on the scaler path), and raising the existingValueError— now naming the index pair and its kinds — otherwise. Add a "Composition with a scaler/bijector" docstring section carrying theR -> R/sargument.autofit/non_linear/search/mle/multi_start_gradient/search.py— delete the construction-time refusal (:365-384); call the clipper helper on:884-885) and beforemodel.ball_constraint_index_pairs()inside_fit, immediately afterself.bijector.from_model(model=model)(_vmappedis built/compiled. Update theclipper/bijectordocstring paragraphs (~:148-201).bfgs/search.pyis 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— rewritetest__a_scaler_or_bijector_is_refused_rather_than_applied(:700) into the composition cases::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 leavesBijectorNonebit-identical to no bijector; a per-pathlogon a non-ell_compspath still projects the corner onto0.999with the angle preserved;BijectorLogiton the ball model still raises and names the pair;scale=[2,2,1]projects onto0.999/2;scale=[2,3,1]raises;BijectorDiagonal(ScalerPriorWidth())with equal widths is accepted. Restate the search-wiring tests (Identifier(...)unchanged.test_autofit/non_linear/test_bijector.py— F5 pintest__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; alogit-on-ell_compscombination 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.py—ClipperPriorBoxJoint.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
logitarm must be restated as aBijectorPerPathwithlogiton every path exceptell_comps.*. That is a config change inautolens_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.