Skip to content

Rift o4d junior calmarg in loop: AFTER main 'distance' merge#139

Merged
oshaughn merged 297 commits into
oshaughn:rift_O4dfrom
oshaughnessy-junior:rift_O4d_junior_calmarg_in_loop
Jun 30, 2026
Merged

Rift o4d junior calmarg in loop: AFTER main 'distance' merge#139
oshaughn merged 297 commits into
oshaughn:rift_O4dfrom
oshaughnessy-junior:rift_O4d_junior_calmarg_in_loop

Conversation

@oshaughn

@oshaughn oshaughn commented Jun 1, 2026

Copy link
Copy Markdown
Owner

calmarg done in the ILE loop, including

  • 'fused' : new kernel, specifically fast GPU-izing the code
  • 'loop': brute-force backtest, looping over cal realizations
    as well as fancy tools to
  • cal-pilot : adaptively sample in cal parameters, to enable sane results at high SNR

oshaughn and others added 30 commits June 1, 2026 08:52
…kes_digits

Update util_TestSpokesIO.py: fix digits call to opt.digits
…rough (incl. time samples)

Adds `make pp` (pp-build + pp-validate): exercises the FULL top-level builder
util_RIFT_pseudo_pipe.py -> helper_LDG_Events.py -> args generation -> create_event_*,
not just the lower-level builder that dag-build calls directly.  Offline build-validate
(the established pattern: .travis/test-build.sh + demo/pipeline/zero_spin_phenomD), reusing
the zero-spin IMRPhenomD ini + ref coinc + a placeholder fake-data cache, so no GPU/data
run is needed for the threading check.  Zero spin (--assume-nospin); iterations forced
small (--internal-force-iterations).

Confirms EVERYTHING threads through the generated pipeline (validated, not just emitted):
- calmarg --calibration-envelope-directory / --calibration-n-realizations /
  --calibration-fused-kernel land in args_ile.txt, ILE.sub AND ILE_extr.sub;
- TIME SAMPLES: --add-extrinsic-time-resampling + --internal-ile-srate-time-resampling ->
  --srate-resample-time-marginalization 4096 in the wide AND extrinsic (ILE_extr) stages,
  alongside --time-marginalization;
- zero-spin IMRPhenomD; small iteration count; full top-level DAG produced.

(Note: --last-iteration-extrinsic-time-resampling is a transient builder arg consumed at
build time -- its persistent effect is the --srate-resample-time-marginalization in
ILE_extr.sub, which is what we assert.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…a (breadcrumb)

Primes and launches a full util_RIFT_pseudo_pipe.py pipeline that ACTUALLY RUNS on the
zero-noise CI fake data (not just the offline pp-build threading check).  Pieces:
- util_SimInspiralToCoinc.py makes a real coinc from the injection (H1/L1/V1, t=1000000014,
  m1=35/m2=30, SNR 17.5) -- `make pp-coinc`/ci_coinc.xml (regenerated, not committed).
- calmarg_ci.ini: CI-matched (FAKE-STRAIN channels x3, srate 4096, seglen 8 -> the segment
  [1000000008.236,1000000016.236] sits inside the cache's 328s frame, fmin 10, zero spin,
  mc [23,35]).
- pp-run-build runs pseudo_pipe with the real zero_noise.cache + CI PSD + calmarg
  (--calmarg-* --calmarg-fused-kernel) + time-resampling (--add-extrinsic-time-resampling
  + --internal-ile-srate-time-resampling 4096) + --assume-nospin + small forced iterations,
  and asserts the RUNNABLE bits threaded (real cache, FAKE-STRAIN, event time, and that
  ILE_extr.sub carries calmarg + --srate-resample-time-marginalization).
- pp-run builds + condor_submit_dag.  Verified: builds clean, submits (the extrinsic stage
  produces TIME SAMPLES with calmarg on).  Single GPU on cardassia (CUDA_VISIBLE_DEVICES=0).

This is the runnable counterpart to pp-build; leaves a working end-to-end breadcrumb for
exercising the full pipeline + calmarg + time samples later.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…TY_RIFT_IMAGE is set

CIT is container-only (needs SINGULARITY_RIFT_IMAGE + --use-osg).  Make pp-run "just work"
there: exporting SINGULARITY_RIFT_IMAGE (or passing OSG=1) auto-populates the OSG target
space --
  --use-osg (-> builder adds --use-singularity, reads SINGULARITY_RIFT_IMAGE),
  --use-osg-cip, --use-osg-file-transfer,
  --ile-additional-files-to-transfer cal_env/{H1,L1,V1}.txt + the PSD (no shared FS on OSG),
  and links the CI frames into rundir_pp_run/frames_dir for --use-osg-file-transfer.
A guard errors if OSG=1 but SINGULARITY_RIFT_IMAGE is unset.  On cardassia (env unset,
OSG=0) it is a clean no-op -- the local shared-FS pp-run is unchanged.
  CIT:  export SINGULARITY_RIFT_IMAGE=/cvmfs/.../rift.sif ; make pp-run

Verified by dry-run (make -n): env set -> all OSG flags + transfer list + frames link;
env unset -> zero OSG flags; OSG=1 w/o env -> guard fires.  NOTE: full OSG validation
(esp. transferred-file path rewriting for the PSD/cal envelopes) awaits a working CIT
container build (currently blocked); the flag layer follows the standard RIFT OSG pattern.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two small CIT fixes (per review):
1. pp-run-build wrongly FAILed "not the real CI cache" in OSG mode: OSG uses local.cache
   (built on the remote worker), not zero_noise.cache.  The cache-name assertion is now
   OSG-aware -- OSG=1 just notes local.cache (no hard fail); local shared-FS still asserts
   zero_noise.cache.  The other runnable-bit checks (FAKE-STRAIN, event time, ILE_extr
   calmarg + time-resampling) are unchanged.
2. pp-run now prints a warning after submit if LIGO_USER_NAME or LIGO_ACCOUNTING
   (a.k.a. LIGO_ACCOUNTING_GROUP) are unset -- at CIT, without these the condor jobs never
   queue.  Silent when set.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…headroom)

Per review: a container needs ~4 GB more disk than the MB-scale shared-FS baseline (the
.sif image is transferred + unpacked on the worker).  OSG mode now adds
--internal-{ile,cip,general}-request-disk (default PP_OSG_DISK=4G, overridable) to the
pp-run pseudo_pipe invocation, covering all container job types.  Local (non-OSG) mode
unchanged.  Verified by dry-run: OSG=1 -> all three disk flags at 4G; OSG=0 -> none.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…disk request

The container +4GB disk bump only took for cip/general; ILE stayed at 4M because
calmarg_ci.ini set internal-ile-request-disk="4M" in [rift-pseudo-pipe], and an ini value
there OVERRIDES the util_RIFT_pseudo_pipe.py CLI (cip/general weren't in the ini, so they
got the CLI 4G).

Fix: remove the disk line from calmarg_ci.ini and drive ALL THREE disk requests from the
CLI, ALWAYS: --internal-{ile,cip,general}-request-disk = PP_DISK, where PP_DISK is 4M
(local baseline) or 4G (container/OSG).  Verified by dry-run: OSG -> all three 4G; local
-> all three 4M; ini sets no request-disk.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…senames, present)

On OSG (no shared FS) the wide-ILE PSD and cal-envelope args referenced ABSOLUTE paths the
worker can't see, and the per-IFO PSD files the transfer list expected were never created.

