Skip to content

[5/5] Sampled leak (--leak-prior) and shared or hierarchical error rates (--error-model) - #145

Open
m-murphy wants to merge 12 commits into
stack/4-exact-speedupsfrom
stack/5-sampled-leak-error-models
Open

m-murphy wants to merge 12 commits into
stack/4-exact-speedupsfrom
stack/5-sampled-leak-error-models

Conversation

@m-murphy

@m-murphy m-murphy commented Nov 8, 2026 •

Copy link
Copy Markdown
Member

Summary

The leak and the observation error rates can be sampled.

--leak-prior loguniform[:LO,HI] | exponential[:RATE]
    the leak becomes a Run scalar (transmission_leak), starting at --transmission-leak
    exponential is truncated to (0, 0.5)
--error-model infection | shared | hierarchical
    infection     each Infection its own rates, prior Gamma(10, 0.001) (default, as before)
    shared        one false-positive and one false-negative rate for every Infection, priors Gamma(2, 0.01) and Gamma(2, 0.02)
    hierarchical  each Infection's rates from Gamma(shape, mean / shape); efp_mean, efp_shape, efn_mean, efn_shape sampled
  • ModelChoice (its own ModelChoice.h/.cpp) carries the Run recipe's model choices to the State and checkpoint::restore as one value, and is the one place leak priors and error models are parsed. A number in a leak prior must parse whole: loguniform:1e-3x,0.1 is refused.
  • The start: a --transmission-leak outside the prior's range (the default 0, say) is moved just inside it, and a notice says so. The manifest records the start used. On resume, the leak given and the one recorded compare as the starts they make, so the original command still resumes, and so does a manifest written before this change.
  • On restore, a leak the prior holds stays bit-identical, so a resumed chain goes on as an uninterrupted one would. Only a leak outside the prior's range is moved, for example one from a Run with another prior.
  • The Transmission memo follows a sampled leak: a change puts its results aside, and a rejected step brings them back without recomputing.
  • On resume, leak priors and error models compare by meaning (loguniform = loguniform:1e-07,0.1; infection = not given).
  • The manifest records leak_prior and error_model. A manifest without them reads as a fixed leak and per-Infection rates.
  • plasmotrackr: pt_convergence() gives R-hat for transmission_leak and efp_*/efn_* when the Runs have them. pt_read() refuses to pool Runs that differ in leak prior or error model, compared by meaning. With a leak prior, Runs that differ only in their recorded start still pool.
  • Glossary: Error model, Leak prior and Model choice.

Evidence

  • Before: the leak and error-rate structure are fixed.
    After: engine tests 344 pass (1 skipped without OpenMP), among them:

    • SampledLeakTest: the cached log posterior matches a Model built from scratch, with leak steps accepted and rejected, and with frequency ids forgotten mid-step; the leak walk matches its exact conditional; a restored leak inside the prior is unchanged; the start; whole-number parsing;
    • RunRecipe: the manifest records the start; resuming compares starts;
    • prior-recovery tests for the hierarchical rates.

    plasmotrackr tests 3732 pass; golden copies in sync; Golden Stores unchanged.

  • On six simulated studies with family moves, the sampled log-uniform leak gives R-hat 1.03 and edge AP 0.666.

Merge Danger

Door: two-way

Both options are off by default, and Runs without them have the Sampler set they had before.

Blast Radius: small

  • Two new manifest keys.
  • With a leak prior, transmission_leak in the manifest is the start used, which can differ from --transmission-leak.
  • --start-from needs the same Sampler set as the source Run (the README says so).

…one value

The null model and the leak reach the State and checkpoint::restore as one ModelChoice instead of a growing list of arguments. The positional constructors stay for tests and tools.
…rror rates (--error-model)

--leak-prior makes the leak a Run scalar (transmission_leak) under a log-uniform or exponential prior, starting
at --transmission-leak. The Transmission memo follows it: a change puts its results aside and starts afresh, and a
rejected step brings them back, so a rejection recomputes nothing.

--error-model shared gives every Infection one false-positive and one false-negative rate, priors Gamma(2, 0.01)
and Gamma(2, 0.02); hierarchical draws each Infection's rates from Gamma(shape, mean / shape), with the mean and
shape sampled (Run scalars efp_mean, efp_shape, efn_mean, efn_shape). Both are part of the Run recipe and the
manifest; a manifest without them reads as a fixed leak and per-Infection rates.

SampledLeakTest checks the cached log posterior against a Model built afresh, with leak steps accepted and
rejected, and the leak walk against its exact conditional.
… tests

Every error rate's walk keeps it in [ERROR_RATE_LOWER, ERROR_RATE_UPPER], so under the hierarchical model each rate's
Gamma(shape, mean / shape) prior is truncated there and divided by its mass inside. Without that, the bounds weighed
the mean and shape by that mass once per Infection, against small shapes and large means.

With the likelihood off the Sampler set recovers the leak's and the hierarchical means' priors, and the truncated
rate prior integrates to 1 over the range for every mean and shape tested.
…s compare by meaning on resume

A Run started from a checkpoint of a Run that also sampled the leak, under another prior, moves the saved leak
inside the new prior's range, as a new State starts it. The manifest records a leak prior in one form per prior and
an error model in one form per model (empty for infection), and resuming compares the given option in that form,
so loguniform resumes a Run started with loguniform:1e-7,0.1, and --error-model infection one started without it.
The --transmission-leak help says it is the starting leak under --leak-prior.
…ce in config.h

gammaLogDensity() is the formula GammaLogPDF evaluates, and the hierarchical rate prior and the
population means' prior use it. The wide Gamma priors (shared rates, hierarchical means) and the
hierarchical mean and shape ranges become WIDE_* and HIERARCHICAL_* constants.
…its prior and is moved only when outside it

- The leak prior and error model parsers and canonical forms leave State.cpp for ModelChoice.h/.cpp,
  the one place they are parsed; RunRecipe.cpp includes that instead of State.h. LeakPrior names its
  fields (lo, hi, rate), and every number in a description must parse whole (loguniform:1e-3x,0.1 is
  refused).
- LeakPrior::startAt: a leak the prior holds (strictly inside its range) is the start; any other is
  moved just inside. A new Run records the start in the manifest and says so in a notice when it
  isn't the leak given. Resuming compares the leak given and the one recorded as the starts they
  make, so the original command, or a manifest written before this, still resumes.
- keepLeakInsidePrior() on restore leaves a leak the prior holds bit-identical, so a resumed chain
  goes on as an uninterrupted one would.
- One State field for the leak, the Parameter transmissionLeak (leakSize and the stale double go);
  the memo's leakParameter() is sampledLeak().
- Test: forgetting frequency ids while a leak step is pending leaves the results put aside valid
  (they carry their own ids and generation).
…ampled hyperparameters and the memo's second lifetime rule
…l; with a Leak prior the recorded leak is only the start
@m-murphy
m-murphy force-pushed the stack/4-exact-speedups branch from 0f82493 to e006cc0 Compare November 8, 2026 02:04
@m-murphy
m-murphy force-pushed the stack/5-sampled-leak-error-models branch from 9f59208 to 446d5d3 Compare November 8, 2026 02:04

This branch has not been deployed

No deployments
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.

1 participant