Skip to content

O4d: prepare 0.0.18.0rc4 release - #191

Merged
oshaughn merged 364 commits into
oshaughn:rift_O4dfrom
oshaughnessy-junior:rift_O4d
Sep 11, 2026
Merged

oshaughn merged 364 commits into
oshaughn:rift_O4dfrom
oshaughnessy-junior:rift_O4d

Conversation

@oshaughnessy-junior

@oshaughnessy-junior oshaughnessy-junior commented Sep 11, 2026

Copy link
Copy Markdown

Summary

Bring oshaughn/rift_O4d at the 0.0.18.0rc3 release mark up to the accumulated work on oshaughnessy-junior/rift_O4d. The rc4 version bump is deliberately deferred until this upstream PR has merged.

  • Base: oshaughn/rift_O4d @ a93e41f7
  • Fork head before the required conflict-resolution merge: oshaughnessy-junior/rift_O4d @ 5e55d030
  • Comparison: 362 fork commits across 133 files (+37,905 / -1,157); the fork is one release-note/version commit behind the base.

Delivery history

This is the next catch-up after #170 (the rc2 integrator/calmarg/convergence payload), #173 (O4c/master consolidation), #178 (the post-consolidation fork sync), and #188 (the rc3 sync). The fork merge log shows this payload begins with the post-#188 JAX/CI work at #245--#248 and continues through #319; those earlier deliveries are already represented by the rc0--rc3 entries below and are not re-released here.

Payload by subsystem

Subsystem Fork PRs What lands
JAX direct marginalization #245, #247, #250, #268, #270, #274--#285, #294, #301--#305, #312--#315 Opt-in, fail-closed four-axis peak-local time/distance/angle controller; exact and band-limited reserve rules; device-aware Q/time pregridding and batching; refusal rather than publication when a requested configuration or evidence cannot be warranted.
JAX sampling and evidence #214, #227, #303, #319 Persistent JAX compilation cache; proposal scoring under the covariance actually sampled; non-finite/implausible-evidence refusal before artifacts are written; value-only adaptive-volume and portfolio sampling, including phase-rotated coordinates.
Detector response and likelihood kernels #255, #307, #309--#311 Combined slow-rotation and finite-size frequency response in ILE/JAX, per-detector NoLoop accumulation, and batched mode cross terms.
Workflow integration #281, #282, #284 Asimov/JAX driver selection and configuration forwarding, with public options wired through the pipeline.
Validation and CI #248, #251, #258, #259, #261, #269, #286--#299 Expanded CPU/JAX regression coverage, CI roster enforcement, JAX sharding, and validation fixtures for peak-local, response, and evidence paths.

New JAX sampling and direct-marginalization capabilities are opt-in. The payload does change failure behavior intentionally: an integration whose evidence is non-finite or fails the guard is not written as a normal successful result.

CHANGES.rst conflict

The only merge conflict is CHANGES.rst: the base supplies the concise rc0--rc3 release history while the fork retains earlier detailed development notes. The resolution keeps the base history, adds a compact rc4 pending summary for this payload, and keeps setup.py at 0.0.18.0rc3.

oshaughnessy-junior/rift_O4d is protected, so the resolution is supplied as fork PR #320. Merge #320 first; it updates the head branch and clears this PR conflict without bypassing branch protection.

Release follow-up

After this PR merges, update CHANGES.rst and setup.py together from rc3 to 0.0.18.0rc4, then run the selected release validation.

Verification

  • Merge resolution checked with git diff --check.
  • No source code was changed by the resolution; functional validation remains the per-feature coverage and CI carried by the fork PRs.

oshaughnessy-junior and others added 30 commits September 5, 2026 15:06
The gate owns the filename pattern test/test_noloop_*.py, so the new file
matched its manifest scope by NAME and went red as "neither registered nor
explicitly excluded" -- which is the gate working: a new file in that namespace
is meant to force a decision rather than be silently unrun.

The decision is exclusion. It matches by name but not by subject: it pins
NoLoop's rho_sq and kappa_sq ACCUMULATOR shapes against a reference, and its
time-integral test is about the quadrature RULE, not about sub-sample
interpolation of Q_lm. It is already registered with core-unit-check, whose
FILES manifest carries it and whose floors count it, and there is no
dual-registration precedent -- every marker-carrying file belongs to this gate
alone and no core-unit file carries the marker. The two gates partition.

Listed in EXCLUDED with a stated reason rather than renamed out of scope. The
gate's own comment notes that a file under another prefix escapes the manifest
entirely; renaming to dodge it is how these gates quietly stop covering things,
and the reason belongs where the next such file will hit it.

No floor changes here: exclusion adds no tests to this job.

