Skip to content

feat: report prior-support clipping in search.summary - #1478

Merged
Jammy2211 merged 1 commit into
mainfrom
claude/clipper-validation-handover-ltin4j
Aug 16, 2026
Merged

feat: report prior-support clipping in search.summary#1478
Jammy2211 merged 1 commit into
mainfrom
claude/clipper-validation-handover-ltin4j

Conversation

@Jammy2211

Copy link
Copy Markdown
Collaborator

The Clipper (#1477) counts how often it fires — n_clipped_lane_steps is accumulated per-lane in AbstractMultiStartGradient and written to search_internal — but the count never reached the artefact a user actually reads to find out what a search did. search.summary said nothing about clipping at all.

The channel already existed

Worth stating, because it makes this change much smaller than it first looks. search_summary_from_samples already reads samples.samples_info and already emits Resurrections, Value-NaN Lane-Steps, Gradient-NaN Lane-Steps and both rates, guarded on the key so searches without them are unaffected. Two things were missing from it, rather than a mechanism being absent:

Three cases, and the last two are the point

case reports
No clipper (ClipperNone, or a search predating the Clipper) nothing at all
Clipped and counted (MultiStartGradient) Clipper, Clipped Lane-Steps, Clipped Lane-Step Rate
Clipped but not observable (LBFGS, other bound-supporting scipy methods) Clipper, Clipped Lane-Steps = not measured (bounds enforced by scipy)

MultiStartGradient enforces the constraint itself every step via Clipper.project, so it knows exactly how often it fired.

LBFGS is declarative — it hands optimize.Bounds to scipy and lets scipy enforce, so project is never called and no mask exists. Reporting 0 there would read as "the clipper never fired" when it means "this search cannot know". It says so instead.

The default path emits nothing, which matters because this file is read by tooling and sits in every archived run's output.

Design notes

  • The clipper is published as its class name, not a bool, so the summary can say which strategy ran and a later strategy needs no schema change here.
  • The count is per-lane, not per-coordinate — a lane clipped in three parameters on one step is one clipped lane-step. That matches how the counters beside it read, so all four stay directly comparable.
  • It is restored from search_internal as a lifetime total, so a resumed run reports the whole run's clipping rather than the current process's share.
  • The rate uses the same n_starts * total_steps denominator as the NaN rates, omitted when that is zero.

Verification

test_autofit/non_linear + test_autofit/text: 501 passed, 3 skipped, 1 failed. The failure (test_nautilus.py::test__single_core_builds_no_pool) is pre-existing — reproduced identically on a stashed clean tree — as is the astropy collection error in paths/test_save_and_load.py.

6 new tests cover: ClipperNone emits nothing; counted reports count and rate; the no-count case says not-measured and never a bare 0; an absent key leaves other searches unaffected; constrained emitted; constrained absent when never written.

Also verified end-to-end against the search.summary files four real searches wrote, not just the formatting helper in isolation:

LBFGS default        -> no clipping lines
LBFGS clipped        -> Clipped Lane-Steps = not measured (bounds enforced by scipy)
MultiStart default   -> no clipping lines
MultiStart clipped   -> Clipped Lane-Steps = 414
                        Clipped Lane-Step Rate = 0.9583333333333334

Incidental, and worth someone's attention: on that toy 3-parameter Gaussian the unclipped arm showed Value-NaN Lane-Steps = 378 (94.5%) and the clipped arm 0. The autolens_profiling#128 mechanism reproduces on a model with nothing astrophysical in it, which would make a far cheaper regression fixture than the imaging/mge cell.

One behaviour change to note in review

Multi-start summaries gain a Constrained Lane-Steps line they did not have before. Everything else is strictly additive and gated, so the ClipperNone path is otherwise unchanged. If byte-identity for existing multi-start runs is wanted too, that one line can be dropped — it is independent of the clip count.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FzF2XmKQaqZRWZfZMxvTNR


Generated by Claude Code

The Clipper (#1477) counts how often it fires -- n_clipped_lane_steps is
accumulated per-lane in AbstractMultiStartGradient and written to
search_internal -- but the count never reached the artefact a user actually
reads to find out what a search did. search.summary said nothing about
clipping at all.

The channel already existed: search_summary_from_samples reads
samples.samples_info and emits the NaN counters and their rates, guarded on
the key so searches without them are unaffected. Two things were missing from
it rather than a mechanism:

- n_clipped_lane_steps never reached samples_info. It stopped at
  search_internal, so nothing downstream could see it.
- n_constrained_lane_steps did reach samples_info but was never emitted, so
  the trapped-lane counter from #1475 has been invisible in search.summary
  since it shipped. Now reported.

Clipping is reported in three cases, and the distinction between the last two
is the point:

- No clipper (ClipperNone, or a search predating the Clipper): emits nothing.
  The default path's summary is unchanged, which matters because this file is
  read by tooling and sits in every archived run's output.
- Clipped and counted (MultiStartGradient): it enforces the constraint itself
  every step via Clipper.project, so it knows how often it fired and reports
  the count and the rate, denominated by n_starts * total_steps like the NaN
  rates beside it.
- Clipped but not observable (LBFGS and the bound-supporting scipy methods):
  declarative, handing optimize.Bounds to scipy and letting scipy enforce, so
  project is never called and no mask exists. Reporting 0 there would read as
  "the clipper never fired" when it means "this search cannot know", so it
  says "not measured (bounds enforced by scipy)" instead.

The clipper is published as its class NAME rather than a bool, so the summary
can say which strategy ran and a later strategy needs no schema change. The
count is per-LANE, not per-coordinate -- a lane clipped in three parameters on
one step is one clipped lane-step -- matching how the counters beside it read,
so all four stay directly comparable. It is restored from search_internal as a
lifetime total, so a resumed run reports the whole run's clipping rather than
the current process's share.

Verified end-to-end against the search.summary files four real searches wrote,
not just the formatting helper in isolation: LBFGS default (no clipping lines),
LBFGS clipped ("not measured"), MultiStart default (no clipping lines),
MultiStart clipped (Clipped Lane-Steps = 414, rate 0.958).

One behaviour change to note: multi-start summaries gain a Constrained
Lane-Steps line they did not have before. Everything else is additive and
gated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FzF2XmKQaqZRWZfZMxvTNR
@Jammy2211
Jammy2211 merged commit bbceff6 into main Aug 16, 2026
3 checks passed
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.

2 participants