Changelog
Versions are derived from git tags by setuptools-scm.
Unreleased
Fixed
-
IMRPhenomT’s coefficient solve evaluated the merger-amplitude boundary at the wrong frequency.
_solve_22_amplitude_coefficientsused the eta-dependenttcut22boundary via the inspiral-only omega ansatz, instead of the fixedt=-150Mboundary via the full piecewiseomega22dispatch that LALSuite’s own amplitude solve actually uses. Only matters for eta far from the tested grid, wheretcut22can fall outside the inspiral region entirely; zero regression on the existing test suite. -
IMRPhenomTwas a placeholder._compute_phenom_coefficientsreturned a fixed dict, ignoring itseta/chi1/chi2arguments entirely — silently wrong for every call, not just outside some range (seedocs/constants.md). Replaced with a real port of LALSuite’s algorithm (Estelles et al. 2020, arXiv:2004.08302): 3.5PN TaylorT3 inspiral plus NR-fitted higher-order corrections solved via collocation, anarcsinh-parametrized merger frequency ansatz, Damour & Nagar 2014’s ringdown ansatz, and phase as the exact analytic integral of each region’s frequency ansatz (no numerical integration). Two LAL convention bugs were found and fixed along the way: the reference phase must be anchored atf_ref(nott=0/merger), andphi_refenters through the spin-weighted Ylm’s azimuthal argument, not a separate per-mode rotation. Verified against real LALSuite: mismatch 1.4e-6–4.6e-6 across a parameter grid (mass ratio 1:1–5:1, spins to 0.8, varying inclination/phi_ref) — the same precision level asIMRPhenomD. -
IMRPhenomTHM’s higher modes were a placeholder envelope, not a waveform model — inventedt_meco/t_ringconstants and ad hoc amplitude/frequency shapes (flagged indocs/constants.md, verified mismatch ~0.8 against LALSuite, i.e. uncorrelated). Replaced with a faithful reimplementation of the 4 higher modes(2,1),(3,3),(4,4), (5,5), generalizingIMRPhenomT’s(2,2)coefficient-solving machinery to arbitrary(l,m): mode-specific PN amplitude coefficients, the same merger/ringdown frequency+phase ansatze with per-mode QNM substitution, and the fixed per-mode phase offsets. Verified against LALSuite’s ownIMRPhenomTHM: polarization-level mismatch 1.4e-10–2.5e-6 across a parameter grid, a direct per-mode comparison againstSimIMRPhenomTHM_Modes, and an explicit check that odd-mmodes vanish exactly at the equal-mass/equal-spin symmetry point.--waveform phenomthm(guarded with a clear error since it was added to the CLI) is now enabled inrun-pe, the same pattern as--waveform phenomt. -
PhaseDistanceMarginalLikelihood.dist_boundsdefaulted to(1000, 8000)Mpc regardless of a run’s actual configured distance prior. For a campaign’s injections as close as 200 Mpc, the distance-marginalization quadrature grid silently never covered the true distance, producing a wrong (not merely noisier) marginal likelihood over the intrinsic parameters — caught by luck, not by design. The identical pattern was independently reproduced inModesNetworkLikelihood.log_marginal_likelihood[_full]’sdist_min/dist_max(100, 5000). Both are now required keyword-only arguments with no library-side default, so an omitted bound fails immediately withTypeErrorinstead of silently answering wrong;MarginalizedIntrinsicLikelihood.__init__additionally validates its settings dict eagerly. The one place a default may still exist —(100, 8000)Mpc, for a degenerate resolved prior box — now lives injaxpe/cli.pyas a named, documented constant behind_dist_bounds_for_marginalization().docs/constants.mdis the full audit this fix came out of, covering every hardcoded numeric literal in the library functioning as a physical or algorithmic boundary. -
Every injection in a campaign received the same noise realisation. The CLI passed
seeds.noisethrough verbatim, sogenerate-injections --noise gaussian --n-injections Nfollowed byrun-peanalysed N copies of one realisation. This silently invalidates a PP-plot campaign, which is the main thing such a campaign exists to run.generate-injectionsnow resolves a per-injection seed withderive_noise_seed(seeds.noise, index)and records it asmetadata.noise_seed, so the realisation is pinned by the artifact;run-pereads it back, falling back to deriving frommetadata.indexfor sets already on disk. -
A detector’s noise depended on its position in
detector_names.make_injectionadvanced a single numpyGeneratorthrough the detector loop, so("H1","L1")and("L1","H1")gave H1 different data at the same seed — measured at a 130% relative change. Streams are now keyed by the detector’s name.
Added
-
HTCondor DAG generator and job scripts for the PE campaign (
bin/condor/generate_campaign_dag.py,run_pe_job.sh,postprocess_job.sh). Emits three per-variant submit-file templates (phenomd_hmc,phenomd_gpry,esigma_gpry), asmoke.dagcovering injection 0 of each variant, and one DAG per track —campaign_<variant>.dag— rather than a single bundledcampaign.dag, since bundling let the slowest track (phenomd_hmc, the only variant without GPry’s own convergence-based early stopping) block submitting the other two at all. Each track DAG carries its ownPOSTPROCESS_<variant>node (postprocess_campaign.py --variants <variant>) as a child of that track’s PE jobs, so corner plots and the PP plot are produced automatically as soon as the track finishes.--maxjobs/--track-maxjobsdefault to0(noDAGMAN_MAX_JOBS_SUBMITTEDcap) — the condor negotiator already does fair-share scheduling, so a DAGMan-side cap just leaves nodes idle without protecting anything.request_cpus=8andrun_pe_job.sh’s matchingtaskset -c 0-Nwrapper are sized from measurements on the real pool (~25–26 static 32-core/63.7 GB nodes; JAX/XLA sizes its CPU thread pool fromsched_getaffinity, notOMP_NUM_THREADS/XLA_FLAGS, sotasksetis what actually constrains it). -
bin/postprocess_campaign.py, a general-purpose post-processing report for PE run suites laid out as<results_dir>/<variant>/<run_id>/. Auto-discovers every variant and completed run, then builds one self-contained,file://-viewable report: per-run corner plots with WebP thumbnails for a gallery grid, a per-variant Beta-order-statistic PP plot with per-parameter KS-test p-values against Uniform(0,1), and a staticindex.html. Missing/failed runs are silently skipped, so the report stays honest about partial completion of a large campaign. -
ESIGMA and the marginalized-intrinsic likelihood, wired into
run-pe, enablingPhenomD+HMC/PhenomD+GPry/ESIGMA+GPryside by side from the same CLI:--waveform {auto,phenomd,phenomt,phenomthm,esigma}, decoupled from--domain(ESIGMA is always internally TD → FFT’d to FD);--likelihood marginalized_intrinsic, the 4-D GPry-facing likelihood via extrinsic importance sampling, for any waveform exposingmode_dict();generate-injections --target-snr-range/--target-snr-waveformto redraw onlyluminosity_distance(viadistance_for_target_snr) to hit a target network SNR, holding every other parameter’s draw bit-for-bit identical to an unflagged run under the same seed;run-peGPry overrides (--gpry-ref-bounds-rel/-abs,--gpry-noise-level,--gpry-svm-threshold,--gpry-trust-region-threshold) for populations spanning a wide SNR range, following the GPry LISA paper’s own high-SNR SMBHB recipe, since GPry’s defaults (tuned for cosmological likelihoods) underfit the sharper log-likelihood curvature of a high-SNR CBC signal; and a persistent JAX compilation cache ($HOME/.jaxpe, NFS-safe) so a campaign’s many single-process CLI invocations reuse compiled XLA across runs sharing static shapes. Backed by a newesigma.*run-configuration section mapping ontoESIGMAInspiral’s constructor kwargs, theesigmaconda/pip extra (diffrax,numba,numbalsoda, an editableesigmapy-devinstall), andexamples/configs/campaign_10to80_aligned.json, the backing config for a chirp-mass-[10,80] M☉aligned-spin method-comparison campaign. -
jaxpe.gw.match: a reusable, PSD-weighted match/mismatch between two frequency-domain waveforms, generalizing a test-local helper that required a jaxpe likelihood object into a standalone function taking arrays + PSD +dfdirectly. Pure JAX throughout, so it composes underjax.jit/jax.vmap. This is the shared comparison primitive theIMRPhenomD/IMRPhenomT/IMRPhenomTHMLALSuite-validation tests are built on;IMRPhenomDin particular had never been checked numerically against LAL before (only jaxpe-internal batched-vs-serial self-consistency) —tests/test_phenomd.pynow does, over a grid spanning mass ratio, spin and inclination: worst-case mismatch ~4e-9. -
make_injectionsbuilds a whole suite of injections in onevmap, keeping signal, projection and noise on the accelerator. Measured on 4 s @ 1024 Hz H1+L1: the vectorised arithmetic alone is ~4x on CPU and ~6x on GPU atn=256, and end-to-end against a loop overmake_injectionit is 28x atn=64on GPU (173.5 s → 6.2 s) — though most of that larger margin is amortisingmake_injection’s per-calljax.jitcompilation rather than parallelism.Deliberately scoped: this is for suites that are the product (training sets, banks, systematics studies), not a speedup for validation campaigns, where injection creation is a fraction of a percent of wall time. A batch needs one analysis grid, so it cannot mix trigger times or durations, and oversized requests are refused with an estimate — a 2048 s BNS segment is 67 MB per injection per detector.
Compare batched against serial by mismatch (~1e-9), not elementwise amplitude: raw amplitudes differ at ~5e-5 because XLA fuses
IMRPhenomDdifferently between jit graphs. That predates this work and is unrelated to batching. -
jaxpe.drivers.relative_binning_pe, the shared core of the twobin/relative-binning benchmarks.bin/run_bns_ce_pe.py(FD, IMRPhenomD) andbin/run_td_phenomt_pe.py(TD, IMRPhenomT) had duplicated 670 lines: kernel construction, the MAP+Laplace mode finder, and the whole warmup/equilibration/production schedule. The duplication was not approximate. Compiled and fingerprinted,run_pe,_make_kernel,eta_to_q,_decimateandrank_normalizedhad byte-identical bytecode in both files; the entire functional difference between the two copies was one leftoverprint("H HAS NANS:")debug block inmap_laplace, now deleted. Two comments naming a driver-specific benchmark ledger were retargeted, since a module both drivers import cannot name one of them.Each driver keeps what genuinely differs —
build_loglike,validate_rb,main— and importseta_to_q,map_laplaceandrun_pe, the only three names either needs. 1406 lines leavebin/; 756 arrive in the library.It is a library module rather than a
bin/script so that it is importable and testable:bin/has zero test coverage, which left ~570 lines of convergence logic unreachable by the suite.tests/test_drivers.pyadds the first coverage this code has had.The implicit interface is now documented rather than merely present:
run_pereads 27 attributes off anargparse.Namespace, listed in the module docstring and indocs/api/drivers.md. Both drivers define all 27.Verified by characterisation baseline, since there was no coverage to rely on: both drivers were run twice before the move and shown bitwise reproducible, then again after, with
samples.npzcompared key by key. Every array is bitwise identical across the move. -
network_snranddistance_for_target_snr, replacing the build-measure-rescale recipe open-coded at three sites.h ∝ 1/Dexactly, so the target is hit in one measurement with no search. -
analysis_gridandresolve_f_max, one definition of the segment grid and of the 90%-of-Nyquist convention, replacing five copies.
Changed
-
Noise generation moved to JAX (
simulate_noise_fd_jax), unifying the RNG with parameter drawing, which already used PRNGKeys.simulate_noise_fd(numpy) remains exported and unchanged.This changes which noise realisation a given seed produces. Nothing in the test suite pins noise sample values, but the cached artifacts under
examples/output/were computed against the old realisation and have been regenerated.One reproducibility property is worth knowing: JAX’s underlying random stream is bitwise identical across CPU and GPU, but
jax.random.normalis not — measured at up to 3e-15 between backends, because XLA compileserf_invdifferently for each. Realisations are therefore bitwise reproducible on a given platform and equal to ~1e-15 across platforms. Usesimulate_noise_fdwhere bitwise cross-platform equality is required. -
examples/09_validate_injection_vs_dynesty.pyfingerprints the injection data into its bilby label. bilby resumes from a checkpoint keyed only bylabel, so a checkpoint computed against different data would previously have been resumed silently under the new likelihood. -
bin/run_bns_ce_pe.py,bin/run_td_phenomt_pe.pyandbin/profile_sampler_scaling.pyusejaxpe.gw.lalsim_psd("CE", ...)instead of acosmic_explorer_psdhelper duplicated in the first two and loaded by file path in the third. Verified bitwise identical at 8 s @ 2048 Hz and 2048 s @ 4096 Hz. -
A driver run that hits
--max-minutesis not reproducible block-for-block. Not a new behaviour, but newly written down (docs/api/drivers.md): the production loop breaks on elapsed wall clock, so the block count depends on how fast the machine was. For a deterministic run, put--max-minutesbeyond reach and bound it with--max-production-blocks. This is why the characterisation baselines guarding the extraction above were captured that way. -
bin/run_td_phenomt_pe.py:--target-snrno longer changes the analysis window. Its rescale rebuild passedtukey_alpha=0.0while the first build used the 0.1 default, so requesting a target SNR silently altered the Tukey taper as well as the distance. Both builds now share one hoisted keyword dict.
Documentation
-
docs/gpry_gw_pe_literature.md— a survey of GP-surrogate active-learning samplers in GW PE, written while diagnosing aphenomd_gpryGPAcquisitionErroron the 300-run campaign. Answers whether GPry itself has prior GW-PE use (one published application: LISA, not LVK/CE/ET) and whether the high-SNR GP-robustness problem hit here has prior art — RIFT’s publication record documents the same failure shape from a different entry point, with fixes that turned out to be transferable. -
docs/constants.mdgained a full audit of hardcoded numeric literals functioning as physical or algorithmic boundaries, tolerances, or resolutions (the distance-bounds fix above is one entry in it), and itsIMRPhenomT/IMRPhenomTHMentries were updated from “placeholder” through “IMRPhenomT fixed, IMRPhenomTHM still open” to fully resolved as each reimplementation landed and was LAL-validated.
Performance
- Post-processing no longer rebuilds an injection to recover the prior.
PostProcessoronly usesproblem.prior; obtaining it cost a waveform generation, projection, FFT and jit compile per samples file — measured at 1.70 s and 1.83 s.InferenceProblem.log_likelihoodnow defaults to a stub that raises, so a prior-only problem is expressible without lying about the density.
0.1.0
First tagged release. Cut to mark a significant performance fix in the sampling engine.
Fixed
-
run_chainsinitialised its chains outside the jit (jaxpe/kernels/base.py).jax.vmap(kernel.init)ran eagerly, dispatching the target’s entire gradient graph one operation at a time. For a gravitational-wave likelihood (~3600 instructions) that cost ~2.2 s per call against ~0.004 s jitted — roughly 550× — and it was a fixed cost, paid on every call regardless ofn_steps.Initialisation now happens inside
_run_chains_jit, which already tooklogp_fnas a static argument, so there is no additional jit cache and no extra recompilation. Every kernel benefits; the effect is largest for expensive targets and for workflows that make many shortrun_chainscalls.Measured on the BNS/Cosmic-Explorer benchmark (
bin/run_bns_ce_pe.py), which makes ~33 such calls per run: 6.05 → 3.11 min mean wall clock, and 15.42 → 3.11 min against the original reference. The fixed per-call cost measures 0.028 s after the fix, down from 2.254 s.Guarded by two regression tests in
tests/test_kernels.py. The primary one asserts the structural cause — that the target is never invoked with concrete, untraced values duringrun_chains— rather than a wall-clock threshold, so it stays meaningful across hardware.
Added
bin/profile_sampler_scaling.py— fitsT = fixed + marginal × workforrun_chainsand_global_blockacross several sizes, plus gradient cost versus chain count and global-block cost versus flow capacity. This is the tool that found the bug above; point measurements had missed it. Recommended as the first thing to run on new hardware.
Changed
- Benchmark defaults in
bin/run_bns_ce_pe.py, each with its measurement recorded at the argument:--equil-rounds5 → 3,--production-steps25 → 12,--flow-layers8 → 4 (new flag),--max-production-blocks40 → 80. The first two had previously measured as losses because of the fixed cost above; both win once it is removed. --max-production-blocksraised because 40 turned “needs two more blocks” into a reported non-convergence on a 1.35 + 1.25 M☉ source.--max-minutesis the real budget guard.
Documentation
docs/bns_ce_pe_benchmark.mdrewritten and pruned (848 → 338 lines), reorganised around the result rather than the chronology of getting there.