Verified locally through the gate's scope-pattern, exclusion and manifest
phases, which are the three my change affects; the run then stops on an
unrelated ImportError (no module named glue) in test_batchmode_stencil_default,
an artifact of this environment rather than of the change. CI has the full
install.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…verify

# Conflicts:
#	.travis/test-core-units.sh
Conflict was in .travis/test-core-units.sh: both sides moved the same floors.  rift_O4d added
test_vectorized_lal_tools_split.py (296/284 -> 299/287); this branch added
test_teobresums_compat.py and test_integrator_studies.py (-> 316/304).

Resolved by keeping BOTH manifest entries and RE-MEASURING rather than adding the deltas: the
floors are now 319/307 from a full run, and the merged comment records both histories plus the
reason the arithmetic is not the method.  (It happens to equal 316+3, which is exactly the kind
of agreement that would have made a computed number look safe.)

Two things the resolution itself broke or exposed, both caught before pushing:

  * my conflict edit DELETED the EXPECTED_PASSED assignment along with the two conflicting
    versions of it.  `set -u` turned that into "EXPECTED_PASSED: unbound variable" and the gate
    exited 1 rather than silently skipping the outcome check -- the failure direction that
    matters.  Restored with its comment.
  * ci.yml claimed timeout-minutes was "an order of magnitude above the 49 s measured on CIT".
    Measured now: 350 s, of which only ~100 s is the pytest run; the rest is the per-file
    collection loop, one RIFT-importing interpreter per manifest entry, which grows with the
    manifest.  20 minutes is ~3.4x, not 20x.  Comment corrected in both files, because a margin
    claim that has quietly become false is how a gate ends up timing out.

Reassessed on the merged tree: ci-roster-check PASS (208 files, 157 reachable, 51 rostered --
the five test files rift_O4d brought are all already registered by its own authors),
roster-verify-check PASS (50 predicates), core-unit-check PASS (319/307/12).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ocation was sized wrong

An independent review ran a 37-configuration differential matrix over both PRs
(phase and distance marginalization, n_cal>1 in loop and fused, the cal self-term
fix, return_lnLt / return_cal_components / return_time_draw, explicit_time_values,
all three stencils, all three quadratures, 1-3 detectors, heterogeneous mode lists,
CPU and GPU). It found the CODE sound -- #255 bitwise across all 37, the whole of
#256 within 1.8e-15 nats -- and the EVIDENCE unsound. This fixes the evidence and
one real defect it surfaced.

1. THE ACCUMULATOR TEST NEVER EXERCISED kappa_sq. epoch was a whole second before
   tref, putting ifirst at ~3979-4152 against an n_time of 512, so every Q window
   was zero-extended and the data term was identically zero in all six parametrized
   cases. The file was named for the kappa_sq change and validated only rho_sq;
   the reviewer demonstrated it passing with the CPU Q producer aliased across
   detectors, which is precisely the hazard kappa_sq = Q_prod_result introduces.
   Fixed by placing the window inside the buffer, and pinned by a new test that
   asserts lnL(t) actually varies in time. Verified by sabotage: dropping either
   accumulator now fails 4 of 9, and the clean tree passes 9.

2. THE SPLIT TEST WAS TAUTOLOGICAL. After the split ComputeDetAMResponse IS
   SourcePolarizationBasis composed with ComputeDetAMResponsePrecomputed, so
   comparing them cannot fail. It passed with a sign flipped in the source-only
   half, the response matrix doubled in the per-detector half, and the speed of
   light wrong by 0.1%. Rewritten against a FROZEN copy of the pre-split bodies;
   all three sabotages now fail. The claim in DESIGN_noloop_per_detector_glue.md
   is corrected rather than deleted, because the general lesson is worth keeping:
   when a refactor splits a function, the halves are not an independent check on
   each other.

3. THE READ-ONLY TEST TESTED NUMPY, NOT THIS CODE, and its rationale was false.
   It called np.broadcast_to directly and passed on any tree; its docstring claimed
   cupy also returns a read-only broadcast, which is measurably wrong -- cupy's is
   writable and writes through to the base. Replaced with a test of what
   _dense_rho_sq actually returns, and the false claim removed.

4. _simps_weights BUILT AN (npts, npts) IDENTITY ON THE DEFAULT HOT PATH. npts is
   2*window*srate and the driver's DEFAULT srate is 16384, so npts is 2457 in the
   default configuration, not the 614 of the srate-4096 runs everything here was
   measured at: a 48 MB identity and ~97 MB held in cupy's pool, inside the first
   likelihood call of every --vectorized --gpu run, in a function whose n_chunk is
   already bounded by device memory. Now built a block of rows at a time, capping it
   at 5 MB; verified bitwise against the whole-identity result at npts 614, 1000 and
   2457. Also keyed the cache on the quadrature function, not just the backend: the
   GPU and CPU simps differ by 0.405 nats on an under-resolved peak, and a future
   caller passing a different rule at the same shape would silently be served the
   other one's weights.

