asimov-simplepe

Allowing asimov to interact with simple-pe

Community

asimov-simplepe

An Asimov plugin for running simple-pe, a rapid, metric/Fisher-matrix-based parameter estimation code for gravitational-wave signals from compact binary coalescences.

📚 Full documentation and tutorial

Overview

Unlike a full stochastic (MCMC/nested-sampling) parameter estimation pipeline, simple-pe expands the likelihood locally around a matched-filter trigger to produce an approximate posterior in CPU-minutes rather than CPU-days -- useful for low-latency estimates, and as a fast cross-check or seed for a more detailed run. This plugin enables Asimov to build and submit simple_pe_pipe jobs for that analysis. It provides:

  • Configuration templating: renders a simple_pe_pipe config file from a bundled Liquid template, populated from a production's ledger metadata (config_template), for use by asimov manage build.
  • Trigger-parameter seeding: writes the small trigger_parameters.json file simple_pe_pipe uses to seed its local optimisation, before the main config is rendered.
  • DAG generation and scheduler integration: simple_pe_pipe builds a real HTCondor DAG (plus a companion local-run bash script); this plugin submits that DAG via Asimov's own scheduler abstraction (self.scheduler, from asimov.pipeline.Pipeline), which supports both HTCondor and Slurm -- rather than talking to a scheduler's API directly.
  • Status tracking: monitors job completion by checking for a genuine posterior samples file.
  • DAG-rescue-aware resurrection: like lalinference_pipe, simple_pe_pipe produces a real DAGMan workflow; an interrupted run leaves a rescue DAG behind, which is resubmitted automatically.
  • Dependency-driven post-processing: once a job completes, its samples are available (via collect_assets()) to any downstream production with a needs: dependency on it -- for example an asimov-pesummary production. This plugin doesn't submit that job itself; Asimov's own dependency resolution does.

Compatibility

Requires asimov>=0.7. This plugin does not depend on the simple-pe Python package itself -- like the sibling asimov-lalinference and asimov-bayeswave plugins, it shells out to the simple_pe_pipe executable, which is expected to be installed separately in the environment pointed at by Asimov's [pipelines] environment config option.

simple-pe is not currently distributed on PyPI or conda-forge; install it from its GitLab repository (it depends on pycbc and lalsuite, both available from conda-forge):

conda install -c conda-forge pycbc lalsuite
echo "setuptools<82" > constraints.txt  # see note below
echo "numpy<2" >> constraints.txt        # see note below
pip install -c constraints.txt "setuptools<82" "numpy<2"
pip install -c constraints.txt git+https://git.ligo.org/stephen-fairhurst/simple-pe.git

