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 module entry point: dependencies, the export list, and theincludeorder for the rest ofsrc/. The functionality itself is split across single-purpose files, listed next.src/data.jl—load_observationsandfreeze_observations, the observation-manifest loader.src/constants.jl— fixed constants, including the published Imperial point estimates (REPORT_SCENARIOS,REPORT_SCENARIOS_CI).src/sampling.jl— NUTS sampling (nuts_sample,fit_parallel) and the AD backend setup.src/renewal.jl— the shared renewal-process helpers:renewal_infections, the delay convolutionsconvolve_delay,convolve_survivalandconvolve_pmf, anddiscretise_censored.src/models/priors.jl,src/models/observations.jl,src/models/joint.jl— the building-block submodels, the observation submodels and the composers. See Model architecture below.src/summaries.jl,src/scoring.jl— summary and comparison tables, and forecast scoring.src/counterfactual.jl— the no-onward-deaths projection (predict_no_onward_deaths).src/forecast.jl— forecast helpers (forecast_reported).src/confirmed_cfr.jl— delay-corrected confirmed-case-fatality-ratio helpers.src/plots.jl— plotting.docs/examples/analysis.jlanddocs/examples/sensitivity.jl— the Literate walkthroughs, split so the expensive fits and the render can fan out across CI runners.analysis.jlcarries the methods, results and one-week-ahead forecast.sensitivity.jlcarries the forecast validation and the comparison/sensitivity analyses. Both load their fits through the shareddocs/examples/_setup.jl.analysis.jlis the main artifact.docs/fits/— the fit-cache machinery:registry.jl(the fit-id list),cache.jl(content-addressed fit caching underlogs/fit_cache),one.jl(fit and cache a single id,task fit),all.jl(fit every model,task fit-all), andlist.jl(print fit ids for the CI matrix).docs/execute.jl— runs one Literate page against the fit cache and writes its markdown, figures and half ofoutput/(used bytask docs-mainandtask docs-sensitivity).docs/make.jl— the Vitepress combine step: copiesREADME.mdtoindex.md, assembles the site from the already-rendered markdown, and builds the bibliography (used bytask docs).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, plusenzyme/,formatter/,jet/andpackage/for the quality checks.external/bdbv-linelist-analysis— git submodule, source of the onset-to-death delay priors.
Running and testing
Taskfile.yml at the repository root wraps the common commands. Run task --list for the full set with descriptions. The ones used day to day:
# Instantiate the package environment (no task wraps this)
julia --project=. -e 'using Pkg; Pkg.instantiate()'
# Fit and cache every model, then render the docs from the cache
task fit-all
task docs
# Fit and cache a single model by id (list ids with
# `julia --project=docs docs/fits/list.jl`)
BVD_FIT_ID=deaths task fit
# Render just the main analysis page from the cache, for fast iteration
task docs-main
task docs-sensitivity
# Format (SciML style) over src/, test/, docs/, scripts/
task format
# Run the full test suite, or skip the quality checks (Aqua/JET/format)
# for a quicker one
task test
task test-quick
# Regenerate the published output CSVs into output/ (no task wraps this)
julia --project=. scripts/run.jlRunning julia --project=. docs/examples/analysis.jl directly instead steps through the full narrative without going through the fit cache, fitting every model inline. This is the slow path. Prefer task fit-all && task docs for anything beyond reading the source.
A build streams per-fit progress by default: every NUTS fit writes logs/<fit>.log (iteration, log-density, divergences) and a TensorBoard run under logs/tensorboard/<fit>/, controlled by BVD_FIT_LOG (all when unset, or progress, tensorboard, none). CI release builds set BVD_FIT_LOG=none. Tail a log for quick liveness, or run task tensorboard to view all fits in the worktree. The logs live under the git-ignored logs/, so each worktree keeps its own. A cached fit lives under logs/fit_cache and is keyed by a content hash of the fit-relevant code and data, so a Turing or dependency version bump invalidates it. Refit rather than debugging a KeyError on a stale chain.
test/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.onset_to_death_modelis the gamma onset-to-death delay, built by convolving two atomicgamma_delay_modeldelays (onset→admission, admission→death) rather than fitting the onset→death delay directly.cfr_modelis the case-fatality ratio.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 and exports_deaths_model (the Uganda imports and import deaths, each an inhomogeneous Poisson process over the dated detection days rather than a rectangular detection window), deaths_model, reported_cases_model, confirmed_cases_model and confirmed_deaths_model (NegBinomial DRC surveillance and laboratory streams), and treatment_flow_model and recovered_model (the isolation-occupancy and recovered-among-confirmed streams).
Composers stitch the blocks into full generative models: exports_only_model, deaths_only_model, cases_only_model, confirmed_only_model, confirmed_deaths_only_model, treatment_only_model, exports_deaths_only_model, exports_joint_only_model (the Uganda export cases and deaths fitted jointly over one shared at-risk prevalence), and bvd_joint (every stream together). 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 write-up prose and markdown. Do not wrap prose at 80 characters. Code, code comments and docstrings keep the 80-character limit and wrap normally. This rule is for prose only.
The shared front matter (title, authors, abstract, scope) is single-sourced in
README.md, up to the<!-- SHARED:END -->marker. Edit it inREADME.mdonly.docs/examples/analysis.jlloads it at build time via a Documenter@evalblock that readsREADME.mdand extracts everything before that marker, 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.6, 0.2); lower = 0)oninv_sqrt_k, following the Stan prior-choice recommendations.Docstrings use DocStringExtensions (
$(TYPEDSIGNATURES)).Two implementation details: the AD backend is Mooncake reverse-mode, and models compose via
~ to_submodel(...).NaN and Inf safe clamps (
safe_nbinomial,eps-flooring of expected counts) guard against extreme NUTS warmup proposals. Keep them when editing the likelihoods.
Analysis report prose
These apply to the narrative prose in docs/examples/analysis.jl, and to write-up prose generally. Use the existing report text as the template for tone. The measured sentence- and paragraph-level rules below were reverse-engineered from a manuscript the maintainers are happy with. The repo-specific rules that follow take precedence where the two disagree. AGENTS.md carries a short version of this section for agent sessions, pointing back here for the full rule. Keep the two in agreement: if this section changes in a way that affects the summary, update AGENTS.md too.
No code references in the narrative. Do not name functions, parameters, files, or
:symbolsin the prose. Describe each quantity in words, and define a derived quantity in words the first time it appears, near its figure or table.Concise and direct. Cut filler and adjectives. Avoid the LLM-indicator words: comprehensive, leverage, robust, framework (when vague), utilise, facilitate, novel, landscape, foster, harness, streamline, pivotal, nuanced, multifaceted, cornerstone, synergy, overarching. Avoid filler and stance markers: very, significant, crucial, essential, clearly, obviously, "it is important to note that". Delete the LLM connective openers
Furthermore,Moreover,Additionally,In conclusion,Overall. Restrict genuine connectives toHowever,Whilst,Yet,Unlike,Similarly,Alternatively,Finally,,Instead.Target 15-30 words per sentence and treat 40 as the ceiling. Exceed it only for a flat, colon-led enumeration, never for nested subordination. Use at most two commas per sentence for internal structure, a third only in a serial list.
Never use a dash as punctuation. Replace it with a full stop, or with parentheses when the aside is a gloss or example. Use a colon only to introduce a list or an expansion, never to join two independent statements. Use a semicolon only to separate list items that themselves contain commas.
Instead of a trailing qualifier (", which means that...", ", thereby enabling..."), cut the comma and start a new sentence with
This,TheseorIt also. Keep any surviving trailing, whichclause to about one sentence in ten, and only where the clause carries the point rather than padding it.Write paragraphs of 2-6 sentences. Open each with its claim, not with context, and close it with a consequence or a limitation, never a restatement of the opening.
Hedge with
can,could,mayorpotential, one hedge per claim. Never stack hedges ("may potentially", "could possibly suggest").Report intervals as sentences, without a leading median. Write the credible interval as a phrase, not a "median (lower, upper)" construction.
Report numbers with their provenance and units. Delete a vague quantity ("a large number of", "significantly more") rather than leave it unsourced.
UK English throughout.
Section and subsection titles are just the title. No descriptive suffix after a title (not "Reproduction number — weekly random walk with intervention ramp", just "Reproduction number"), and no detail-dump in the first sentence after a heading.
Order the methods generatively, infections through to observation endpoints: the infection process first, then the epidemiological processes (delays, case-fatality ratio), then the observation models (surveillance streams before exports), then the joint model.
Define every quantity before it is used. Define the reproduction number before the seeding that relies on it; introduce the initial infection count before describing how it arises; define every symbol and operator (including convolution) the first time it appears. Never use a symbol the reader has not met. Give an equation once in display maths and refer to its symbols inline afterwards; never narrate a displayed equation in words.
Do not repeat. State a convention once (the credible-interval levels, the delay discretisation) and do not restate it per bullet or subsection. Cut sentences that duplicate earlier content.
Cite the source of each prior and carry the uncertainty the source reports. When a source gives a distribution with uncertainty (a shape and scale with intervals), propagate that, not a self-assigned weakly-informative spread. Do not write "with an assumed weakly-informative spread" repeatedly. If a prior is our own choice, say so plainly ("we use a prior of ...").
State assumptions as assumptions ("we assume a single seed case", "we assume the response scale-up takes about three weeks"). Do not assert a false rationale for a modelling choice (not "a Poisson because the count is small").
Do not editorialise or justify priors in the narrative (not "a diffuse prior would let the background absorb the whole stream"). State what the model does.
Methods belong in the methods. Do not leave model description (the intervention model, the counterfactual, the forecast, the evaluation) in the results. Move it to the methods and keep the results to findings.
No project or issue history in the narrative: no PR or issue numbers, no "we previously used X", "this was changed to Y", "after refactoring". State the current design only. A difference from work being replicated, or a limitation of an external dependency, is the exception, written as a present-tense fact. An issue number may also stay where the issue is the record for something still unresolved, such as a workaround pointing at the open root cause it does not fix, or a data decision whose reasoning lives in the issue. The test is whether the number still tells the reader something they cannot get from the code. "Tracked by #495" earns its place; "changed in #495" does not.
Label quantities accurately. Do not call suspected cases onsets, and prefer "current cumulative" over "final cumulative".
For a latent quantity (infections, onsets, deaths) report the modelled estimate without overlaying observed data that sits downstream of unmodelled processes.
Plots use the same credible-interval ribbons as the tables, not a bare median, and show only the period being estimated rather than greying out the rest.
Model code shown in the report is clean. Strip working comments before it is displayed.
Flag a future improvement as a GitHub issue, not a buried caveat in the prose.
Bullet lists, bold and italics are advisory rather than enforced here, since this is a technical report rather than a journal manuscript. Do not add new bullet lists or bold to the narrative prose, and do not restructure an existing list that carries genuinely parallel content, but do strip bold or italics used for mid-sentence emphasis.
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.