5. THE loglikelihood= CALLBACK CONTRACT NARROWED SILENTLY. rho_sq now reaches the
   callback as a stride-0 view, so a callback writing in place raises on CPU and
   RACES on GPU -- cupy's broadcast is writable and every column aliases one address.
   No in-tree callback writes (_factored_lnL_helper and the driver's
   distmarg_loglikelihood both allocate), so nothing is broken; densifying at the
   callback boundary would undo the optimization, so this is documented in the
   NoLoop docstring instead.

core-unit-check floors raised to 312/300: baseline 299/287, +4 as the split test
goes 3 -> 7, +9 for the accumulator test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…lidate

Three findings from the automated review, all real holes I left, merged from the branch:

  * my LEGACY predicate was FAIL-OPEN.  It errored only when the file COLLECTED tests, so a file
    that imports cleanly and finds none satisfied "cannot be imported" -- when a clean import is
    exactly what falsifies that status.  Now only a collection/import ERROR counts.
  * my EXPENSIVE predicate asked for "passed none", which a suite that FAILS or ERRORS also
    satisfies.  A broken suite is not a guarded one.  Now: collects, and SKIPS in a run that
    exits cleanly.
  * my collected-count used len(re.findall("::")), which double-counts class-based node ids.

AND THE ONE THAT MATTERS MOST.  test_integrator_studies.py invoked the five studies WITHOUT
--as-test.  All five keep their scientific comparisons and their SystemExit(1) behind
`if args.as_test` (verified in each: lines 144/70/179/230/126), so the wrapper I shipped as "the
AV bias gate now has CI behind it" detected only CRASHES and gated none of the five criteria it
named.  The evidence I offered for it -- three consecutive runs exiting 0 -- could not have come
out any other way.  That is the inert-guard class this whole effort exists to catch, authored by
me, and review caught it rather than I.