PSD (demo Makefile): on OSG, drop --use-online-psd-file (which forced the absolute HLV
path into the args) so RIFT emits the normal per-IFO basenames --psd-file H1=H1-psd.xml.gz
(which it lists + transfers), and COPY the CI PSD into the run dir as {H1,L1,V1}-psd.xml.gz
so those files exist AND carry the real PSD.  Local (shared-FS) mode keeps
--use-online-psd-file (absolute path, no transfer).  Verified: OSG args are basenames,
files present; local unchanged.

Cal envelopes (util_RIFT_pseudo_pipe.py, the general fix -- part of task oshaughn#23): when
--use-osg-file-transfer, reference --calibration-envelope-directory as '.' (the per-IFO
<IFO>.txt land flat in the job scratch dir) and auto-append them to the ILE transfer list,
so calmarg-on-OSG works without the user remembering --ile-additional-files-to-transfer.
Local mode unchanged (absolute path).  Verified the OSG args_ile.txt + helper_transfer_files.txt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
condor refuses "Transfer of symlinks to directories is not supported", so the OSG
frames_dir is now `cp -r` of the CI frames (not ln -sfn).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…d to ILE as stray arg)

Hardening (task oshaughn#26): args_ile.txt starts with a placeholder token 'X' (stripped by
create_event_parameter_pipeline_BasicIteration), not the exe.  util_CalPilotStage only
recognized an exe first-token, so 'X' leaked into the pilot's ILE dump command as a stray
positional argument.  It happened to be tolerated by optparse, but is wrong/fragile.  Now
drop the first token when it is 'X' or contains 'integrate_likelihood'.  Verified on a real
pseudo_pipe args_ile.txt: X dropped, $(macro) stripped, pilot opts removed for re-supply,
calmarg + time-marginalization kept.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…pipe (CI data)