pkg_resources note. simple_pe_pipe imports pycbc.waveform, which does a bare import pkg_resources in retrieve_waveform_plugins() (see simple-pe issue #29, still open upstream). setuptools removed the bundled pkg_resources module starting with 82.0.0 (confirmed directly: 81.0.0 still provides it, 82.0.0 doesn't), so simple_pe_pipe fails to even import with ModuleNotFoundError: No module named 'pkg_resources' against an unpinned (or too-recent) setuptools -- confirmed directly via this plugin's own CI. A bare pip install "setuptools<82" isn't enough on its own, either: installing simple-pe in a separate pip install right after it can silently re-resolve setuptools back past 82 as one of its own transitive dependencies (also confirmed directly). A constraints file applied to both commands, as above, is what actually holds the pin.

numpy<2 note. simple_pe_filter's own load_trigger_parameters_from_file() wraps the loaded trigger-parameters dict in SimplePESamples (a pesummary-style samples container built for posterior chains, so even a single scalar comes back as a shape-(1,) array rather than a true 0-d array) before simple_pe.io.io.estimate_data_length_from_template_parameters does int(2**(np.ceil(np.log2(wf_len)))) on a value derived from it. NumPy hard-errors this exact implicit shape-(1,)-to-scalar conversion since 2.0 (only a DeprecationWarning before that): TypeError: only 0-dimensional arrays can be converted to Python scalars, hit on every real analysis regardless of channel mode -- confirmed directly via this plugin's own e2e CI on the real filter DAG node. Pinning numpy<2 (alongside every pip install here, for the same re-resolution reason as setuptools above) keeps it a warning instead of a crash without touching simple-pe's own source.

A note on the config schema below. The keys used by this plugin's bundled template (configs/simplepe.ini) -- trigger_time, trigger_parameters, outdir, channels, asd, f_low, f_high, approximant, accounting_group, accounting_group_user, generate_corner, disable_pesummary -- and their value formats are confirmed directly against a real, live simple_pe_pipe --help and a real successful DAG build in this plugin's own end-to-end CI. Its output-file detection (samples()) is still an unconfirmed best-effort guess, though: it searches for common posterior/samples filenames rather than one exact confirmed name. If you hit a mismatch there (or anywhere else) against a real simple_pe_pipe release, please open an issue or PR.

Installation

pip install asimov-simplepe

For development:

git clone https://github.com/transientlunatic/asimov-simplepe
cd asimov-simplepe
pip install -e ".[test]"

Usage

Once installed, Asimov discovers this plugin automatically via its asimov.pipelines entry point. Add a production to an event with pipeline: simplepe:

kind: analysis
name: simplepe-imrphenomxphm
pipeline: simplepe
waveform:
  approximant: IMRPhenomXPHM
likelihood:
  minimum frequency:
    H1: 20
    L1: 20
scheduler:
  accounting group: ligo.dev.o4.cbc.pe.simple_pe

asimov manage build submit will render a simple_pe_pipe ini (and the accompanying trigger-parameters file) from the bundled template (unless a config already exists in the event repository), build a DAG, and submit it.

Peak finder

peak_finder (default metric, simple-pe's own Fisher-matrix algorithm) selects the peak-finding method simple_pe_pipe uses, e.g. production.meta['peak_finder']: scipy. This isn't optional in practice: simple_pe_pipe's real --peak_finder CLI option defaults to an empty list, and its DAG-building code creates every actual analysis stage (the match-filter, metric, corner-plot, and PESummary jobs) inside a loop over that list -- an empty list means the loop runs zero times and the DAG silently ends up containing only its datafind job, no error of any kind. Confirmed directly via this plugin's own e2e CI, which is why this template always sets it.

Sky localisation

production.meta['localization file'] (unset by default) is the path to a FITS skymap, rendered as localization_file in the ini. simple_pe_analysis draws its sky-position samples from it; this is how a real analysis uses the search's BAYESTAR skymap. Without one, every posterior sample has the single sky position from the trigger, so the other parameters are conditioned on that position and no skymap can be produced. When a localization file is given, ra/dec are left out of the trigger parameters, because simple_pe_analysis raises if given both.

Effective sample target

neffective (unset by default) caps the number of effective posterior samples simple_pe_analysis targets before it stops iterating, e.g. production.meta['neffective']: 20. Left unset, simple_pe_analysis uses its own default (1000 effective samples, confirmed directly from its real source) -- the right choice for real production use. This plugin's own e2e test sets it low (see tests/test_blueprints/ simplepe_production.yaml): a real analysis against real GW150914 data, even with a higher-mode-capable, non-precessing approximant (see the "Known issue" note below), genuinely needs well over 25 wall-clock minutes on a single CPU to converge to the default 1000, confirmed directly via this plugin's own e2e CI -- more than is reasonable to demand from a smoke test of this plugin's own config rendering, DAG building, and submission (not a validation of simple-pe's actual scientific output).

Data

Strain data is read from a production's data metadata, using the same conventions as the other Asimov gravitational-wave pipeline plugins (e.g. asimov-gwdata, asimov-lalinference, asimov-pycbc):

  • data.channels -- per-interferometer strain channel name, without the leading IFO: (this plugin adds that itself when rendering the ini). simple_pe_pipe also recognises two special values here, confirmed directly from its own --help text: GWOSC to read public GWOSC open data instead of a private frame channel (this plugin's own end-to-end test uses this, against GW150914's real data), and INJ to have simple_pe_pipe simulate an injection itself rather than reading any real strain data at all -- no datafind access needed. When any interferometer uses INJ, before_config() also writes an injection.json file from the production's trigger metadata and the ini references it via injection = ... -- simple_pe_datafind requires this unconditionally in that case (confirmed directly from its real source, via this plugin's own e2e CI). Known issue: INJ mode currently hits a genuine upstream simple-pe bug once write_converted_injection_parameters() runs (a SimplePESamples- wrapped value eventually produces a NaN GPS time, sending LALSuite into what is for all practical purposes an infinite loop) -- confirmed directly via this plugin's own e2e CI (see CHANGELOG.md for the full trail); this plugin's own code fully supports INJ (unit-tested) and will use it correctly once fixed upstream, but the e2e test itself uses GWOSC instead to avoid depending on that fix.

  • data.asd -- per-interferometer path to a real, two-column (frequency, ASD) text file. There is no analytic-PSD-model-name shortcut -- confirmed directly from --help ("ASD files to use for the analysis") and from this plugin's own e2e CI, which generates a real file from pycbc's analytic aLIGOZeroDetHighPower model for its test (a PSD is needed for the Fisher-matrix/SNR calculation regardless of where the strain data itself comes from). There's no Asimov-core-level mechanism for ASDs, so this is the only way to supply one.

  • PSDs -- simple_pe_pipe accepts --psd as a same-shaped alternative to --asd (confirmed directly from its own --help/parsed-Namespace output: both are dict-typed, {}-default options using the same IFO:path token format as --channels). This plugin resolves the PSD to render from two sources, in priority order:

    1. production.psds -- Asimov core's own, built-in mechanism (confirmed directly from its real source, asimov.analysis.GravitationalWaveTransient._collect_psds()): a top-level psds: key on the production's ledger entry (a dict of IFO: path, not nested under data:), or, if that's not set directly, PSDs pulled automatically from a needs:-linked upstream production's collect_assets()["psds"]. This is the real, established Asimov convention for pre-computed PSDs (confirmed directly from a real user's project ledger) -- prefer this.
    2. data.psd -- a secondary, plugin-local override for setting a PSD directly on the production alongside data.channels/data.asd, used only if production.psds is empty.

    Supplying neither omits the psd/asd key entirely, which simple_pe_pipe will itself reject.

  • data.data files -- Asimov core's own, built-in mechanism for local frame files (confirmed directly from its real source, asimov.analysis.GravitationalWaveTransient.__init__, which defaults self.meta["data"]["data files"] = {} for every production, and from the sibling asimov-gwdata datafind pipeline, which populates it with exactly this {ifo: [path, ...]} shape). When every interferometer in the analysis has exactly one frame file here, this plugin renders a strain = {...} option alongside channels. Confirmed directly from simple_pe_pipe's real source: providing --strain at all makes it skip DataFindNode/simple_pe_datafind entirely and read the local frame file directly instead (via gwpy.timeseries.TimeSeries.read()) -- which also means the INJ/GWOSC channel magic-value detection above never runs, so a real channel name that happens to contain "inj" (e.g. a custom test frame's channel literally called "Injection") is read literally instead of being misrouted into simulate-injection mode and its upstream bug. Known limitation: more than one frame file per interferometer isn't currently supported (simple_pe_pipe's bare --strain IFO:path ini-dict form -- the same format used for channels/psd/asd -- has no list-literal syntax for stitching several files together); this raises a clear error rather than silently picking one.

This means a production populated by a data-retrieval step (for example asimov-gwdata) can be picked up by making the simplepe production needs: that data-retrieval production -- its PSDs are then picked up automatically via production.psds, and its channel output can be mapped into data.channels/data.asd (stripping the data-retrieval step's own IFO: channel prefix, if it includes one).

Trigger parameters

simple_pe_pipe seeds its local optimisation from an approximate set of trigger parameters (masses, spins, sky location). Supply these via a trigger block on the production:

trigger:
  mass1: 36
  mass2: 29
  spin1z: 0.0
  spin2z: 0.0
  ra: 1.95
  dec: -1.27
  distance: 440
  phase: 0
  psi: 0

Any parameter left unset falls back to a sensible default: mass1/mass2 default to 1.4 (solar masses), ra/dec/spin1z/spin2z/phase/psi default to 0, and distance defaults to 400 Mpc; time is always taken from the event's event time, not from this block.

Regenerating a production's ini

Asimov core's own asimov manage build command (asimov/cli/manage.py's build(), confirmed directly from its real source) only renders a production's .ini -- and only calls this plugin's before_config()/make_config() at all -- when no .ini already exists for that production in the event repository. If one is already checked in, asimov manage build finds it via production.event.repository.find_prods(), does nothing, and moves on: it does not re-render the ini and does not re-run before_config() (which is what (re)writes trigger_parameters.json/injection.json into the rundir).

This matters whenever a production's rundir has been cleared/reset (e.g. after a stale/interrupted run) while its .ini is still checked into the repository, or after upgrading this plugin to a version that changes what the ini should contain: simply rerunning asimov manage build is a no-op in both cases, and asimov manage submit will then fail because the rundir-side files the (unchanged) ini still references are missing or stale. To force a real regeneration, remove the existing .ini first (and, for a clean rundir, the working directory too) before rebuilding:

rm path/to/checkouts/<event>/<category>/<production>.ini
rm -rf path/to/working/<event>/<production>/
asimov manage build
asimov manage submit

Post-processing

This plugin does not run PESummary (or anything else) itself. When a simplepe production completes, after_completion() only marks it finished -- post-processing is expressed as a separate production with a needs: dependency on it, e.g.:

kind: analysis
name: simplepe-test-pesummary
pipeline: pesummary
needs:
  - simplepe-imrphenomxphm
postprocessing:
  pesummary:
    multiprocess: 2

Asimov's own dependency resolution builds and submits simplepe-test-pesummary once simplepe-imrphenomxphm reaches finished; PESummary picks up its samples via simplepe-imrphenomxphm's collect_assets() (through production._previous_assets()).

This is also why configs/simplepe.ini sets disable_pesummary = True. simple_pe_pipe's own DAG otherwise bakes a full PESummary post-processing job into itself as a child of the analysis node whenever that option is left at its default False (confirmed directly from its real main() source) -- duplicating the separate, needs:-linked PESummary production above, and running PESummary twice for no benefit.

Known issue (root-caused, not a permanent blocker): simple_pe_pipe's own analysis stage can hit a real crash before producing a posterior samples file: PESummary's subdominant-multipole rejection-sampling reweighting (pesummary.core.reweight.rejection_sampling(), called unconditionally from simple_pe.param_est.pe. reweight_based_on_observed_snrs()) has no guard against a non-finite weight, and raises OverflowError: Range exceeds valid bounds when one occurs. This is a different use of PESummary than the disable_pesummary/PostProcessingNode above: it happens inside simple_pe_analysis itself, as part of generating posterior samples from the Fisher-matrix point estimate, and isn't gated by disable_pesummary at all.

This first looked like a genuine, unfixable upstream numerical-robustness bug -- but root-causing it directly via this plugin's own e2e CI (see CHANGELOG.md for the full trail) found the real trigger: the reweighting step unconditionally measures the observed SNR in the (3,3)/(4,4) higher multipoles and in precession, regardless of whether the configured waveform.approximant can represent them. Against a dominant-mode-only, non-precessing approximant (e.g. IMRPhenomD), those observed SNRs come back NaN, and the reference distribution built from them underflows to exactly 0.0 probability for essentially every sample once zeroed -- not a rare outlier, but systemic corruption. Using an approximant that actually supports higher modes (e.g. IMRPhenomXHM or IMRPhenomXPHM -- the latter is simple_pe_filter's/ simple_pe_analysis's own --approximant default) avoids this entirely, confirmed directly: no OverflowError (or any other crash) recurs. simple-pe's own real-waveform interpolation grids do make a precessing+higher-mode approximant meaningfully slower to converge than a dominant-mode-only one, though -- see the Effective sample target section above for how this plugin's own e2e test manages that.

Defensive safety net. simple-pe's reweight_based_on_observed_snrs()/PESummary's rejection_sampling() still have no guard against a non-finite weight even with a correct approximant choice -- a real analysis could still hit a similarly degenerate weight for other reasons this plugin hasn't characterised. scripts/patch_simple_pe_reweight_guard.py patches an installed simple-pe's reweight_based_on_observed_snrs() in place to zero out any non-finite weight before it reaches rejection_sampling() -- treating a numerically broken sample as zero-probability (rejected) rather than crashing, or (worse) treating it as certain. The correct fix still belongs upstream (in either simple-pe or pesummary, since any of rejection_sampling()'s other callers could hit the same crash); this is deliberately just a stopgap. Run it once, in the same Python environment simple_pe_analysis runs in, right after installing simple-pe and before running any real analysis:

python scripts/patch_simple_pe_reweight_guard.py

It's idempotent (a no-op if already applied) and fails loudly, rather than silently doing nothing, if simple-pe's source has changed enough that its exact expected original text can't be found -- see the script's own docstring for the full rationale. This can't be applied automatically by asimov_simplepe itself at import time: the buggy code runs inside simple_pe_analysis's own HTCondor subprocess, a completely separate Python process from asimov's, so a plugin-level monkeypatch would never reach it -- it has to be applied to the installed simple-pe package directly, in whatever environment actually runs the analysis jobs. This plugin's own CI applies the exact same script (see .github/actions/setup-simplepe-env/action.yml).

Testing

Unit tests (mocked, no external dependencies):

pip install -e ".[test]"
pytest

.github/workflows/e2e.yml also runs a genuine end-to-end test: a real simple_pe_pipe DAG built and submitted through a real HTCondor scheduler against real GW150914 GWOSC data, waiting for and validating the real peak_parameters.json/peak_snrs.json output its analysis node produces. It also waits (best-effort; not yet a hard pass/fail requirement) for a real, final posterior_samples.dat -- see the "Known issue" note under Post-processing and the Effective sample target section above for the full trail on why a full analysis needs neffective capped for this to be feasible in CI at all.

.github/workflows/docs.yml also checks that the subcommand and flags used by every asimov ... command shown in docs/*.rst still exist on the live, installed asimov CLI, on every pull request (scripts/lint_tutorial_commands.py).

Contributing

Contributions welcome! Please submit issues or pull requests to transientlunatic/asimov-simplepe.

License

MIT -- see LICENSE.

This page is generated from the package's own README and metadata. To change it, update the repository or the package's PyPI metadata.