With the flag the gates actually run and all five pass, printing their real criteria ("all warm
starts no more biased than cold", "oracle improved sampling and stayed unbiased").  They cost
8/4/19/7/5 s instead of 5/2/14/4/4 -- larger precisely because the work now happens.  The roster
timings said the old numbers and are corrected, with the reason recorded beside them.

Re-measured on the merged tree: core-unit-check 319/307/12 PASS (315 s), roster-verify-check 50
predicates PASS (340 s), ci-roster-check PASS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…-broadcast

NoLoop: accumulator shapes and the time integral (-44% cumulative with #255)
… on a runner

roster-verify-check went red on the runner, and was right.  The entry was
OPTDEP needs:glue,htcondor; with htcondor absent there it still collected 15 and passed 15.
Confirmed locally with BOTH blocked via a sys.meta_path finder: 15/15.  Its `import htcondor`
and `from glue import pipeline` are capability probes inside the tests, not requirements.

CIT has both packages, which is why CIT could not see this and a runner could -- the same
asymmetry that produced the rimsky mistake, this time caught by the check instead of by a red
core-unit-check.  That is the predicate earning its place: it is the third false reason it has
found, and the first that only a runner could expose.

Moved into core-unit-check's manifest; floors re-measured in the follow-up commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…'s own floors

SECOND merge conflict on the same floors: rift_O4d added test_noloop_accumulator_shapes.py
(+8/+8, 299/287 -> 307/295) while this branch was at 319/307 and adding
test_backends_lowlevel.py.  Resolved the same way -- keep every manifest entry from both sides
and RE-MEASURE, never add the deltas.  Measured: 347 collected, 335 passed, 12 skipped, 363 s.
The history block now records that arithmetic has been the wrong method twice.

THE RESOLUTION EXPOSED A FAIL-OPEN IN THIS SCRIPT.  With the MEASURED_* placeholders still in
place the gate reported

    == outcomes: 347 collected, 335 passed, 12 skipped, 0 failed, 0 errored ==
    line 197: [: MEASURED_TESTS: integer expression expected
    rc=0

`[ 347 -lt MEASURED_TESTS ]` does not fail the build: bash prints to stderr, returns 2, and the
`if` is simply FALSE.  A malformed floor therefore DISABLES the comparison it appears to
perform, and the gate goes green having checked nothing.  `set -u` catches a MISSING floor --
it did, earlier in this branch -- and nothing caught a malformed one.

The three floors are now validated as non-negative integers, and that check sits ABOVE the
per-file collection loop so it reports in 1 s rather than after 363 s; a developer-error guard
that costs six minutes to fire is one people route around.  Checked both directions: the
placeholders are rejected, AND a valid-but-too-high floor (999) still fails, so the comparison
really runs rather than merely being reachable.

Also in this merge: test_backends_lowlevel.py moves from the roster into the manifest.
roster-verify-check found its OPTDEP reason false ON THE RUNNER -- htcondor absent there, and it
still collected 15 and passed 15.  Confirmed locally with glue AND htcondor both blocked via a
sys.meta_path finder: 15/15.  Those imports are capability probes inside the tests.

Every runner-vs-CIT disagreement this round ran the same way: CIT is richer, and that richness
hid a false claim (asimov for rimsky, htcondor here, jax for jax_gp).  Two of the three were
caught by CI rather than by a local run that preceded it.

Merged tree: ci-roster-check PASS (209 files, 159 reachable, 50 rostered), core-unit-check
347/335/12 PASS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…y there

PR #256 rewrote test_vectorized_lal_tools_split.py (3 -> 7 tests) and added a
ninth test to test_noloop_accumulator_shapes.py (8 -> 9), but the floor update
never reached the commit: the edit was made on one host and committed over ssh
from another before NFS made it visible, so git add -A staged the previous
content. Nothing failed, because the floors are MINIMUMS and 312 collected
comfortably clears 307 -- which is exactly the silent under-coverage this gate
was built to prevent, and its own comment warns about: "a floor left at the old
value passes while covering less".

Baseline is unambiguous. At ebb4b58 the floors were 299/287 with the split
file contributing 3 tests, and the ONLY change to the FILES manifest between
that commit and rift_O4d HEAD is test_noloop_accumulator_shapes.py. So
299 + 4 (split 3 -> 7) + 9 (accumulator) = 312 collected, and 287 + 13 = 300
passed. MAX_SKIPPED is unchanged: neither file skips or xfails.

The comment block records why the split file grew, since the reason is the
useful part: it compared the combined wrapper against the composition of its own
two halves, which after the split IS the wrapper, so it could not fail. It is
now checked against a frozen copy of the pre-split bodies and parametrized per
detector.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The runner reports 350 collected / 338 passed for this manifest; CIT reports 347 / 335.  The
results are IDENTICAL -- both "335 passed, 11 skipped, 1 xfailed" -- and per-FILE collection is
347 on both, file by file.  The gap is pytest-subtests: present in the runner's dependency
closure, absent from CIT's IGWN environment, and it counts the three `subTest` blocks in
test/backends/test_backends_lowlevel.py as separate cases in the junit XML this gate parses.

Floors stay at the PLUGIN-FREE 347/335, which is robust in the direction that matters: the
runner's 350/338 clears them today, and if pytest-subtests ever leaves that closure the counts
fall back to 347/335 and still clear.  Raising them to 350 would convert an unrelated dependency
change into a red gate -- and the note is here because "the runner measured more, so raise the
floor" is the obvious wrong move for the next person, including me.

Found by diffing per-file collection against the combined junit rather than accepting that a
higher number on the runner was self-explanatory.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
247 rewrote the opening paragraph to describe both phi paths and left the
SCOPE block forty lines below it saying "Localizing phi as well ... is not
attempted here".  Same docstring, opposite claims, and the second one is the
stale one -- the profile F(phi) and its envelope derivatives it says would be
needed ARE u_profile, and phi_local_lnI is built on them.

This is the defect the integration review predicted for this file and it was
only half fixed: the header was reconciled during the 250 merge and this block
was missed, so main now carries a docstring that argues with itself.

Rewritten as two rules rather than one, which is what the file actually
contains: joint_lnL_phi_dense scans phi densely and is the production path;
phi_local_lnI localizes phi too, declines rather than returning an unbounded
number, and is not wired into production.  The retraction is left visible
rather than silently overwritten, because "true when written" is the reason
this kind of claim goes stale.

Documentation only; no executable change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ors-after-review

core-unit-check: raise the floors to match the tests that are actually there
…cope_fix

Documentation only. Fixes a module docstring on rift_O4d that made opposite claims about the same file forty lines apart -- the half of the #250/#247 reconciliation that was missed.
…egrid

ILE: add accurate reflected Q time pregrid
rift_O4d raised core-unit-check's floors 307 -> 312 in #258.  That is a legitimate raise and a
good catch on their side, but merging it changes THIS manifest's numbers by ZERO: measured
before and after, 347 collected / 335 passed / 12 skipped.

The reason is the whole argument for re-measuring.  #258's growth -- test_vectorized_lal_tools_
split.py 3 -> 7 and test_noloop_accumulator_shapes.py 8 -> 9 -- had ALREADY landed in the files
via eedf7a8; only rift_O4d's floors lagged its own contents, so 312 collected was clearing a
307 floor.  #258 reconstructed 312 arithmetically (299 + 4 + 9); this branch had measured 347
with those same files already grown, because it never adds.  An arithmetic floor can sit one
commit behind reality without anything failing; a measured one cannot.

Conflict shape: git saw delete-vs-modify, because this branch had relocated the floors block
above the per-file collection loop, so their side arrived whole and ours empty.  Dropped the
conflicted region and folded their per-file rationale into the history here -- the reasons those
two files grew are the useful part, and both are inert-test findings worth keeping: a comparison
that pitted the combined wrapper against the composition of its own halves (so it could not
fail), and synthetic inputs that left kappa_sq identically zero (so an assertion held for the
wrong reason).

Dropped one line from their side: "CONFIRMED ON A RUNNER ... reported the same 278/266/12".  It
is no longer true.  The runner reports 350/338 against CIT's 347/335 -- identical results, plus
three subTest cases that pytest-subtests counts and CIT's environment cannot -- and that is
documented immediately above.  A "confirmed identical" claim beside a known divergence is worse
than no claim.

Measured with deliberately impossible floors (999999) so the run had to fail and report real
counts, rather than relying on noticing a leftover placeholder.  All 35 manifest entries from
both sides survive.  ci-roster-check PASS (159 reachable, 50 rostered); core-unit-check
347/335/12 PASS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…cted twice

Two hunks, both in .travis/test-jax.sh.  The FILES array takes the UNION of both
sides (PR #250's two files and this branch's one).  The floor is re-derived from
a collection RUN over the merged tree: 451/452 collected (1 deselected) from 31
files.

Worth recording that this branch has now hit this conflict TWICE.  339 was
itself a merged collection read off a real run, and it was stale inside a day --
so the constant does not go stale through carelessness, it goes stale because
rift_O4d moves faster than any one branch.  And BOTH times the arithmetic would
have landed on the right answer (312+24+3 = 339, 424+27 = 451), which is exactly
what makes it an unreliable shortcut: it is usually right, so the one time it is
wrong there is no habit of checking left to catch it.

rift_O4d rewrote 389 lines of samplers.py and 82 of the driver under this
branch.  Git merged them without conflict, which is not the same as correct, so
the #227 work was re-checked against the merged tree rather than assumed: all
eight regularize_cov call sites survive, the AST class gate finds no new
cholesky-with-inline-identity, and all 27 pins pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tion

Fix JAX drop-in handling for Q pregrid option
…epted

The phi-local rule declined nearly every row that realized its own cost win.
Measured before this change, sweeping KP x amplitude, exactly ONE case in 36
both localized (area_outside > 0) and accepted -- at amplitude 100.  Everything
above that localized into 4-14 regions and was refused.

THE OMITTED MASS WAS NEVER THE PROBLEM.  At amplitude 3e4 the TRUE margin is
about -66 nats against a -23 threshold: the row deserved acceptance by a wide
margin.  The computed margin was +2.7e5.  The entire gap was one term.

M2F = M20 + M10^2 bounds Var(d_phi g) by its worst case -- as if d_phi g spread
over its whole range under exp(g) du.  That measure CONCENTRATES as amplitude
rises, so the bound is loosest exactly where the physics is tightest, and the
Taylor lift M2F delta^2 / 2 sat five orders of magnitude above the integral.
Refining the grid cannot rescue it: n_bound * u_nodes both grow with amplitude,
and at 3e4 that is 8192 * 4 * 7302 evaluations -- the process is killed.

So the bound stops going through the profile at all:

    F(phi) = log int exp(g) du  <=  log(2 pi) + max_u g(phi, u)

max_u g is exact -- four quartic roots u_stationary_roots already returns -- and
its Lipschitz constant is M10 by the envelope inequality, not M10^2, so
refinement is LINEAR.  sup_g_bound costs no quadrature and is flat in amplitude.
required_bound_grid sizes the grid from M10 the way required_u_nodes sizes u.

VERIFIED SOUND rather than assumed: bound - F >= +1.15 nats over KP x amplitude
x phi, slack 2.8-5.5, which is the Laplace width log(sqrt(2 pi)/sigma_u) it
throws away.  A test checks it directly against the profile.

Measured after, with both knobs sized:

    KP=3 amp=1e3   5 regions  margin -33.9  ACCEPTED  err 1.1e-13
    KP=3 amp=3e3   4 regions  margin -58.7  ACCEPTED  err 0.0
    KP=5 amp=3e3   8 regions  margin -24.9  ACCEPTED  err 1.7e-11
    KP=9 amp=1e3   8 regions  margin -48.3  ACCEPTED  err 1.1e-13

Localized, accepted, and right to machine precision -- the regime the cost
argument was always about, and which was unreachable before.

TWO REVIEW FINDINGS RETIRE BY CONSTRUCTION.  The lift could be applied to a
profile the u fallback had underestimated; there is no profile on that grid now,
and a test asserts sup_outside is BIT-IDENTICAL across an 8x range of u_nodes.
And u_sizing_ok moves to the quadrature grid, where it belongs -- it reports
whether the integration that produced `value` was sampled adequately, not the
grid that bounds F from outside.  It is non-vacuous there: it fires on a fixture
whose margin (-29.3) and resolution both pass at 48 u nodes.

The nested-grid test drops from four profile grids to THREE.

31 tests pass.  Gate floor re-collected: 424 -> 426.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…had not done

All three review findings reproduced before fixing, and each matched the report exactly.

[P2] REQUIRED DEPENDENCIES COULD BE LABELLED OPTIONAL.  _in_requirements compared the IMPORT
name against requirements.txt's DISTRIBUTION names, so `needs:sklearn` sailed past a line
saying `scikit-learn`, and `needs:lal` past `lalsuite` -- both installed by CI, both therefore
not optional, both accepted.  Reproduced for sklearn, lal and lalsimulation.

The import name is now resolved to the distributions providing it, cheapest first: the metadata
index, then a scan of each distribution's files for a top-level `<mod>/`, then a short alias
table.  The table is not laziness: on CIT `lalsuite` is a conda metapackage whose dist-info
declares NO top-level modules (files() shows only __pycache__ and the dist-info), so nothing
can resolve `lal` there, while a pip wheel does declare them.  That limit is written down.
Checked that no real roster entry changes verdict: jax, hydra, omegaconf, EOBRun_module,
asimov, liquid, nflows and vegas all still report correctly absent from requirements.

[P2] TIMEOUTS PASSED SILENTLY.  The OPTDEP branch discarded both the collection and the run
timeout and still counted the entry as checked.  With TIMEOUT dropped to 1 s on an OPTDEP-only
roster -- every subprocess timing out -- the old code reported "OPTDEP 8 checked ... PASS".  It
verified nothing and said so affirmatively, which is precisely the failure this file exists to
remove.  Both paths now error, saying that a timeout is not a pass.

[P2] THE EXPENSIVE CHECK INHERITED THE OPT-IN FLAG.  _pytest copied os.environ, so the run
billed as "without RIFT_RUN_EXPENSIVE" kept it when the caller had it exported.  That INVERTS
the predicate: measured with the variable set, a correct guard gave rc=0 skipped=1 passed=3 and
would have been reported broken, while an inverted guard would skip and be reported fine.
_pytest now removes a variable when its value is None, and both EXPENSIVE subprocesses -- the
run and the collection, since a module-level skip can change what is collected -- drop it.
Same file now gives skipped=4 passed=0.

Mutations, each broken and seen to fail:
  OPTDEP needs:lal      -> FAIL "which requirements.txt DOES install"   (passed before)
  OPTDEP needs:sklearn  -> FAIL, same                                    (passed before)
  OPTDEP collection timing out -> FAIL "a timeout is not a pass"         (PASS before)
And the direction that must NOT fail: the full roster passes both in a clean environment and
with RIFT_RUN_EXPENSIVE=1 exported, which before this fix would have reported a false failure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI: check that roster reasons are true, not just present
The guard compared the adapted estimate against the pilot's RAW estimate. The
pilot is prior Monte Carlo -- unbiased, but with a heavy right tail once the
target is a tiny fraction of the prior: for a mode of prior mass m one lucky
draw gives ~L_max/n_pilot against a truth of ~L_max*m, overshooting by
1/(n_pilot*m). That same draw is what _moment_match centres the adapted proposal
on, so an inflated reference and a well-seeded proposal arrive together, and a
correct answer is converted to nan.

MEASURED, not conceded on argument. At a synthetic width of 0.05 rad
n_pilot*m = 1.6e-3 and the pilot ran up to +5.46 nats above the truth,
P = 1.1e-3 over 900 seeds; +6.31 nats at 0.03 rad. The mechanism is real.

The fix is prior_pilot_floor(): a Markov lower confidence bound. Markov needs
only non-negativity and unbiasedness, both of which hold, so ln Zhat + ln rate
is a floor at level 1 - rate. The threshold becomes a CHOSEN false-positive rate
instead of a tuned constant, and it is distribution-free -- it does not assume
the pilot resolved anything. That assumption would have been false: pilot ESS is
~1 (median 1.0-1.3) in every regime where the guard does any work, so "rests on
one draw" is the pilot's normal state, not its failure state.

rate = exp(-8), read off a measured operating curve over 5400 runs rather than
asserted: against exp(-5) it costs 4.5 points of power (0.651 -> 0.606) and buys
a 20x smaller worst-case false-positive rate. A false positive fails the event
and writes no row, so it is worth paying for.

Two alternatives were measured and rejected with numbers, not opinion. A
bootstrap lower confidence bound loses too much power (misses 244 vs 148
inaccurate runs at 0.06 rad) -- publishing a wrong number is the failure this PR
exists to stop. Gating on pilot ESS is worse than useless: every firing observed
had pilot ESS < 5, so the gate would disable the guard exactly where it works.

The test-design half of the finding was right without qualification. The narrow
cases asserted isnan on EVERY seed, so a recovered, accurate answer would have
been recorded as a regression -- a test that can only pass while the code fails
cannot witness the guard being too aggressive, which is the risk under review.
They now assert the property that matters: no inaccurate number is ever
published, and an accurate one is explicitly allowed through.

Added the regression case asked for, found by sweeping 5400 runs for the shape:
sig=0.15 seed=885, pilot +1.35 nats above truth, adapted right to 0.001 nats at
neff 1.4e4. Honest limit, stated in the test: over that sweep the largest
pilot-minus-adapted gap on an accurate run was +1.654 nats, so no false positive
was ever observed and this case does not reach the shipped threshold. It pins
the margin; the Markov bound, not the sweep, is what the guarantee rests on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two P1s, one of which invalidates the certificate this PR exists to fix.

SUP_G_BOUND RETURNED A LOWER BOUND WHEN THE U QUARTIC DEGENERATES.  It took
max over u_stationary_roots as if that set contained the maximizer.  A max over
candidates is a LOWER bound unless it provably does, and u_stationary_roots
builds a companion matrix for c2 z^4 + ... and substitutes lead = 1 when c2 = 0
-- solving a DIFFERENT polynomial -- so for a table with no q = +-2 content its
angles need not contain the maximizer.  Measured over four such draws: 0.024 to
0.092 nats BELOW log(2 pi) + max_u g.  Not a loose bound, an invalid one, and
every margin in this PR rests on that inequality.

a + |c1| + |c2| >= max_u g holds for every table and needs no roots, and in the
degenerate regime it is also TIGHT because c2 -> 0 makes max_u g -> a + |c1|.
It is used wherever the quartic cannot be trusted -- |c2| <= 1e-8|c1|, or an
argmax that fails a stationarity test against the axis's exact derivative bound.
Re-measured: +0.00000 on all four.

Every table elsewhere in the suite carries full mode content, so nothing could
see this.  The regression test is the degenerate table itself.

EMPTY MERGED-REGION SLOTS VOTED ON u_sizing_ok.  Found by this session's review
and by external review independently, and the external description was sharper:
an empty slot is neutralized for the VALUE by zeroing its position and masking
its WEIGHT, but its nodes are still evaluated at the artificial point phi = 0
and their fallback counts were summed with the rest.  A risky cell there could
decline a row whose every contributing node was adequate; the reported
n_u_*_quad counters were contaminated the same way.

The tell was that the counts tracked the SLOT ALLOCATION and not the regions: 5
risky at n_slots=2 rising to 176 at n_slots=8 while the region count only went
2 -> 4.  So the test pins that the counters do not move once slots exceed
regions, which no real structure could cause.

Also: the docstring claimed this bound is "strictly better" than the profile
route while disclosing eight lines later that it discards the Laplace width and
sits ~6 nats high.  Both cannot hold -- the profile bound converges to F under
refinement and this one does not.  Reworded, and the reason no affordable grid
reaches the crossover is stated.

seg_lo/seg_width are now exported, because the soundness check for this bound
must compare it to the set it is a bound ON.  Comparing to the GLOBAL sup of h
reads w_sigma^2/2 = 72 nats low and condemns correct code, which is what my
first attempt did.

33 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
oshaughn and others added 19 commits September 9, 2026 07:08
`multipeak_planner.multipeak_local_marginalize` was callable only from inside
its own module: ANGLE_MARG_CHOICES did not list it and wrapper.py contained no
reference, so no driver path could select the controller that was validated on
the ladder-2 injection. This wires it as --angle-marg-scheme multipeak.

Operating point is the measured one (tier0 (2,3,24), tier1 (3,5,48), tol 1e-3,
cell_sigma 5, quadrature_order 7, refine_iterations 18), with log_measure
derived by policy_log_normalization rather than chosen. Guard comes from the
caller; it defaults to 16, the value the ladder measurement used, and the
driver's production default is larger.

The scheme OWNS the time integral, so it refuses return_lnLt instead of
returning a wrong-shaped array, and no time_quadrature reaches it.

The reserve is a PEAK-LOCAL time rule, not a refined whole window: nodes sit
only in +-12 sigma_t windows about the envelope peaks at spacing sigma_t/8, so
window and spacing scale together and the node count does not grow with rho. A
refined whole-window reserve needed 4905 nodes for a peak 0.06 native samples
wide at rho 163 and failed its own half-refined warrant at rho 40.77; this rule
reproduced it to 0.0 on every row with 193 nodes.

Defaults unchanged: multipeak is reachable by name only and choose_angle_marg_
scheme never returns it.

Tests (7, wired into .travis/test-jax.sh in this commit, floor 754 -> 761):
membership and export, absence from auto, the return_lnLt refusal, shape and
finiteness, provenance, optparse choice validation through a subprocess, and a
seam test that calls the REAL coefficient_table_distphipsimarg_laplace rather
than a stand-in -- a fake there passes while the shipped call raises.

Regression: 112 passed across the peak-local, default, buffer-cap and
gh-selection wiring suites on /scratch/$USER/envs/jaxci-py311 (jax 0.10.2,
cupy absent, CPU backend), ldas-pcdev13.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ve-kernel-kwargs

jax_ile: psi-Laplace reserve kernel called with the exact kernel's keywords
… EXPECTED_TESTS=762 measured

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ltipeak

jax_ile: reach the four-axis multipeak controller from the driver
…ossterm-batching

ILE precompute: batch the mode cross terms (26x on the cross terms, 3.6x ILE wall)
At rung 652 the peak-local reserve refused 3 of 8 rows on the focus
certificate: the locator's search grid maximizes the angles on a 64-node phi
lattice whose ripple, about rho^2 (pi/n_phi)^2 = 1000 nat at rho 652, exceeds
the 25-nat change of the profile per search cell, so the search maximum
landed up to 0.4 samples off, beyond the polish's reach; refining the time
search did not help, and the (phi, u) Newton polish clipped at 0.1 rad x 3
steps sat 500 nat low. New PolicyConfig fields (static shapes, in the ledger):
reserve_peaklocal_search_phi_nodes = 4096 (ripple under 1 nat to rho 1300),
reserve_peaklocal_newton_steps = 8, reserve_peaklocal_newton_step_max = 1.0;
locate_time_maxima takes newton_steps/newton_step_max. Test: a carrier at rho
632, tau 8, is located within tau/rho at the profile's true maximum.

Measured (likedata_snr640.pkl, S=8 draw, guard 128, GH 16, decline forced):
8/8 rows warranted at the first tier, 357 nodes, 28 s/row; the refused rows
are located (row 0 centre 307.108 vs the dense-lattice maximum 307.109).
Rung 163: 8/8 unchanged to all printed digits at 9 s/row. DESIGN carries the
sweep table and the before/after rows. EXPECTED_TESTS collapsed to the single
measured value 763 (the #312/#313 merges had left three assignments).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ator-sizing

jax_ile: size the peak-local locator's angular search from the amplitude (rung 652 usable)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Conflict: .travis/test-slowrot.sh EXPECTED_TESTS.  #307 raised 43 -> 47 (its
test_slowrot_rotating_freqresponse.py), this branch raised 43 -> 46 (three block-form
tests in test_slowrot_freqresponse.py).  Neither number is the merged floor; re-derived
by running the gate: 50.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…se-vectorize

likelihood: build the finite-size response coefficients per block (tau_it,like 1707 -> 37 us)
JAX ILE: add value-only AV and portfolio sampling
@oshaughnessy-junior
oshaughnessy-junior deployed to private-review-dispatch-rift-upstream September 11, 2026 00:05 — with GitHub Actions Active
@oshaughnessy-junior
oshaughnessy-junior deployed to private-review-dispatch-rift-upstream September 11, 2026 00:12 — with GitHub Actions Active
@oshaughn
oshaughn merged commit c00f0a3 into oshaughn:rift_O4d Sep 11, 2026
10 checks passed
@oshaughn
oshaughn deployed to private-review-dispatch-rift-upstream September 11, 2026 00:13 — with GitHub Actions Active
mebiri pushed a commit to mebiri/research-projects-RIT that referenced this pull request Sep 15, 2026
mebiri pushed a commit to mebiri/research-projects-RIT that referenced this pull request Sep 15, 2026
mebiri pushed a commit to mebiri/research-projects-RIT that referenced this pull request Sep 15, 2026
…p-kernel-bounds

fit_gp: make the kernel bounds settable, and report when a fit is saturated
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