Adds PP_PILOT toggle + `make pp-run-pilot` (and pp-run-pilot-build): pp-run with
--calmarg-pilot, so the full top-level pilot DAG (harvest->dump->fit->consolidate->seed
wide_{N+1}) runs on the CI fake data.  build-validate asserts CALPILOT.sub runs
util_CalPilotStage.py, the CALPILOT job is in the DAG, and the wide ILE args carry the
--calibration-proposal-breadcrumb seed.  Honours OSG/CIT like pp-run.  Verified the build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…(task oshaughn#23)

The CALPILOT job runs ILE internally, so on OSG it needs the same container + input set as
a wide ILE job.  write_calpilot_sub gains use_osg/use_singularity/frames_dir/transfer_files
(mirrors write_ILE_sub_simple):
  - runs in the singularity image (exe at SINGULARITY_BASE_EXE_DIR; transfer_executable
    False; MY.SingularityImage/BindCVMFS/flock_local; HAS_SINGULARITY requirement);
  - a calpilot_pre.sh prescript rebuilds local.cache (relative paths) from the transferred
    frames, then execs the stage;
  - transfer_input_files = transfer_files (PSD + cal envelopes, sans the wide grid) +
    frames_dir + composite + args_ile.txt; transfer_output_files = the consolidated
    breadcrumb; stage args reference BASENAMES (no shared FS), workdir '.'.
  - refinement (--prev-breadcrumb) is skipped on OSG (the prev breadcrumb is produced at
    runtime, can't be reliably listed for transfer at iteration 0) -> each OSG pilot is an
    independent cold start, which the prior-shrinkage fit makes safe.
create_event_parameter_pipeline_BasicIteration passes the OSG params + transfer_file_names
to the calpilot job.

ILE robustness: the wide-ILE breadcrumb seed load is now wrapped in try/except -> a
missing/partial/invalid breadcrumb (esp. under OSG file transfer) falls back to PRIOR cal
draws with a warning instead of killing the job.

DONE: the CALPILOT jobs RUN on OSG and produce cal_consolidated_N.npz (transferred back).
REMAINING (task oshaughn#23): consuming the seed on OSG -- transferring cal_consolidated_{N-1}.npz
to the wide_{N+1} ILE jobs (pseudo_pipe basename ref + the iteration-start-absent edge),
so the wide jobs use the learned proposal rather than always falling back to prior.
UNTESTED off-CIT: validate the container + transfer on a real OSG run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ist (task oshaughn#23 complete)

Wide-ILE seed consumption on OSG (util_RIFT_pseudo_pipe.py): when --use-osg-file-transfer,
reference the proposal breadcrumb by BASENAME (cal_consolidated_$(macroiterationprev).npz),
add it to the ILE transfer list, and create a placeholder cal_consolidated_-1.npz so
condor's transfer for the first iteration (prev=-1, never produced) succeeds -- ILE's
breadcrumb load is try/except and falls back to the prior for the placeholder.  So
wide_{N+1} now actually consumes the learned proposal on OSG, not just falls back to prior.

Clean CALPILOT transfer list: write_ILE_sub_simple mutates transfer_file_names in place
(appends frames_dir, ile_pre.sh, the grid), and CALPILOT is built after the wide ILE, so
it had inherited that pollution (frames_dir x3, the wide grid, the ILE prescript).  Snapshot
a clean PSD+cal-envelope transfer list BEFORE those mutations and pass it to the pilot.
Verified: the CALPILOT transfer_input_files now lists each file exactly once (PSD, cal
envelopes, composite, args_ile.txt, frames_dir, calpilot_pre.sh, prev breadcrumb).

This completes the OSG pilot file transfer (CALPILOT runs in-container + transfers I/O;
wide_{N+1} gets the seed).  UNTESTED off-CIT -- validate on a real OSG run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… rundir_pp_run

pp-run-build starts with `rm -rf $(PP_RUN_REAL)`, and pp-run-pilot reused rundir_pp_run --
so launching the pilot demo DESTROYED an in-progress vanilla pp-run.  pp-run-pilot[-build]
now overrides PP_RUN_REAL=rundir_pp_pilot so the two run directories are independent and
neither clobbers the other.  clean removes both.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n the OSG prescript too

On OSG the CALPILOT.sub executable is calpilot_pre.sh (the prescript that rebuilds
local.cache then runs the container's util_CalPilotStage.py), so the stage name is in the
prescript, not CALPILOT.sub.  The build-validate grep now checks BOTH CALPILOT.sub and
calpilot_pre.sh (grep -qs), fixing a spurious "CALPILOT.sub does not run
util_CalPilotStage.py" on OSG.  Pipeline-writer/demo-level only -- no container rebuild.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…+ PoC

The decade-old "save the extrinsic distribution to inform the next iteration" goal,
generalized from the cal pilot's breadcrumb.  GMM-first (mcsamplerEnsemble is already
seedable via gmm_dict).

- breadcrumbs.py (schema v2): the extrinsic slot now carries a per-param-group Gaussian
  mixture -- means/covariances/weights/bounds + the param NAMES (so dim-group indices
  reconstruct against the next run's params_ordered).  cal + extrinsic coexist in one
  breadcrumb.  save/load round-trip test (cal + extrinsic) PASSES.
- RIFT/calmarg/extrinsic_handoff.py:
    fit_extrinsic_proposal(samples, log_weights, groups, bounds, n_comp) -- per group, fits
      with RIFT's OWN gaussian_mixture_model.gmm (the exact fitter the sampler uses in
      update_sampling_prior), so stored means/covs are in the model's internal frame and
      restore byte-identical -- no coordinate guesswork, no sklearn.
    gmm_dict_from_breadcrumb(extrinsic, params_ordered) -- reconstructs gmm objects keyed by
      dim-group indices (looked up by name), ready to seed mcsamplerEnsemble's gmm_dict.
  Standard groups (ra,dec),(distance,incl),(phi_orb,psi).  Handles the GMM running on cupy.
- PoC (__main__): synthetic BIMODAL sky posterior -> fit -> breadcrumb -> load -> seed ->
  the seeded sky GMM recovers BOTH modes.  PASS.

Worktree branch rift_O4d_junior_extrinsic_handoff (off the calmarg branch); does not touch
the running pipeline checkout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cept)

Complete the GMM extrinsic-handoff loop:

- ILE (integrate_likelihood_extrinsic_batchmode, EXECUTE-POINT -- needs container
  rebuild): --extrinsic-proposal-output harvests the run's extrinsic posterior
  samples + importance weights from sampler._rvs after integrate (same weight recipe
  as the distance-grid export, incl. the GMM sampler's raw-integrand storage), fits
  per-group GMMs via RIFT.calmarg.extrinsic_handoff, and writes a breadcrumb.
  --extrinsic-proposal-breadcrumb seed side (pre-fill gmm_dict) was added prior.
  Both wrapped in try/except so the handoff can never break a production integration.

- DESIGN_extrinsic_handoff.md: the decade-old "carry the extrinsic posterior between
  iterations" goal, GMM-first rationale (mcsamplerEnsemble.gmm_dict is trivially
  seedable), module/ILE pieces, PoC result, pilot-DAG plug-in plan, and the AV
  partial-reset limitation (task oshaughn#30: AV resets every integrate(), can only contract).

PoC (python -m RIFT.calmarg.extrinsic_handoff) and breadcrumb round-trip
(python -m RIFT.calmarg.breadcrumbs) both PASS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make the extrinsic handoff usable end-to-end (standalone; does NOT require the cal
pilot), gated by --extrinsic-handoff and requiring the GMM sampler:

- util_RIFT_pseudo_pipe.py --extrinsic-handoff: thread per-event
  --extrinsic-proposal-output extr_proposal_$(macroiteration)_$(macroevent).npz and the
  seed --extrinsic-proposal-breadcrumb .../extr_consolidated_$(macroiterationprev).npz
  into args_ile.txt (OSG: basename + transfer-list + iteration-0 placeholder; shared FS:
  absolute path), mirroring the cal breadcrumb.  Warns if --ile-sampler-method != GMM.
  Passes --extrinsic-handoff[-select] through to the pipeline builder.

- util_ExtrinsicConsolidate.py (NEW): pick the single most representative per-event
  proposal (default by lnL = nearest the peak; neff/n_samples also available) ->
  extr_consolidated_<it>.npz.  Skips unreadable/placeholder inputs; ALWAYS writes output
  (empty if nothing valid) so the next iteration's seed/transfer never fails.

- dag_utils_generic.write_extrconsolidate_sub (NEW): the consolidation job, LOCAL universe
  on the submit node (pure-python file selection, no GPU/ILE/container/frames).  On OSG the
  per-event ILE outputs are transferred back to <wd>/iteration_<it>_ile, so it reads them
  from the shared FS -- no per-event input transfer (which condor cannot glob).

- create_event_parameter_pipeline_BasicIteration: one consolidation node per iteration,
  gated behind that iteration's unify (ILE barrier), and the next iteration's wide ILE jobs
  depend on it:  unify_{it} -> EXTRCONSOLIDATE_{it} -> wide ILE_{it+1}.

- ILE save side: record true lnL + neff in the proposal breadcrumb meta so consolidation
  can pick the most representative point.

- demo/rift/calmarg: `make extr-build` builds + offline-validates the whole thread
  (args_ile.txt flags, EXTRCONSOLIDATE.sub, unify->consolidate->next-ILE DAG edges);
  separate rundir_pp_extr so it never touches other run dirs.

Verified: `make extr-build` passes; util_ExtrinsicConsolidate standalone tests pass
(picks highest-lnL, skips placeholders, writes empty on no-input).

NOTE: the ILE binary change (--extrinsic-proposal-output/-breadcrumb, save+seed) is
EXECUTE-POINT -- rebuild the container before an OSG/CIT run.  The convergence subdag
(--first-iteration-jumpstart) does not yet carry --extrinsic-handoff (same as --calmarg-pilot).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rget

Found by running the GMM extrinsic handoff on a real GPU (cardassia, NVS 510); the full
loop now works end-to-end (iteration-0 writes proposal -> consolidate -> iteration-1 prints
"Extrinsic GMM SEEDED ... [(4,5),(3,2),(0,1)]" for all three groups -> integrates -> writes
the next proposal):

- reconstruct_gmm: move self.bounds onto the GPU (identity_convert_togpu).  The sampler's
  score()/_normalize write into a cupy array, so a leftover numpy bounds raised
  "non-scalar numpy.ndarray cannot be used for fill".
- gmm_dict_from_breadcrumb(existing_keys=...): match each breadcrumb group to the sampler's
  actual gmm_dict key by dim-SET and permute the stored means/covariances/bounds columns
  into that key's order.  Fixes the phase/pol group being silently dropped because the
  sampler pairs (psi,phi_orb)=(0,1) while the breadcrumb stored (phi_orb,psi)=(1,0).
- reconstruct_gmm(cov_inflate=2.0): broaden the seed (a warm start should be conservative;
  the ensemble sampler can contract but starves if seeded too tight).  Mitigates -- does not
  rescue -- a degenerate source: on a bad batch the sampler _reset()s gmm_dict[k]=None,
  i.e. discards the seed and continues cold (correct safety net).  So seed quality tracks the
  SOURCE iteration's convergence; a useful (accelerating) seed needs n_eff in the hundreds,
  i.e. a real --n-max / larger GPU, not the tiny smoke (n_eff~1 -> seed safely discarded).

- demo/rift/calmarg: `make extr-run[-build]` -- tiny GMM extrinsic-handoff pipeline on the CI
  data (300 initial / 200 per-gen intrinsic, 50 evals/ILE job, n-chunk 4000, n-max bounded to
  40000 vs the 4,000,000 production default, >=2 iterations).  Derives a run-specific ini
  (sed) because [rift-pseudo-pipe] ini values override the CLI.  Separate rundir_pp_extr_run.

DESIGN_extrinsic_handoff.md: documents the GPU validation, the two bugs, and the
seed-quality-vs-source-convergence finding.

ILE binary change is EXECUTE-POINT (container rebuild for OSG/CIT).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Background
----------
util_ConstructIntrinsicPosterior_GenericCoordinates.py has long used
three CLI flags for declaring how a parameter is treated:

  --parameter X         both fit dim AND MC sampling dim
  --parameter-implied X fit dim only (the converter produces X from the
                        data file's columns; the MC integrator never
                        sees it)
  --parameter-nofit X   MC sampling dim only (the integrator integrates
                        over it; the fit never sees it)

util_ConstructEOSPosterior.py declared the same three flags but never
honoured them: the integrator at line 487 hardcoded
`low_level_coord_names=dat_orig_names` in its convert_coords closure,
which only worked when the sampling basis equalled the data-file basis;
sampler.add_parameter iterated over coord_names (the fit basis); the
arity dispatch for likelihood_function keyed on len(coord_names); and
sampler.integrate was passed *coord_names rather than
*low_level_coord_names. The net effect: any user who tried to fit in a
transformed basis (e.g. via the new --supplementary-coordinate-code
plugin) silently got a wrong likelihood evaluation -- the rotation was
applied an extra time inside convert_coords every Monte Carlo step.

What this commit changes
------------------------

bin/util_ConstructEOSPosterior.py
  * Parameter-resolution block rewritten to mirror IntrinsicPosterior's
    semantics, plus a clean fallback to dat_orig_names when none of the
    three flags are supplied (legacy bare-invocation unchanged). Seven
    CLI permutations now map to documented (coord_names,
    low_level_coord_names) pairs.
  * The convert_coords closure used by the integrator captures
    low_level_coord_names as its input basis (was dat_orig_names). The
    initial dat->X conversion still uses dat_orig_names, since that's
    the basis of the file columns.
  * Sampler add_parameter loop now iterates over low_level_coord_names
    (the MC basis), and sampler.integrate is passed *low_level_coord_names.
  * The arity-dispatched likelihood_function definitions key on
    len(low_level_coord_names) and route every input -- including the
    scalar branches -- through convert_coords so a non-trivial converter
    is never silently bypassed.
  * Output-writer iterates samples by low_level_coord_names (the keys
    sampler._rvs actually carries) and applies the "constant fill"
    check in the sampling basis, not the fit basis. Implied (fit-only)
    coords correctly skip the output file.
  * Added a guard: if low_level_coord_names != coord_names but no
    coordinate plugin is supplied, raise a clear error instead of
    silently feeding samples through an identity convert_coords into a
    fit built in a different basis.
  * Help text for --parameter / --parameter-implied / --parameter-nofit
    rewritten to describe what each flag actually does now.

RIFT/hyperpipe/coords.py
  * HyperCoordSpec.from_strings accepts integration ranges for names in
    coords-nofit (the MC sampling basis is coords-fit + coords-nofit);
    unknown range names are still rejected.
  * HyperCoordSpec.validate accepts empty coords-fit so long as
    coords-implied covers the fit basis and coords-nofit covers the
    sampling basis; emits distinct errors for empty-fit vs empty-sample.
  * to_parameter_args emits --integration-parameter-range for the
    sampling basis (parameters + nofit), not just parameters.
  * to_puff_args and to_test_args emit --parameter for the sampling
    basis -- the puff lane and convergence-test driver operate on the
    data-file columns, which is the sampling basis after decoupling.

RIFT/hyperpipe/config.py
  * validate_config accepts empty coords-fit when coords-implied
    (fit-side) and coords-nofit (sample-side) are non-empty.

demo/hyperpipe/hyperpipe_conf_linear_uvw.yaml
  * Rewritten to actually exercise the decoupled path: coords-implied
    "u v w" (fit), coords-nofit "x y z" (sample), coords-sample ranges
    in (x, y, z), coord-module pointing at the linear plugin with the
    uvw_rotated chart. Iteration / puff / marg stay in (x, y, z); the
    EOS posterior fits in (u, v, w) and writes its posterior in
    (x, y, z).

Verified
--------
  * Parameter-resolution unit test (in this commit's worktree) covers 7
    CLI permutations -- legacy no-flags, legacy --parameter, IntrinsicPosterior
    --parameter+implied and --parameter+nofit, the new --implied-only,
    --implied+nofit, and full --parameter+implied+nofit -- all map to
    the documented (coord_names, low_level_coord_names) pairs.
  * HyperCoordSpec unit test covers the new decoupled emit (post sees
    implied/nofit and ranges; puff/test see the sampling basis only),
    a legacy-regression case (unchanged output), the two new validation
    errors (empty fit, empty sample), the new "range for nofit name"
    permission, and the still-rejected "unknown range name" case.
  * AST + yaml parses on every edited file.
  * validate_config passes on hyperpipe_conf_linear_uvw.yaml plus the
    demo's baseline and tracer yamls.
… GPU

Attempting the seed-acceleration demo on the CI point (SNR~17.5, lnLmax~90-115) showed the
ensemble (GMM) sampler does not converge there: n_eff pinned at ~1 through ~200k samples,
with OR without calmarg (vanilla GMM: 1.00007 at 196k / 50 iterations).  GMM collapses onto
the dominant sample at a sharp high-SNR peak; AV (the production sampler) handles these but is
not seedable.  So the GMM->GMM handoff is correct+safe but cannot bootstrap a useful seed on
real high-SNR data -- its payoff is gated on seedable/partial-reset AV (task oshaughn#30/oshaughn#25) or a
cross-sampler AV->GMM seed (fit_extrinsic_proposal already accepts any sampler's samples).
Recorded in DESIGN_extrinsic_handoff.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…sal-adapt) + cross-sampler findings

- --extrinsic-proposal-adapt (default OFF = freeze): the seeded GMM groups are no longer
  re-fit each iteration.  Re-fitting a seed on a bad first batch dies in the GMM init
  (random.choice "probabilities are not non-negative") and triggers _reset, discarding the
  seed.  _train already skips groups with gmm_adapt=False, so freezing preserves the seed.
  Freezing is also the right semantics for a handed-off / cross-sampler proposal.  With
  freeze the seeded run completes with 0 resets and n_eff rises from cold ~1 to ~5-10.

- DESIGN_extrinsic_handoff.md: document the cross-sampler AV->GMM result.  AV converges as a
  source (n_eff~7 at 400k, lnLmax~143); the frozen seed lands cleanly and lifts n_eff, but the
  seeded GMM INTEGRAL is wrong (sqrt(2 lnLmax)=nan, Z~1e-4 vs cold ~1e43) -- the proposal is
  importance-sampling a displaced region.  Two suspects to audit (no more blind GPU): AV-vs-GMM
  _rvs coordinate convention (angle vs cosine for incl/dec), and cov_inflate pushing distance
  out of [1,1000] into NaN likelihood.  Same-sampler GMM->GMM round-trips cleanly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…d weights, ESS n_comp, distmarg)

Debugging the wrong-integral the GPU run showed (the seeded GMM integrated as if lnL~44 vs
the true ~140), found+fixed four real issues; the cross-sampler seed is now numerically
correct (finite lnLmax, valid Z) end-to-end:

1. SAVE side used the sampler's stored log_weights, but mcsamplerGPU/AV stores
   log_weights = tempering_exp*lnL + ln(prior) - ln(s_prior) (adapt-weight-exponent baked in).
   Fitting the GMM to those flattened weights displaces the proposal.  Now build the TRUE
   untempered weight from log_integrand + log_joint_prior - log_joint_s_prior and prefer the
   raw components over 'log_weights'.  (GMM's own _rvs is untempered -> GMM->GMM unaffected.)
   This took the seeded n_eff from ~5 to ~26.
2. cov_inflate default 2.0 -> 1.0: a frozen seed should match the source, not be widened
   (inflation pushes samples past hard bounds -> NaN likelihood).
3. fit_extrinsic_proposal: cap mixture components by the weight ESS (k <= ESS/(d+2)) and DROP
   any non-finite component (renormalize; skip group if none survive).  A starved source
   collapses a component to a singular/NaN covariance that poisons the whole seed.
4. The persistent nan lnLmax was distance sampled against [1,1000]: a seeded distance Gaussian
   spills past the bound -> NaN.  The calmarg path is meant to run with --distance-marginalization
   (the fused kernel IS a distmarg kernel); with distmarg on, the seeded integral is finite and
   valid.  (Gap: pseudo_pipe/extr-run don't add --distance-marginalization yet -- noted in doc.)

Result (distmarg on, CI point SNR~17.5): seeded GMM has 0 resets, finite lnLmax, valid Z, but
n_eff ~1 == cold n_eff ~1.  The handoff is correct+safe but does NOT accelerate here because
GMM does not converge on this peak (cold or seeded) and the AV source (n_eff~5) is too
under-converged to inform a strong seed.  Hard evidence the payoff needs seedable AV (task oshaughn#30)
or a converged source.  Full analysis in DESIGN_extrinsic_handoff.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nstance (XML compat)

copy_lsctables_sim_inspiral iterated lsctables.SimInspiralTable.validcolumns.keys() (the full
schema) and did bare getattr(row, simattr) for string columns (waveform/source/numrel_data/
taper) and numeric columns.  On the current igwn_ligolw + lalsuite stack, ILE-written
sim_inspiral tables contain only the columns actually set, so the schema view and the written
columns drift apart -> reading a saved ILE output_*.xml.gz raised
"AttributeError: 'SimInspiral' object has no attribute 'waveform'"
(and would equally fail on any absent numeric column via the else branch).

Fix: skip columns not present on the row instance (hasattr guard), after the
process_id/simulation_id default-setting branch (which doesn't read the row).  RIFT's own
grids (written via lsctables.New with all columns) are unaffected; column-subset tables now
round-trip.  Verified both a 300-row RIFT grid and a waveform-less ILE-style table read with
no AttributeError.

Per /home/oshaughn/BREADCRUMB_rift_xml_compat.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… for the calmarg pipeline

There was no pipeline-code gap: --internal-marginalize-distance already composes cleanly with
--calmarg-fused-kernel (verified -- args_ile gets --distance-marginalization + a util_InitMargTable
lookup table AND --calibration-fused-kernel), and the fused kernel does NOT require distmarg (it
has both Q_fused_calmarg_cupy and Q_fused_calmarg_distmarg_cupy; the ILE binary wires whichever
applies).  The only gap was that the demo targets didn't expose distmarg.

- demo Makefile: PP_DMARG toggle (default 0, optional) -> --internal-marginalize-distance
  --internal-distance-max PP_DMAX, threaded into extr-build, extr-run-build, pp-run-build.
  (Distinct from the direct-ILE dag-build DMARG knob, which uses a pre-built lookup table.)
  extr-validate checks --distance-marginalization + lookup table when PP_DMARG=1.  Verified
  `make extr-build PP_DMARG=1` passes.
- DESIGN_extrinsic_handoff.md: corrected -- distmarg is OPTIONAL with the fused kernel, not
  required; RECOMMENDED with --extrinsic-handoff (removes distance + its hard bound from the
  seeded GMM proposal, which was the source of the boundary-NaN).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…fairdraw cupy crash)

BREADCRUMB_export_cal_posterior.md: the final export with extrinsics did not carry the
recovered calibration posterior.  Now --calibration-export-posterior (ILE) /
--calmarg-export-posterior (pseudo_pipe): at the fairdraw export, for each fair-draw sample
draw ONE cal realization in proportion to its posterior weight (per-realization likelihood
components from return_cal_components, times the importance weight cal_log_weights) and write
a SELF-CONTAINED sibling <output>_<event>_cal.dat with the FULL draw -- intrinsic + extrinsic
+ the drawn realization's spline nodes as labeled cal_<IFO>_amp_<k>/cal_<IFO>_phase_<k>
columns.  (The fairdraw LIGOLW/.dat schema can't carry arbitrary columns, so per the user the
cal posterior rides a row-aligned sibling .dat with the whole draw, plottable as-is.)
  - node retention: the production prior path now keeps the cal node vectors
    (draw_prior_realizations_with_nodes) when the flag is set; the seed path already returns them.
  - verified on GPU: writes 1 sample x 90 cols incl 60 cal cols (amp_0..9 + phase_0..9 over H1,L1,V1).

Also fix a PRE-EXISTING crash this surfaced: mcsamplerEnsemble (GMM sampler) fairdraw on GPU
did `self.xpy.min([n_extr, 1.5*eff_samp, 1.5*neff])` -- cupy.min has no Python-list overload
("'list' object has no attribute 'min'"), so ANY GMM-sampler fairdraw export on GPU crashed
(independent of calmarg).  Use Python min() of floats.

ILE binary is EXECUTE-POINT (container rebuild to run on OSG/CIT).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…+ thread cal-export into demo

PILOT OSG bug: iteration 0 seeds from cal_consolidated_$(macroiterationprev).npz with
macroiterationprev=-1 -> cal_consolidated_-1.npz, the 0-byte placeholder pseudo_pipe creates
so condor's transfer_input_files of that path does not fail.  Locally the file simply does
not exist (the "missing -> fall back to PRIOR" check fires); on OSG it IS transferred in, so
it EXISTS but is empty, and np.load raised "EOFError: No data left in file", crashing the
first-iteration ILE.  Fix: treat a missing OR EMPTY breadcrumb as "not present yet" (size
guard before any load), for BOTH the calibration and extrinsic seed paths.  EXECUTE-POINT --
rebuild the container.

demo/rift/calmarg: PP_CALPOST toggle (default 1) threads --calmarg-export-posterior into
pp-run-build and extr-run-build, so the recovered cal posterior is written in the runnable
demos.  (Does NOT touch a running rundir_pp_run -- pp-run-build starts with its own rm -rf.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nders; works on old container)

Complement to the ILE empty-breadcrumb size-guard: make the iteration-0 placeholder a VALID
breadcrumb that LOADS cleanly, so the pipeline-writer fix ALONE (no container rebuild) keeps an
older ILE binary from crashing on it.

- generate_realizations.prior_cal_breadcrumb_dict(env_dir, dets, fmin, fmax, n_spline_points):
  build the 'cal' breadcrumb for the broad PRIOR with proposal == prior.  Seeding from it
  draws cal realizations from the prior with ZERO importance weights -- exactly equivalent to
  the cold prior draws.  Layout matches seed_realizations_from_breadcrumb (per-det
  [amp,phase] blocks; dim = 2N*len(dets)).
- util_RIFT_pseudo_pipe.py: on OSG file-transfer, write cal_consolidated_-1.npz as that valid
  prior breadcrumb (was a 0-byte file) and extr_consolidated_-1.npz as a valid EMPTY breadcrumb
  (extrinsic=None -> cold).  Falls back to a 0-byte file only if the build fails (then the ILE
  size-guard catches it).  PIPELINE-WRITER change -- no container rebuild needed.
- util_CalMakePriorBreadcrumb.py (NEW): (re)generate the prior placeholder for an ALREADY-built
  run dir IN PLACE (overwrite the 0-byte cal_consolidated_-1.npz), so an in-flight pilot run can
  be patched without re-running pseudo_pipe or rebuilding the container.

Verified: the placeholder loads + seeds with max|cal_log_weights| ~ 1e-14 (== prior draws).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lmarg flags

The demo grew from the single-ILE correctness check into a ladder up to a runnable condor
pipeline.  Document all targets grouped by what they exercise (A: numerical correctness +
single-ILE; B: direct-ILE DAG + tuning; C: offline pipeline build-validate incl. extrinsic
handoff; D: runnable pipeline on CI data + pilots + extrinsic-handoff GPU run), the runnable
toggles (OSG/PP_PILOT/PP_DMARG/PP_CALPOST/PP_NIT), the helper utils, and the advanced pipeline
flags (--calmarg-export-posterior, --internal-marginalize-distance, --calmarg-pilot,
--extrinsic-handoff).  Add the recovered-cal-posterior section, the iteration-0 prior
placeholder note (+ util_CalMakePriorBreadcrumb.py), and the execute-point vs pipeline-writer
rule.  Points to DESIGN_adaptive_driver.md / DESIGN_extrinsic_handoff.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
oshaughn and others added 28 commits June 14, 2026 03:56
Fit the LISA ecliptic sky as ordinary CIP coordinates (phi/theta) read from
the NAMED hyperpipeline columns ecliptic_longitude/ecliptic_latitude, instead
of the eccentric branch's positional all.net column-shift hack (which bakes in
technical debt).  Adds the reflected (secondary) sky mode as an in-loop DAG
step.  Both validated end-to-end on a CIT cluster smoke run (4-iteration
ILE<->CIP loop, file-transfer container): vary-sky recovers masses+sky to the
truth; the reflect node maps the primary mode to its secondary at iteration 2
and the next ILE consumes it cleanly.

- util_ConstructIntrinsicPosterior_GenericCoordinates.py: ingest/fit/emit sky
  by named column (use_sky), prior_map/prior_range_map phi/theta,
  --phi-range/--theta-range.
- helper_LISA_Events.py: vary-sky grid jitters sky off-lattice (breaks sky<->mass
  collinearity), CIP --parameter phi/theta + ranges, ILE per-row fixed sky.
- convert_primary_sky_mode_to_secondary (new): hyperpipeline-aware sky reflection.
- create_event_parameter_pipeline_BasicIteration / util_RIFT_pseudo_pipe.py:
  reflected-sky DAG node (gated before the target iteration's ILE) + forwarding.
- LISA contract tests updated to the corrected design (named-column CIP params,
  per-point fixed sky, off-lattice sky spread); all 12 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Allow util_RIFT_pseudo_pipe.py --use-ini to build the LISA known-sky surface,
instead of hard-exiting.  The generic [rift-pseudo-pipe] parser already maps
every CLI arg by name (so all lisa-* scalars/algorithm options just work); a
small _lisa_data_products_from_ini() fills the per-channel data products
(channels, PSDs) from the conventional [data]/[lalinference] sections.  No LDG
data-find is invoked; the LISA branch stays self-contained.

The ini path is a thin front-end over the validated CLI machinery: with matched
config it renders a BYTE-IDENTICAL workflow (args_ile/args_cip_list/args_test
and DAG, incl. the reflected-sky node).  Verified against the CLI render that was
itself validated end-to-end on a CIT cluster run.

- bin/util_RIFT_pseudo_pipe.py: replace the --use-ini LISA hard-exit with
  _lisa_data_products_from_ini(); flat [rift-pseudo-pipe] lisa-* keys still win.
- test/test_lisa_ini_contract.py: render ini vs CLI, assert byte-identical args
  + matching DAG/reflect node (locks the equivalence).
- demo/rift/lisa/BBH_lisa_demo.ini: toy template (IMRPhenomD, vary+reflected sky).
- demo/rift/lisa/run_lisa_ini_demo.sh: build inputs + instantiate + render.
- demo/rift/lisa/README.md: document the production-ini path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… curvature

Adaptive re-whitening alone still left SNR640 collapsed: even seeded from the
tempered best sample, gradient-ascent MAP-polish stalled ~1615 nats below the peak
(on a 1/SNR-narrow peak the gradient is ~SNR^2-huge, so backtracking shrinks the
step to a crawl) -> the Fisher there was far too broad (theta-widths ~0.25 vs the
true ~1/640) -> still collapses. Add Newton refinement to _map_polish_4: after the
gradient phase, take curvature-normalized steps dx = F^{-1} g (F=-Hessian,
eig-floored pos-def) with backtracking line search; this lands on the sharp peak in
~1 step, so the subsequent Fisher whitening uses the TRUE peak curvature. n_newton
param (default 30). py_compile OK; testing SNR640 end-to-end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…her-is-samples)

The flow collapses at extreme SNR from NF-training nan, not because the posterior is
unsampleable. Add a flow-free fallback: after annealing, draw N sky samples from the
Fisher-whitened Gaussian about the (Newton-polished) MAP and importance-reweight by
the true lnL (reuses _fisher_whitening + _gaussian_logq + evidence_from_logweights).
Overrides exported samples; TI logZ stays primary (IS logZ reported as cross-check).
CLI --fisher-is-samples (implies --fisher-precondition). py_compile OK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…l) for high SNR

Robust alternative to the flow that does not collapse on sharp peaks: a cloud of
walkers climbs an adaptive temperature ladder; each rung tempered-resamples then
applies K random-walk Metropolis 'puffball' moves whose proposal covariance is
estimated FROM THE CLOUD (shrinks to match the posterior; never the Hessian, so
slivers/non-Gaussianity can't fool it; re-broadens every rung -> cannot collapse).
SMC analogue of RIFT-AV sample->puffball->sample / nested sampling. SMC evidence.
CLI --smc-puffball/--smc-walkers/--smc-move-steps/--smc-puff-scale.
…n constant)

Now that SMC maps the posterior + finds the peak well, fit a moment-matched
('Fisher-like') Gaussian to the CONVERGED CLOUD (inflated for fat tails) and do one
importance-sampling pass -> a well-conditioned normalization constant
logZ = logmeanexp(lnL + log_prior - logq). Unlike the earlier Hessian/MAP Fisher-IS
(off-peak curvature -> ESS=1), this proposal matches the posterior the cloud already
mapped, so ESS is high. Forward-only (no AD). Reported as primary logZ; raw SMC logZ
kept in logZ_laplace. Params is_evidence/is_samples/is_inflate.
…en proposal fits)

SNR640: cloud-IS gives ESS=18464/60000, logZ=204039.18 +/-0.006, agreeing with the raw
SMC logZ to 0.1 nat and superseding the collapsed-flow per-sample TI (201962, biased
~2077 low). SNR40 (broad multimodal sky): single-Gaussian ESS=31 -> too crude, keep SMC
logZ. Gate the IS-Z override on ESS>=max(500,2%N) so the high-SNR accurate evidence is
used and the low-SNR poor fit is not.
…h drop)

Clearing sampler._rvs by setting each key to [] (rather than resetting the
dict) left a stale empty-list entry for any key added AFTER the fairdraw
subset -- notably 'integrand', set in mcsamplerAdaptiveVolume.integrate()
under --internal-use-lnL. In a batched run (n-events-to-analyze>1) the next
binary's integrate_log() fairdraw then did self._rvs['integrand'][indx_list]
and raised "list indices must be integers or slices, not ndarray", caught by
the per-binary handler and skipped. This silently dropped every binary after
the first in each ILE batch whenever fairdraw extrinsic output was enabled
(e.g. distance-slice export runs), decimating the grid ~99% (succ=1/job).

Resetting sampler._rvs = {} drops stale keys; integrate_log repopulates the
needed columns fresh each binary. Verified on a real GPU (cupy, AV sampler,
gwsignal SEOBNRv5PHM, l-max 4, --internal-use-lnL + --fairdraw-extrinsic-output
+ --resample-time-marginalization + --export-distance-slices): 3/3 batched
binaries now export, was 1/3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
all-fresh slice centers come from quantile_slice_centers(), which uses fixed
equi-probable quantiles (k+0.5)/K -- identical for every intrinsic, so K=1
pins every point to the median d and the union over the grid samples only the
median locus. Add randomize=True: draw the K quantiles uniformly-in-CDF per
call, so a single all-fresh slice per intrinsic becomes a fair-draw of d from
THAT intrinsic's posterior. Over the intrinsic grid this samples (intrinsic,d)
jointly -- cheap (~1 slice/intrinsic) dense coverage for a continuous AD
surrogate -- while the dense fairdraw extrinsic posterior provides the
reference. Verified numerically: K=1 randomized over 5000 draws reproduces the
input d-posterior quantiles; deterministic K=1 stays on the median.

Wired end-to-end: ILE --distance-slice-randomize -> pseudo_pipe
--export-distance-slices-randomize -> cepp
--last-iteration-export-distance-slices-randomize. Optional rng for repro.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…r_fairdraw_batch

Rift o4d fix rvs clear fairdraw batch
The container-family manifest emits an expression-valued
MY.SingularityImage = ifThenElse(TARGET.GPUs_Capability ...). HTCondor's
native singularity (CIT-local startds) evaluates this against the slot ad,
but OSG/OSPool glidein pilots read SingularityImage as a LITERAL string and
hard-hold the job ("Unable to download or build singularity image
ifThenElse(...)"). Match-time $$()/ifThenElse also cannot see the real GPU on
opportunistic glideins.

Add an opt-in alternative that selects the container at JOB START on the real
device:

- container_manifest.build_runtime_selection_wrapper(manifest, inner_command):
  generates a bash wrapper (cap->image table baked from the same manifest) to
  be used as the Condor executable on the bare node (NO +SingularityImage). It
  detects nvidia-smi compute_cap, picks the matching family image, acquires
  ONLY that image (in-place CVMFS/local verbatim; osdf:// single stashcp/pelican
  fetch; never the whole family), and execs the real in-container command via
  nested apptainer.

- write_ILE_sub_simple: when RIFT_CONTAINER_RUNTIME_SELECT is set (and a family
  manifest + use_singularity), emit the wrapper as the executable instead of
  MY.SingularityImage / the $$() transfer token. The require_gpus capability
  floor is still applied. Default (env unset) behavior is byte-identical: the
  existing ifThenElse path is unchanged for CIT-local runs.

Tests: wrapper text/syntax, live capability->image selection (incl. fallback),
and integration asserting no MY.SingularityImage / no $$() token / floor
present / wrapper emitted with the right inner command. Existing CIT-local and
single-sif tests unchanged.

This is an investigation branch for review; CIP/PSD/calibration submit writers
still use the ifThenElse path (fine on CIT-local; would need the same wrapper
for a fully OSG run). CVMFS publication is intentionally not required (no direct
publication authority); the wrapper's default OSG acquisition is a single OSDF
fetch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…bustness)

The DAG sizes per-iteration MARG chunks from n-samples-per-job, but the grid an
adaptive iteration actually places (e.g. a puffball) can be SMALLER, so the tail
chunks ran off the end and the marg driver raised SystemExit ("index range
[a,b) exceeds grid size N"). Clamp the range to the grid instead; a fully
out-of-range chunk yields an empty (header-only) output the consolidation
ignores. Surfaced running an adaptive Rapster (popsynth_hyperpipe) loop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…0 = inf)

fit_rf used sample_weight=1/y_errors**2; a zero error (placeholder rows can leak
into the accumulated marg net with sigma=0) makes the weight infinite, which
sklearn rejects ("Input sample_weight contains infinity"). Floor sigma at 1e-3.
Surfaced in an adaptive Rapster (popsynth_hyperpipe) EOS-posterior fit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…bustness)

The DAG sizes per-iteration MARG chunks from n-samples-per-job, but the grid an
adaptive iteration actually places (e.g. a puffball) can be SMALLER, so the tail
chunks ran off the end and the marg driver raised SystemExit ("index range
[a,b) exceeds grid size N"). Clamp the range to the grid instead; a fully
out-of-range chunk yields an empty (header-only) output the consolidation
ignores. Surfaced running an adaptive Rapster (popsynth_hyperpipe) loop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…0 = inf)

fit_rf used sample_weight=1/y_errors**2; a zero error (placeholder rows can leak
into the accumulated marg net with sigma=0) makes the weight infinite, which
sklearn rejects ("Input sample_weight contains infinity"). Floor sigma at 1e-3.
Surfaced in an adaptive Rapster (popsynth_hyperpipe) EOS-posterior fit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rescue

When a dev run's rescue DAG gets zeroed (a resubmit bypassed it, a cluster hiccup,
an accidental fresh start), DAGMan restarts from scratch and redoes work whose outputs
are already on disk.  The dagman.out is appended across every invocation and still logs
each finished node ("Node <name> job proc (...) completed successfully"), so this
reconstructs a partial rescue (v2.1.0) marking those DONE -- recursively for nested
SUBDAG EXTERNAL inner DAGs, and unioning in existing DONE marks to preserve manual fixes.
Validated recovering 519/2638 outer + 430/1948 inner nodes after a fresh-restart wiped
the rescue.  Dev recovery tool; does not submit.  Generalizes the by-hand mark-DONE trick.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… from a run's posterior

A cal pilot normally starts COLD (iteration-0 seed = prior placeholder, wide_0 draws cal
realizations blindly).  Any run with --calibration-export-posterior writes, at its final
fairdraw, <out>_<event>_cal.dat: samples of the recovered cal posterior over the same
spline-node vector the pilot breadcrumb is a Gaussian over.  This fits those samples into
proposal_mean/proposal_cov and writes them as cal_consolidated_-1.npz (reusing the
placeholder's prior + node structure), so the pilot starts WARM -- seeded where the
likelihood already constrained calibration.  Reusable to harvest cal posteriors from ANY
prior run (esp. high-SNR / well-constrained events, where prior-draw cal n_eff collapses).
Validated end-to-end on synthetic samples (fitted proposal ~13x tighter than prior).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t from production PE

Like asimov bootstraps an intrinsic grid, a finished PE analysis already constrains an
event's CALIBRATION -- so a cal pilot can seed from it instead of starting cold from the
broad prior.  Production pesummary/bilby posteriors carry the recovered cal posterior as
recalib_<IFO>_amplitude_<k> (fractional dA/A) and recalib_<IFO>_phase_<k> (rad) -- the same
spline-node vector a RIFT cal breadcrumb is a Gaussian over.  This fits those samples into
proposal_mean/proposal_cov and writes the pilot's iteration-0 seed cal_consolidated_-1.npz
(reusing a placeholder's prior + node structure).  Reads via pesummary.io.read (h5py fallback).

Validated against REAL data (S240426s bilby SEOBNRv5PHM, 14392 samples, H1/L1 x 10 nodes):
fitted proposal ~2.7x tighter than prior, centered on the recovered cal offset.

COMPATIBILITY (enforced/warned): same detectors + spline count required (clear error on
mismatch); spline node FREQUENCIES must match too (documented caveat -- seeds the same /
cal-identical event's pilot, not a cross-event transfer).  Complements
calposterior_to_breadcrumb.py (which reads a RIFT in-loop _cal.dat).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Read-only, engine-agnostic query/pool layer over the Archive so inference
reuses completed sims instead of regenerating:
- iter_completed, param_distance (normalized, dict/scalar, ignores seed)
- find_matching (same_q -> unbiased same-Lambda pooling / seed-averaging)
- find_nearby (k-NN / tolerance, distance-annotated, approximate)
- pool_catalogs (concat shape across sims+levels; mu reported per-level,
  never summed) + gather_samples reuse entry point
Additive only (database.py untouched). 6 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…worker

Keep no-worker CIP grids in workflow root
…ikelihood

Update JAX ILE likelihood work in calmarg
The cal_env/H1.txt rule was a bare file-existence target, so it silently reused a
stale cal_env after make_cal_envelopes.py's default envelope dropped from 5%/3deg to
a realistic 1%/1deg. The stale 5-8% 'wide' envelope collapses cal n_eff to ~3 at
SNR~17 by design, making the pp-run demo look permanently unconverged (sigma_lnL~0.6)
when the code/estimator were correct. Add the generator as a prerequisite (force regen
on default change) and echo the in-use amp/phase 1-sigma at build so a wide envelope
is loud instead of silent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A single integrate_likelihood_extrinsic_batchmode invocation evaluates the
contiguous intrinsic-grid range [--event, --event+--n-events-to-analyze)
serially on ONE GPU. On clusters where whole multi-GPU nodes are reserved but
ILE only requests request_GPUs=1, the remaining GPUs sit idle (e.g. macrongroup
~100 points per job on a 4-GPU node uses 1/4 of the hardware).

ile_pre.sh now wraps the ILE executable in a small launcher that, when opted in,
splits that point range into N disjoint shards run concurrently -- one per GPU.
Each shard is pinned with CUDA_VISIBLE_DEVICES and given a distinct
--output-file prefix (<orig>.gpu<dev>), so the per-point output files
(<prefix>_<localidx>_.dat / .xml.gz) never collide. Downstream collection is
unaffected: util_ILEdagPostprocess.sh globs CME*.dat and util_CleanILE.py
de-duplicates by parameter value, not filename. The shards partition the range
exactly (sizes differ by <=1), so coverage is identical to the serial run; the
launcher's exit code is the first non-zero shard code, preserving condor
retry/hold behaviour (e.g. CUDA hard-fail 62).

Controlled by env var RIFT_ILE_GPU_FANOUT (propagated to jobs via getenv=*RIFT*):
  unset / "0" / "1"  -> no fan-out; the launcher exec()s the binary unchanged,
                        so default behaviour is byte-for-byte identical.
  "auto"             -> one shard per visible GPU (CUDA_VISIBLE_DEVICES, else
                        nvidia-smi); for a whole node held with request_GPUs=1.
  <int N>            -> up to N shards (capped by #GPUs and #points); the DAG
                        also requests request_GPUs=N and request_CPUs=N so
                        HTCondor assigns the devices.

Changes are mirrored in dag_utils.py and dag_utils_generic.py (each carries its
own copy of write_ILE_sub_simple). request_CPUs is threaded through the
singularity branch so the fan-out CPU count is not clobbered back to 1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make the fan-out usable through every front-end, including asimov (which builds
the DAG in a clean environment and cannot rely on RIFT_ILE_GPU_FANOUT being
exported in the submit shell):

- Bake the resolved fan-out value into the generated ile_pre.sh as
  export RIFT_ILE_GPU_FANOUT="${RIFT_ILE_GPU_FANOUT:-N}", so the job needs NO
  runtime environment (a runtime value still overrides).  ile_invocation_shell()
  now takes the value; ile_gpu_fanout_value() resolves it at build time.  Mirrored
  in dag_utils.py and dag_utils_generic.py.

- Add --ile-gpu-fanout to util_RIFT_pseudo_pipe.py and
  create_event_parameter_pipeline_BasicIteration; both funnel it through
  RIFT_ILE_GPU_FANOUT (pseudo_pipe runs BasicIteration via os.system, inheriting
  the env), so request_GPUs/CPUs sizing and ile_pre.sh baking happen on one path.

Asimov needs no code change: a blueprint sets the value via
  scheduler.environment variables: {RIFT_ILE_GPU_FANOUT: N}   (rift.py copies it
  into os.environ before running the pipeline) or
  scheduler.pipeline: {ile-gpu-fanout: N}                     (-> CLI flag).

Demo: demo/rift/infra/multi_gpu/ (README, Makefile, CI ini, asimov blueprint +
frozen container-family pin, fake_ile stub).
  - make smoke-local: builds a REAL ile_pre.sh from the shipped helper around a
    stub ILE and runs it across this node's GPUs; asserts exact coverage, GPU
    spread, distinct per-shard output prefixes.  Runs anywhere (no cupy/condor).
  - make build / make verify: builds a real pipeline run dir on the CI synthetic
    data (singularity/OSG so ile_pre.sh is emitted) with --ile-gpu-fanout and
    asserts ILE.sub gets request_GPUs=N/request_CPUs=N and ile_pre.sh bakes N.
  Verified end-to-end: ILE.sub -> request_GPUs=4/request_CPUs=4, ile_pre.sh ->
  RIFT_ILE_GPU_FANOUT:-4 wrapping the container ILE binary; default stays 1 (no-op).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the two ways to request a VARIABLE number of GPUs (HTCondor's plain
request_GPUs is a single fixed count, so it cannot natively ask for "1 to N"):

- RIFT_ILE_GPU_FANOUT=auto-max-N  (shared / partitionable-slot pools)
    request_GPUs/request_CPUs become a ClassAd expression that asks for up to N of
    the capability-matching GPUs available on the matched slot:
      ifThenElse(countMatches(RequireGPUs,AvailableGPUs) >= N, N,
                 ifThenElse(... >= 1, ..., 1))
    (same countMatches idiom RIFT already uses for cross-platform GPU matching).
    ile_pre.sh bakes 'auto', so the launcher splits across exactly the 1..N GPUs
    condor grants.  Override the expression with RIFT_ILE_GPU_REQUEST_EXPR if your
    pool exposes GPU counts under a different attribute.

- RIFT_ILE_GPU_FANOUT=all  (dedicated / whole nodes you reserve)
    keep request_GPUs=1 (matches a node with ANY GPU count) and have the launcher
    enumerate EVERY physical GPU via nvidia-smi, ignoring CUDA_VISIBLE_DEVICES.
    Launcher _devices(physical=True) drives this.

The runtime split was already adaptive (the launcher splits the point block across
however many GPUs it is handed); these add the matching request side.  Implementation:
ile_gpu_fanout_count() -> ile_gpu_request() returning (request_gpus, request_cpus) as
an int OR a ClassAd expression; ile_gpu_fanout_value() maps auto-max-N -> baked 'auto'.
Mirrored in dag_utils.py and dag_utils_generic.py.

Verified: launcher splits across 1/2/3/4 granted GPUs (full coverage each); 'all'
uses all 4 physical even with CUDA_VISIBLE_DEVICES=0; generated ILE.sub carries the
adaptive expression for auto-max-4, request 1 for 'all', fixed N for N.

Demo: new `make requests` shows request_GPUs/CPUs + baked launcher for each mode;
README "Values -- fixed vs. adaptive" documents the hot-swap options, the
partitionable-slot requirement, and the cgroup/reservation caveats; blueprint shows
the adaptive variants.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…back

Per deployment policy (whole nodes are reserved; partitionable GPU slots are not a
sustainable long-term path), make the GPU multi-GPU policy default to 'all':

- DEFAULT_ILE_GPU_FANOUT = 'all'.  When RIFT_ILE_GPU_FANOUT is unset, a GPU ILE job
  keeps request_GPUs=1 (matching unchanged -- lands on any GPU node exactly as before)
  but the launcher enumerates EVERY physical GPU (nvidia-smi, ignoring CUDA_VISIBLE_DEVICES)
  and splits the ILE block across all of them.  On a 1-GPU node this is a no-op; only
  multi-GPU nodes change.

- Fallback to the old single-GPU run: RIFT_ILE_GPU_FANOUT=1 (or 'single'/'off', or
  --ile-gpu-fanout 1).  Aliases handled in the resolver and the launcher.

- Safety: the bake is gated on request_gpu, so a CPU-only ILE job always bakes '1' and
  never grabs the node's GPUs under the 'all' default.

Implementation: shared _raw_ile_gpu_fanout() applies the default + single/off aliases;
ile_gpu_fanout_value()/ile_gpu_request() build on it; write_ILE_sub_simple passes
fanout=(ile_gpu_fanout_value() if request_gpu else '1').  Mirrored in dag_utils.py and
dag_utils_generic.py.

Note: HTCondor partitionable GPU slots DO work today (verified on the CIT pool: a 2-GPU
partitionable slot carves per-GPU dynamic slots), so auto-max-N remains available, but it
is no longer the recommended/default path.

Demo updated: README "Default policy" + Values table (all=default, 1/single=fallback);
`make requests` shows default vs fallback; blueprint defaults to no override.

Verified: default bakes 'all' and runs across all 4 physical GPUs even with CVD=0;
RIFT_ILE_GPU_FANOUT=1 runs single; CPU-only job bakes '1'; fixed N and auto-max-N unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@oshaughn

Copy link
Copy Markdown
Owner Author

Considerable dev testing at this point, so likely not breaking critical-path outcomes. This is into the main dev branch, so perfect safety is not guaranteed

@oshaughn oshaughn merged commit 00113fa into oshaughn:rift_O4d Jun 30, 2026
17 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.

3 participants