Contributing
Issues and pull requests are welcome at epiforecasts/BVDOutbreakSize. This page covers how the project is laid out, how to run it, and the conventions to follow when changing it.
Repository layout
src/BVDOutbreakSize.jl— the package: data loading (load_observations), NUTS sampling (nuts_sample), the shared Gauss-Legendre integrators (integrate,expected_deaths,integrate_cumulative,integrate_exports_deaths), summary and comparison tables, plotting, the no-onward-deaths projection (predict_no_onward_deaths) and forecast helpers (forecast_reported). The published Imperial point estimates live here asREPORT_SCENARIOS.docs/examples/analysis.jl— the Literate walkthrough that is the analysis. It defines the Turing submodels and composers, runs the fits, and writes every output. This is the main artifact.docs/make.jl— DocumenterVitepress build. CopiesREADME.mdtoindex.md, executes the literate toanalysis.md, and builds the bibliography.data/observations.toml— single source of truth for observation data (case and death counts, traveller volumes, sources). Loaded viaload_observations()and never hardcoded. Update this one file for a new situation report and the analysis picks it up. The literate re-binds its observationconsts from the loaded TOML, so the package constants are defaults only.scripts/run.jl— regenerates published results by including the literate and writes CSVs tooutput/.test/— one file per feature, driven bytest/runtests.jl.external/bdbv-linelist-analysis— git submodule, source of the onset-to-death delay priors.
Running and testing
There is no Taskfile. Use the julia --project commands:
# Instantiate the package environment
julia --project=. -e 'using Pkg; Pkg.instantiate()'
# Run the analysis
julia --project=. docs/examples/analysis.jl
# Regenerate the published output CSVs into output/
julia --project=. scripts/run.jl
# Run the full test suite
julia --project=. -e 'using Pkg; Pkg.test()'
# Build the docs (executes the literate, HTML in docs/build/)
julia --project=docs -e 'using Pkg; \
Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()'
julia --project=docs docs/make.jltest/runtests.jl includes each test/test_*.jl. To iterate on one file, run it inside a REPL after using BVDOutbreakSize, or temporarily comment out the others in runtests.jl.
CI runs the test suite (.github/workflows/test.yml) and builds the docs, publishing output/ as a GitHub Release on each push to main (.github/workflows/docs.yml).
Model architecture
The model is assembled from small, swappable Turing submodels rather than one monolithic block (the build-up is drawn as a flowchart on the Analysis page). There are three layers.
Building-block submodels, one per parameter family, each owning its own priors:
exponential_growth_modelsamples the doubling timeτand the doubling-time multiplierm = T/τ, notτandTdirectly, to break theC(T) = exp(rT)ridge.delay_modelis the gamma onset-to-death delay.cfr_modelis the case-fatality ratio.detection_window_modelis the export detection windoww.surveillance_dispersion_modelsamples on the1/√kscale.pooled_ascertainment_modelpartially pools the DRC and Uganda reporting fractionsp_drcandp_ugandaon the logit scale.
Observation submodels, one per data stream, each taking the growth state, adding its forward integral and likelihood: exports_model (Poisson), deaths_model (NegBinomial), cases_model (NegBinomial), and exports_deaths_model (Poisson).
Composers stitch the blocks into full generative models: exports_only_model, deaths_only_model, cases_only_model, exports_deaths_only_model, imperial_only_model (exports and deaths, the Imperial joint configuration), and bvd_joint (all four streams). Each composer conditionally includes only the likelihoods for the streams it carries. A single-stream composer never instantiates the other observation submodels, so a discrete stream is never left sampled, which would trip Turing's model check. Pass a stream as missing to drop its likelihood; bvd_joint with all streams missing is the generator used for the prior and posterior predictive checks.
Conventions
Maximum 80 characters per line of code.
One sentence per line in prose and markdown; do not wrap prose at 80 characters.
The abstract is single-sourced in
README.md, wrapped in<!-- ABSTRACT:START -->/<!-- ABSTRACT:END -->markers. Edit the abstract inREADME.mdonly.docs/examples/analysis.jlloads it at build time via a Documenter@evalblock that readsREADME.mdand regex-extracts the text between those markers, so do not duplicate it into the analysis page.Table-construction and other setup code in
analysis.jlis hidden inside<details>dropdowns via#md # @raw htmlblocks; the bare result object follows (with#hide) so only the output renders.The surveillance dispersion prior is a half-normal
truncated(Normal(0, 1); lower = 0)oninv_sqrt_k.Docstrings use DocStringExtensions (
$(TYPEDSIGNATURES)).The AD backend is Mooncake reverse-mode; integrals use Gauss-Legendre quadrature (
DEATH_INTEGRAL_ALGwithn = 64,CUMULATIVE_INTEGRAL_ALGwithn = 32); models compose via~ to_submodel(...). The deaths-among-exports CDF is written as an inner integral of the density because the reverse-mode AD backend does not support the gamma CDF shape-parameter derivative.NaN and Inf safe clamps (
safe_nbinomial,eps-flooring of expected counts) guard against extreme NUTS warmup proposals; keep them when editing the likelihoods.
Pull requests
mainis branch-protected; changes go through pull requests.Run the test suite before opening a pull request.
Add a bullet to the News page under
Unreleasedfor any user-visible change.