Factorized sampler: a composable Step-based inference spine#389
Factorized sampler: a composable Step-based inference spine#389stephengreen wants to merge 66 commits into
Conversation
Introduce the factorized-sampler spine from the hackathon design (vault/Hackathon/Factorized_Sampler_Design.md): the posterior as an ordered product of conditional factors, q(theta|d) = prod_i q_i(theta_i | f_i(theta_<i, d)). dingo/core/factors.py (domain-agnostic): - Factor (ABC): physical-in/physical-out sample_and_log_prob / log_prob - ChainComposer: autoregressive composition, topological-order validation, per-factor log-prob summation - Standardization: per-factor mean/std adapter (both directions + Jacobian) - ComposedSampler: thin run_sampler facade (batching -> DataFrame) - SamplerContext protocol dingo/gw/inference/factors.py (GW plain-NPE path): - GWSamplerContext.from_model: domain + one-time data prep, cached prepared_data - FlowFactor: wraps a posterior model, encapsulates standardization - GWComposedSampler: adds GW _post_process (fixed-param injection + RA correction) - FixedFactor / SyntheticPhaseFactor stubs for later steps Plain-NPE parity verified bit-exact against the current GWSampler on a real GW200129 NPE model, both at the core (_run_sampler) and end-to-end (run_sampler, batched, with post-processing) levels. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
FlowFactor is domain-agnostic -- it reaches data only through the SamplerContext.prepared_data() protocol -- so it belongs in core alongside the Factor ABC rather than in the GW module. The GW layer keeps only the GW-specific pieces: GWSamplerContext (which builds the data-prep conditioning map f_i) and GWComposedSampler (post-processing). Document GWSamplerContext.raw_context (= EventDataset.data; retained for the not-yet-wired likelihood and the serialized transport state). dingo.gw.inference.factors re-exports FlowFactor, so existing imports keep working. Plain-NPE parity with GWSampler remains bit-exact. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add multi-iteration time-GNPE to the factorized sampler: - core: GibbsComposer (seed from an init factor, iterate a GNPE step to a fixed point, return samples without log_prob); ChainComposer.sample; unify ComposedSampler._run_batch so the facade drives either composer. - gw: GNPEFlowFactor (one GNPE iteration -- blur proxies, time-shift strain, standardize context, sample, recompute detector times; replicates GWSamplerGNPE's transforms) and GWComposedSampler.from_gnpe_models. Multi-iteration GNPE parity is bit-exact vs GWSamplerGNPE over 4 Gibbs iterations on the GW200129 init/main pair; plain-NPE parity unchanged. Single-step GNPE (num_iterations=1, log_prob-preserving / BNS) is the chain composer path and is not yet implemented. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The façade already drives either composer at runtime (it needs only sample(num_samples, context) -> dict); broaden the type hint and docstring to match (Union[ChainComposer, GibbsComposer]). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move batching from the ComposedSampler façade's single whole-chain loop down to each Factor: a per-factor `batch_size` plus base-class batching wrappers (`sample_and_log_prob`/`log_prob`) that chunk `num_samples`, slice the conditioning to match, call the `_sample_and_log_prob`/`_log_prob` hooks, and concatenate. The ChainComposer already materializes the full sample set between factors (cheap parameter vectors), so each factor batches independently at the size that fits its own memory footprint; the peak is the single most expensive factor, not the whole chain at one global size. GNPEFlowFactor.gibbs_step gets the same treatment, chunking the (independent) Gibbs walkers. The façade keeps `run_sampler(batch_size=...)` as a convenience that broadcasts a default across factors without their own. `batch_size=None` short-circuits to the original single-pass path, so default behavior is unchanged and plain-NPE/GNPE parity is preserved. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Export the factorized sampler's output to a gw Result so the existing post-processing pipeline -- synthetic phase, importance sampling, evidence, plotting -- runs on it unchanged. `context` is the raw event data (GWSamplerContext.raw_context), which Result needs to rebuild the likelihood. Validated end-to-end: NPE -> to_result() -> importance_sample reproduces the current GWSampler pipeline bit-exactly (log_prob, log_likelihood, log_prior, weights, log_evidence all max|delta|=0); GNPE -> to_result() builds a valid Result. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two cosmetic fixups in the merged factor files (docstring close + a one-line call) so the branch is black-clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pull batching out of the factors into a single vertical (depth-first) executor on ChainComposer, driven by a shared chunk_and_concat helper (also used by GibbsComposer). Add Stage(factor, fan_out) so a chain can expand num_samples per conditioning row (M intrinsic x K extrinsic); num_samples is the base/root count, total rows = num_samples * expansion. Factors become single-pass: drop batch_size, the batching wrapper methods, and _slice_given / _batch_bounds / set_default_batch_size / _gibbs_step (keep _cat_dict); inline ComposedSampler._run_batch into run_sampler. Fix FlowFactor's conditioned path to draw num_samples per conditioning row (N-row context + expanded data), matching what GNPEFlowFactor already does. Rationale: only the vertical executor bounds fan-out memory (you cannot materialize M*K intermediates for 1000-extrinsic-per-intrinsic), and chunking the whole chain / Gibbs loop per base-chunk reproduces the old sampler's batching order, so parity is exact. See the hackathon design doc section 14. Bit-exact vs GWSampler (NPE) and GWSamplerGNPE (multi-iteration GNPE), batched and unbatched, including post-processing. Add tests/core/test_factors.py (mock-factor unit tests for fan-out expansion + alignment, log-prob summation, topological validation, and the batching primitive). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Gibbs sampling in Dingo is used only for GNPE, and the class's members (gnpe_factor, proxy_parameters, gibbs_step) are all GNPE-specific, so the general name was misleading. Rename to GNPEGibbsComposer and keep it in dingo.gw.inference.factors alongside GNPEFlowFactor. Core keeps the domain-agnostic pieces -- ChainComposer, chunk_and_concat -- and gains a Composer protocol that ComposedSampler depends on, so core no longer references the GW-specific composer by name. GNPE sampling remains bit-exact vs GWSamplerGNPE. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rewrite the dingo.core.factors docstrings as reference documentation: state what each class and function is and does, with parameters and returns. Drop design-justification, codebase-history narration, comparisons to the old samplers, and conversational asides -- that rationale lives in the design doc. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the bundled GNPEFlowFactor (a non-Factor Gibbs step) and the single-step GNPEChainFactor with two conditional Factors: - GNPEKernelFactor: the perturbation kernel p(theta_hat | theta). Blurs detector times into proxies; its log_prob is the delta_log_prob_target importance-sampling correction. - GNPEFlowFactor: the main network q(theta | theta_hat, d), now a real Factor, shared by both composers. GNPEGibbsComposer becomes the generic GibbsComposer in core.factors (cycles a factor list, no GNPE specifics). Multi-iteration GNPE is GibbsComposer([kernel, flow]); single-step GNPE is ChainComposer([proxy_source, flow]) with the kernel correction applied out of the proposal sum. ComposedSampler keeps the Composer protocol, now used consistently by GWComposedSampler too (dropped the dead @runtime_checkable). Bit-exact vs the old GWSampler / GWSamplerGNPE for NPE, multi-iteration GNPE, and single-step GNPE across both verification harnesses. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GWComposedSampler.from_singlestep_gnpe builds a ChainComposer of [proxy_source, GNPEFlowFactor] for density-preserving single-step GNPE. proxy_source supplies the detector-time proxies: a FixedFactor for prior conditioning (BNS), or an unconditional NDE for density recovery (which supersedes prepare_log_prob's single-step half). The kernel correction delta_log_prob_target = log p(theta_hat | theta) is evaluated in post-processing (via a held GNPEKernelFactor) at the proxies and the detector times recomputed from theta, then the intermediate detector times are dropped. It is kept out of the proposal log_prob; the column-driven importance-sampling layer applies it to the joint target. Bit-exact vs GWSamplerGNPE(num_iterations=1) end to end through run_sampler + to_result (fixed-proxy case). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add a `Step` protocol (parameters / conditioning / sample_and_log_prob -> (dict, Optional log_prob)); Stage.factor -> Stage.step, .factors -> .steps. - ChainComposer's fold is None-aware: a single density-free step nulls the chain's log_prob, and sample() then omits it. - Replace GibbsComposer with GibbsBlock, a density-free Step; multi-iteration GNPE is now ChainComposer([GibbsBlock(...)]). The Gibbs loop is unchanged, so parity stays bit-exact. - Drop the now one-implementer Composer protocol. Parity: NPE, multi-iteration GNPE, single-step, and packaged to_result all bit-exact (max|delta|=0); tests/core/test_factors.py 12/12. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e-plug Reparametrizations (Increment 1b): - Add Reparametrization, a core bijector Step (forward/inverse/log_det, contributing -log|det J|, 1:1). RAReparam ports GWSamplerMixin's t_ref/RA sky rotation into the chain, via a ra@t_ref -> ra factor-boundary alias, and deletes _correct_reference_time. ra is stored float32 (a bounded angle; the old float64 was an incidental numpy promotion). The correction is computed in float64 (absolute GPS times); harnesses compare ra at float32. - FlowFactor / GNPEFlowFactor gain an `aliases` map exposing trained names under canonical names at the factor boundary. - The composer makes a Reparametrization consume its input (in-place bijection), so the network-frame intermediate never reaches post-processing. Fixed parameters (Increment 2): - Delta-prior parameters move from _post_process into a FixedFactor chain step (_fixed_prior_steps, resolved once at build time). - The composer supports an unconditioned non-root step (a filler): it draws one value per current row rather than fanning out. FixedFactor also remains the chain root for prior-conditioning / known proxies. Drop the dead _post_process inverse branch: its job was to strip the log_prob column so old Sampler.log_prob's in-band `+=` would not double-count. The factorized log_prob sums per-factor densities explicitly, so both the `+=` and the strip are obsolete. _post_process is now just the single-step-GNPE kernel correction; the `inverse` parameter is gone. Parity: NPE, multi-iteration GNPE, single-step, and packaged all bit-exact (ra at float32). tests/core/test_factors.py (reparam consume + -log_det + round-trip; unconditioned filler incl. as root) and tests/gw/test_ra_reparam.py (forward-inverse round trip + no-op) green; 21 tests total. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add TargetCorrection, a core kind-3 Step: it emits an importance-sampling target-side column and contributes 0 to the proposal density (1:1). GNPEKernelCorrection emits delta_log_prob_target = log p(theta_hat | theta) and consumes the intermediate detector times. - Generalize the composer's consume to a `consumes` attribute (used by both reparametrizations and target corrections), and add `produces` so a step's side-channel columns (e.g. GNPEFlowFactor's recomputed detector times) satisfy the topological check. - from_singlestep_gnpe adds GNPEKernelCorrection to the chain; drop the kernel_factor __init__ argument and _add_kernel_correction. - GWComposedSampler no longer overrides _post_process (all GW-specific processing is now chain steps); the base ComposedSampler hook stays as a no-op. - Tighten docstrings and NotImplementedError messages. Parity: NPE, multi-iteration GNPE, single-step, and packaged all bit-exact; tests/core/test_factors.py + tests/gw/test_ra_reparam.py green (24 tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The factor is a point mass q = delta(theta - c). DeltaFactor names the math -- matching bilby's DeltaFunction, the prior these pinned parameters come from -- and frees "fixed" for a future unconditional / data-independent flow factor. Also renames the _fixed_prior_steps helper to _delta_prior_steps. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… runner With all GW-specific processing expressed as chain steps (RA reparametrization, delta factors, kernel correction), the ComposedSampler._post_process hook is a dead no-op. Remove it and its run_sampler call: ComposedSampler is now a domain-agnostic runner, and GWComposedSampler is a GW builder (the from_* constructors) and exporter (to_result) on top of it. Parity: NPE, multi-iteration GNPE, single-step, and packaged all bit-exact; tests/core/test_factors.py + tests/gw/test_ra_reparam.py green (24 tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@max-dax — this is the factorized-sampler spine we've been coordinating on |
Conditioning was a 2-field dataclass (context, given) with no behaviour. Steps now take sample_and_log_prob(n, context, given) / log_prob(theta_i, context, given) directly, and the composer passes the two. Any parameter-dependent transform lives inside the factor (as GNPE shows), so the conditioning never needed to carry more than these two fields. Parity: NPE, multi-iteration GNPE, single-step, and packaged all bit-exact; tests/core/test_factors.py + tests/gw/test_ra_reparam.py green (23 tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Give GWSamplerContext a likelihood() that builds the exact GW likelihood on the event's raw data (the network's decimated/whitened view stays in prepared_data()), and add SyntheticPhaseFactor, a terminal chain factor that reconstructs the coalescence phase for a phase-marginalized network from that likelihood on a phase grid, returning phase and its proposal log-prob. - GWSamplerContext gains base_metadata and likelihood(...), porting Result._build_likelihood. The likelihood reference time is the event time (the training-frame RA correction is already applied to the samples). - SyntheticPhaseFactor supports the (2, 2) approximation and the exact mode-decomposed grid, with a uniform floor for mass coverage. Verified bit-exact against Result._build_likelihood and Result.sample_synthetic_phase via model-based harnesses; 7 mock-based CI unit tests added in tests/gw/test_synthetic_phase.py. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
For the quartodoc (markdown) docs migration: replace RST double-backticks with markdown single backticks across dingo/core/factors.py and dingo/gw/inference/factors.py, and add numpy-style Parameters/Returns blocks to the public constructors and builders. Docstrings only -- no code changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduce a cached prior property on GWSamplerContext that builds the static intrinsic+extrinsic prior from model metadata, and route _marginalized_prior through it. First step of the context / Result-diet consolidation: the static prior gets a single home on the per-event context. Defer the _delta_prior_steps and Result._build_prior call sites to the Result-diet step; in the GNPE builder the context is built from the init model while those read the main model metadata, so unifying them needs that resolved first. Bit-exact: 30/30 factor CI, plus verify_likelihood_context and verify_integrated (NPE+GNPE) at max|delta|=0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Complete the prior consolidation started in 2b (context.prior), part of the Result diet (2c): - Build the multi-iteration GNPE context from the main model (it owns the analysis), guarded by a check that the init and main models agree on the shared data-prep view (domain, detectors, reference time). - _delta_prior_steps and Result._build_prior now source the static prior from context.prior. to_result passes the live GWSamplerContext as a new sampler_context arg; Result deepcopies it (the marginalization split-off mutates the prior) and still self-builds when loaded from file (no context). Bit-exact / pass: verify_integrated (NPE+GNPE), verify_singlestep_packaged, verify_prior_delegation, 30/30 factor CI. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ep log_probs ChainComposer.log_prob previously called step.log_prob uniformly, which raised for every real chain (reparams and corrections had raising stubs, and consumed columns like ra@t_ref were absent from the samples). Now the steps fold in exact reverse chain order, restoring the column state each step saw during sampling: a Reparametrization rebuilds its inputs via inverse and contributes -log|det J|, a Factor contributes its log_prob, a TargetCorrection contributes nothing, and a density-free chain (GibbsBlock) raises. - _validate now rejects a step overwriting an existing column, except a Reparametrization replacing its own inputs (invertible, so log_prob can restore the state); this is the invariant the reverse fold relies on. - GNPEFlowFactor.log_prob: run the proxies-present data prep, undo the post-network geocent_time proxy shift via PostCorrectGeocentTime(inverse=True), standardize, and score the network; carries its own Standardization now. - DeltaFactor moves to core (nothing GW about a point mass) and gains log_prob = 0 per row on the evaluated block's device; the gw module re-exports it for the builders. - Drop the dead raising log_prob stubs on Reparametrization/TargetCorrection; document that a correction must only consume side-channel intermediates. - RAReparam: document the principal-branch semantics of the modulo for samples drawn outside [0, 2pi). - Tests: analytic-density re-plug through reparam + correction chains (incl. an in-place same-name reparam), overwrite validation, density-free raise. Model-based re-plug harness (NPE + single-step GNPE) agrees to float roundoff on in-range rows (max ~2e-4, median ~4e-6); sampling parity stays bit-exact. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…texts - SamplerContext/GWSamplerContext gain device (the model device): steps that create fresh tensors (DeltaFactor) create them there; steps that transform existing rows (the Reparametrization.log_det default, TargetCorrection, GNPEKernelFactor.log_prob) follow their inputs, so a chain stays on one device end to end (the GPU pipe case). The kernel density converts each side to numpy before subtracting (times and proxies may live on different devices; the kernel is a bilby PriorDict -- the same object that samples the blur). SyntheticPhaseFactor returns its phase and log-prob on the chain device. - GWSamplerContext.likelihood() keeps the most recently built likelihood with its arguments and rebuilds only when they change: the synthetic-phase factor requests one per chain chunk, and importance sampling will request its own configuration once. The arguments are captured via locals() so the comparison tracks the signature automatically. - Remove the never-used phase_grid constructor argument from StationaryGaussianGWLikelihood, Result._build_likelihood, and GWSamplerContext.likelihood(); the grid is assigned as an attribute by its consumers (synthetic phase) or set internally by phase marginalization. - Result.reset_event() drops the live sampler_context: after resetting to (possibly regenerated) event data it no longer describes self.context. - Tests: device placement (meta unit test + an MPS end-to-end mixed chain), likelihood cache semantics, reset_event invalidation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The word "context" meant four things (the sampler context, the network's
conditioning parameters, the Result storage key, and the raw strain/ASD
dict). The new API now reserves it for the sampler context: the data dict is
event_data, matching StationaryGaussianGWLikelihood(event_data=...) and
EventDataset.data. The serialized Result key ("context") and the training
metadata name (context_parameters) are frozen by back-compat and unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An unconditional NDE (unconditional: True in its training metadata) takes no network input: FlowFactor now draws it with no arguments and never touches the context, mirroring the old Sampler._run_sampler branch. This is the proxy source for density recovery on the spine: prepare_log_prob's re-sample half becomes from_singlestep_gnpe(main, FlowFactor.from_model(nde)). Metadata transparency, restoring the old two-handle split: the network-bound settings (standardization, inference_parameters, context_parameters) are always the model's OWN -- an unconditional NDE carries its own proxy standardization, distinct from the base model's under metadata["base"]. FlowFactor and GNPEFlowFactor now read model.metadata directly (previously _base_model_metadata, which for an NDE would have silently applied the base model's standardization); _base_model_metadata keeps the analysis-side role (domain / dataset / detector settings) and its docstring now states the rule. - Mock CI test with a decoy base standardization (using it fails loudly). - Parity harness (verify_unconditional_flowfactor.py, local models): replays prepare_log_prob end to end -- Gibbs run, train a small NDE on the proxies, then (A) FlowFactor(nde) vs GWSampler(nde)._run_sampler bit-exact, and (B) from_singlestep_gnpe(main, FlowFactor(nde)) vs the old GWSamplerGNPE(num_iterations=1, init_sampler=GWSampler(nde)) -- all 18 shared columns bit-exact. (The old full run_sampler() path is itself broken for unconditional models -- _post_process reads metadata[dataset_settings]; production only ever calls _run_sampler, which is the reference used.) - Note the embed-once gap for row-identical data with row-varying conditioning (intrinsic/extrinsic split) as a TODO at the conditioned path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…witch
Add --sampler-implementation {legacy, composed} (default legacy) to the
sampler options. The sampling stage builds a GWComposedSampler for plain
NPE when composed is selected (GNPE not yet supported on this path), and
GWComposedSampler gains the to_hdf5 shim mirroring Sampler.to_hdf5.
A/B parity vs legacy on GW200129: sampling-stage Result HDF5 bit-exact
(all columns, settings, context; ra stored float32), and the unmodified
importance-sampling stage consumes the composed file with identical
output up to ra-float32 precision (~1e-8 in log evidence).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The .pt branch referenced a nonexistent args.checkpoint (the parser defines file_name), so it could never have run, and both branches missed a window_factor nested in a multibanded domain's base_domain -- which is exactly where pre-fix models carry it. Both file types now strip the key at either level. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ProxyOffsetReparam reconstructs a physical parameter from a proxy-conditioned network's offset output, X = delta_X + X_proxy (the DINGO-BNS chirp-mass pattern): a pure shift at fixed proxy (log_det = 0) that consumes the offset column while keeping the proxy recorded with the samples, which is precisely what makes the chain output self-sufficient for inversion. Inverting it needs the proxy, so Reparametrization.inverse gains a "given" argument carrying the non-consumed conditioning still present in the chain -- the third bijection to need its parameterization at inversion (RA needs none: its rotation is a per-event constant from the context; the spin-convention change needed the invariant conditioning and had to raise). The reverse fold supplies it, and SpinConventionReparam.inverse is now implemented through it (physical -> network, roundtrip-tested), closing the documented protocol gap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
c4a3e6d to
9c32a78
Compare
GWSamplerContext.from_model_metadata accepts fixed_context_parameters: for a chirp-GNPE model, the raw strain is heterodyned once with the fixed proxy (HeterodynePhase at the head of the data preparation, before decimation, on the base domain); only proxy entries parameterize the data preparation, so a pinned sky position is conditioning-only. Without pins the network-input view fails loudly while the likelihood and prior views keep working (from-file results reconstruct their context without pins). GWComposedSampler.from_model becomes the general single-network builder: a model with context_parameters (the DINGO-BNS chirp-mass prior conditioning with a fixed sky position) requires pins for exactly those parameters, and the chain becomes [DeltaFactor(pins), conditioned FlowFactor, one ProxyOffsetReparam per inferred offset with a pinned proxy]. Plain-NPE assembly is unchanged. ProxyOffsetReparam moves to dingo.core.factors: it is domain-agnostic (a name-keyed shift), meeting the core criterion alongside DeltaFactor. Verified on the DINGO-BNS GW170817 model (window-fixed copy): the chain samples in a single pass with finite log_prob, chirp_mass minus the proxy fills the +/-0.005 training kernel on noise-only data, the re-plug through the offset inverse reproduces the stored density at float32 roundoff, and to_result exports the tidal prior intact. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The chirp-mass heterodyne now runs in parameters mode: prepared_data receives the chain's conditioning from the conditioned factors, verifies the required columns are constant, injects the values into the transform chain under their physical names (extracted as Python floats, preserving the float64 heterodyne path), and keys its cache on them -- a later call with different values fails rather than silently serving stale data. The pin values therefore have a single owner, the chain's root DeltaFactor, consumed by both the network conditioning and the data preparation, so the two cannot disagree. The context records only the conditioning names its preparation is a function of. The composer gains the point-mass rule: DeltaFactor declares point_mass = True, and the requested sample count lands on the first stage that carries multiplicity -- a root prefix of pins emits a single row each, and the flow draws N in one conditioned call (one embedding), with the prefix rows expanded to match. Chains with stochastic roots (NPE, GNPE, sample tables) are unchanged -- verified bit-exact by the pipe regressions -- and the BNS chain produces the same draws to float32 kernel-order noise (measured ~1e-5). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add the --fixed-context-parameters option (the demo-INI dict form) and pass it through SamplingInput to the composed single-network builder, so context-conditioned models (single-step chirp-mass GNPE, e.g. binary neutron stars) run through the pipe. Combining it with model-init raises. Verified end-to-end on real GW170817 data (demo INI adapted to the exact base-domain likelihood; efficiency 11.4%, pins and full chain provenance in the result) and bit-exact on the no-pins NPE pipe regression. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sweep the chirp-mass proxy over the training prior on the ordinary chain machinery -- a SampleTableFactor grid root and a conditioned FlowFactor drawing per grid row -- and take the maximum-likelihood draw as the trigger value (Dax et al., Nature 639, 49 (2025)). The scan runs the grid in blocks and selects with a phase-marginalized likelihood that now optionally reports the matched-filter SNR (return_aux_snr). To support this, prepared_data gains a uniform contract: with conditioning, the result is row-aligned -- one data row per conditioning row -- with a pinned value prepared once behind a value-keyed memo and varying values prepared in a single batch-native pass. Columns the preparation does not consume condition the network alone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add the --chirp-mass-scan option ('true' for model-derived defaults, a
dictionary for overrides; mutually exclusive with a pinned
chirp_mass_proxy and with model-init). The sampling stage runs the scan
once before building the sampler, fills the winning trigger into the
fixed context parameters, and records the scan settings and winner
alongside the chain provenance. Verified end-to-end on GW170817 with
only the sky position pinned: the scanned trigger matches the posterior
median, and the evidence agrees with the externally-pinned run within
Monte Carlo error.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
dingo/gw/inference/factors.py (1673 lines) becomes three modules with
one concern each, moved verbatim:
context.py GWSamplerContext and the frequency-range update helper
steps.py the GNPE factors, SyntheticPhaseFactor, and the
coordinate reparametrizations
sampler.py GWComposedSampler and its chain builders
All imports are retargeted repo-wide (pipe, result, importance
sampling, scan, tests, docs); no compatibility shim is kept, since the
module was introduced on this branch and has no external consumers.
Pure relocation -- no code changes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A parameter that is frame-corrected at the output must be inversely corrected at the input: a sky position pinned at the event time now enters the network as ra@t_ref via RAToTrainingFrame, and the trailing RAToEventFrame (formerly RAReparam; renamed as a directional pair) restores the event-frame value in the samples, so likelihood and provenance stay physical. The rotation is exactly zero when the event time equals the training reference time, as for all current networks. Supporting changes: FlowFactor applies its alias map to conditioning names as well as outputs; the composer validator models consumed columns; and the chain fold defers the base count past 1:1 steps (reparametrizations, target corrections) to the first sampling stage, with a mock-level regression test for that chain shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New advanced-guide page documenting the factorized sampler: the product-of-conditionals model, step types, the sampler context, multiplicity rules, the reverse density fold, provenance, and chain building, with mermaid figures for the chain, the context, and the combined system. Also fixes two unterminated inline-code spans in the ChainComposer docstring that produced docutils warnings under autodoc. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Refine the density presentation on the sampling-chains page: the product of conditionals is owned by the factors, which may be stochastic or point masses (conditioned on, not integrated over), while a reparametrization is a bijective change of variables that removes its inputs from the table and contributes a Jacobian term rather than a new factor. The Dirac-delta reading of deterministic steps is dropped except for point masses, where it is exact. Also states the composer's consistency check in the intro and introduces each step type by what it does rather than what it is not. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New advanced-guide page for DINGO-BNS inference: phase heterodyning as single-step chirp-mass GNPE, prior conditioning with pinned context parameters, the fixed-proxy chain, the chirp-mass scan, and the dingo_pipe options (fixed-context-parameters, chirp-mass-scan) with a GW170817 configuration example. Adds the Nature 2025 reference and links the page from the sampling-chains examples. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
inference.md becomes a proper workflow page: the two inference routes, the event_data contract, the sampler builders, running and output, and the refreshed Injection section. The injection example is updated to the current API and was executed end to end against a trained GNPE model pair; its ASD line now restricts the dataset to the model's detectors. The GNPE inference section replaces the removed dingo_analyze_event script with the dingo_pipe and Python routes. build_model_from_kwargs is re-exported from dingo.core.posterior_models so the documented import works. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
gnpe.md gains a single-step section covering the two density-preserving uses (density-recovery re-sampling and fixed BNS proxies). dingo_pipe.md points single-network context-parameter models at fixed-context-parameters / chirp-mass-scan on the BNS page and notes the base-domain likelihood default for multibanded models. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@max-dax For reviewing, I would recommend starting by reading the updated documentation, mainly the sections on "Sampling chains" and "Binary neutron stars". |
Conflict resolutions: result.py takes the upstream comment fix (the numpy>=2 bilby prior workaround merged cleanly); test_result.py keeps both new test sections (upstream statistical properties + the sampler-context lifecycle test); test_likelihood.py combines the two independently added suites (upstream inner-product/marginalization tests + the decomposition/d_inner_h tests). Known follow-up: #387 also added tests for the legacy samplers, which this branch removed; those tests are triaged in the next commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The merge from hackathon-1 brought in #387's tests for the removed legacy samplers. Of their 32 tests, 17 are adapted to the new API (composed-sampler counting/batching, FlowFactor log_prob round-trips including the data-conditional branch, the frequency validators, the sidereal shift magnitude, delta-prior filling, and Result export round trips), 7 are dropped as duplicates of existing coverage, and 8 are dropped as obsolete along with the removed Sampler machinery. All other #387 test files pass unchanged and are kept. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GPU testing of the chirp-mass scan exposed two device leaks: the SampleTableFactor emitted its fixed table on the CPU, feeding CPU conditioning into a CUDA network, and the consumed conditioning columns reached the numpy data-preparation world as CUDA tensors. The table now joins the chain on context.device (the same policy as DeltaFactor), the conditioning columns are normalized to the host alongside their float64 dtype, and the scan moves chain outputs to the CPU before numpy. Adds a CUDA-gated regression test. Validated on an A100: the GW170817 scan recovers the trigger (1.19758, snr 32.9) and matches the pinned run's evidence within Monte Carlo error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Covers Result.update_prior directly: the string-form metadata round trip, reweighting before and after importance sampling (hand-computable PowerLaw-vs-Uniform tilt on identical support), and a from-file reload rebuilding the evolved prior rather than the training prior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
bilby's PriorDict instantiates its input dict in place, replacing the string values with Prior objects. The stored metadata was already protected by a copy; the caller's dict was not, a latent behavior dating back to the introduction of prior updates. Instantiate from a copy and assert the caller's view in the tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the hand-written entry with the INSPIRE export (same texkey and fields, verified against the published paper; adds the LIGO report number). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Removes the four mermaid blocks and their captions (100 diff lines), bringing the PR to 9,988 changed lines against a suspected 10,000-line limit. To be reverted after the review runs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ze cap" This reverts commit f20edf5.
The auxiliary SNR in the phase-marginalized likelihood reused the marginalized-likelihood term ln_i0(|kappa2C|) in place of the phase-maximized matched-filter statistic |kappa2C|, biasing every reported scan SNR low by ~0.5*ln(2*pi*|kappa2C|)/sqrt(rho2opt). Trigger selection was unaffected (it maximizes the log-likelihood, which was correct). Found by the cloud review; adds a regression test pinning the statistic against an independent computation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
misc_scripts/evaluation.py still imported the removed GWSampler; port it to GWComposedSampler.from_model. The --log-probs help text concatenated adjacent string literals without a space. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| # Phase-maximized matched-filter SNR: |kappa2C| / sqrt(rho2opt). | ||
| # (ln_i0(|kappa2C|) is the phase-marginalized likelihood term | ||
| # above, not a matched-filter statistic.) | ||
| snr = np.abs(kappa2C) / rho2opt**0.5 |
There was a problem hiding this comment.
@max-dax flagging this line for a look — it feeds the trigger SNR that the chirp-mass scan logs and stores in provenance.
The deep review caught that this previously reused the phase-marginalized likelihood term ln_i0(|kappa2C|) as the SNR numerator. The intended statistic is the phase-maximized matched-filter SNR: under the (2,2) approximation, <d, h(phi)> = |kappa2C| cos(2 phi - arg kappa2C), so maximizing over phase gives |kappa2C| / sqrt(rho2opt) (with rho2opt phase-invariant). By contrast, ln_i0(|kappa2C|) ~ |kappa2C| - 0.5 ln(2 pi |kappa2C|) is the marginalized (integrated) weight — the maximized value minus the phase-volume Occam penalty — so the old value was biased low (GW170817 scan: 32.89 → 32.99 after the fix). Trigger selection was never affected, since the scan argmaxes the log-likelihood, which was always correct.
A regression test pins the statistic now (test_phase_marginalized_aux_snr_is_phase_maximized). Note return_aux_snr is new in this branch — it did not come from #354; the PR description's earlier attribution was wrong and has been corrected.
Summary
This PR replaces the monolithic
Sampler/GWSampler/GWSamplerGNPEclasses with a factorized inference spine: the posterior is sampled by an explicit chain of steps — factors (networks, pins, tables), reparametrizations, and corrections — composed by aChainComposerover a per-eventGWSamplerContext. Every existing workflow (NPE, multi-iteration GNPE with density recovery, importance sampling with synthetic phase) runs on the spine bit-exactly, and the spine's first new physics capability — DINGO-BNS single-step chirp-mass GNPE (Dax et al., Nature 639, 49 (2025)) — runs end-to-end throughdingo_pipeon real GW170817 data.Architecture
dingo/core/factors.py— the domain-agnostic spine:Factor/Reparametrization/TargetCorrectionstep kinds (FlowFactor,DeltaFactor,SampleTableFactor,ProxyOffsetReparam,GibbsBlock),Stage/ChainComposer(fan-out, point-mass and 1:1 multiplicity rules, kind-aware reverse-foldlog_prob), and theComposedSamplerbase.dingo/gw/inference/— split by responsibility:context.py(GWSamplerContext: event data with its network-input, prior, and likelihood views; importance-sampling representations via immutablederive()),steps.py(GNPE factors,SyntheticPhaseFactor, theRAToEventFrame/RAToTrainingFramepair,SpinConventionReparam),sampler.py(GWComposedSampler+ chain builders),scan.py(the chirp-mass scan).Resultcarries no construction knowledge: prior, domain, and likelihood delegate to the context (reconstructed from metadata for from-file results); synthetic phase runs as a chain rooted in the proposal sample table.settings["sampler"]holds the serialized chain description, model paths, and (where applicable) density-recovery and scan recipes.Binary neutron stars
HeterodynePhase, batch-nativefactor_fiducial_waveform) ported from Add heterodyning transforms for BNS chirp mass GNPE #355;d_inner_hphase decomposition (return_rho2opt) ported from Core GNPE extensions for BNS support #354; matched-filter SNR reporting (return_aux_snr) added for the scan.[DeltaFactor(pins) → RAToTrainingFrame → FlowFactor → ProxyOffsetReparam → RAToEventFrame]: single-pass sampling with preserved density (no Gibbs, no density recovery). The pinned values have one owner (the chain), and the heterodyne consumes the proxy throughprepared_data(conditioning=...)— one method with a uniform row-aligned contract serving pins and sweeps alike.--chirp-mass-scan): sweeps the proxy over the training prior on the ordinary chain machinery (grid table root, per-row batched heterodyne, one network pass per block), selects the maximum-likelihood draw, and records the trigger + SNR in provenance.--fixed-context-parametersaccepts the published demo-INI form.Verification
legacy-samplers-final.bns_add_dingo_pipe_maxresearch branch on frozen event data; IS side verified live ≡ from-file bit-exactly.log_probre-evaluated on-device (float32-level agreement); real GW170817 BNS pin and scan runs reproduce the reference evidences within Monte Carlo error (scan trigger 1.19756, snr 33.0, efficiency ~11%); GNPE with density-recovery training andprior-dict-updatesexercised end-to-end on CUDA. Two device leaks in the previously CPU-only table-rooted chain path were found and fixed.Documentation
dingo_analyze_event; a new single-step section) and dingo_pipe (the BNS sampler options; the base-domain likelihood default).build_model_from_kwargsis re-exported fromdingo.core.posterior_modelsso the documented import works. Build locally withuv run --group docs sphinx-build -b html docs/source docs/build.Compatibility and deferred work
compatibility/remove_domain_window_factor.py(unchanged story).compute_likelihoodfast path, embed-once onFlowWrapper, calibration-as-a-factor, per-event time scans for pre-merger networks, CUDA/server realism runs.🤖 Generated with Claude Code