Skip to content

API

Public functions and constants exported by BVDOutbreakSize.

BVDOutbreakSize.BASELINE_FIT Constant
julia
BASELINE_FIT

Value of a fit column identifying a row as the persistence baseline, which is no model's output.

source
BVDOutbreakSize.CHAMLA_CONFIRMED_CENTRAL Constant
julia
CHAMLA_CONFIRMED_CENTRAL

Central-scenario cumulative laboratory-confirmed case projection of Chamla et al. (Chamla et al., 2026) (WHO Regional Office for Africa, Lancet Infectious Diseases, 2026), as (date, median, lower_90, upper_90) tuples from their Table 1 (mean accepted R₀ = 1.71). Their stochastic SEIRD ensemble is calibrated by simulation filtering to 598 cumulative confirmed cases on 8 June 2026, with the reporting fraction fixed at 1.0, so these are projected confirmed cases (a floor on the true size) rather than the ascertainment-corrected cumulative cases that this package's C_T and McCabe et al. estimate. The 8/10 June row is their calibration anchor; the dates from 24 June on are forward projections. The bounds are 90% prediction intervals.

source
BVDOutbreakSize.CHAMLA_CONFIRMED_W12 Constant
julia
CHAMLA_CONFIRMED_W12

Chamla et al. (Chamla et al., 2026) week-12 (24 June 2026) cumulative confirmed-case projection under their three transmissibility scenarios, as (scenario, median, lower_90, upper_90) tuples (low R₀ = 1.42, central R₀ = 1.71, high R₀ = 2.08). Week 12 is the forward horizon closest to this analysis's current cut-off, so the scenario spread sits beside the observed confirmed count and our matched-date projection. The bounds are 90% prediction intervals; the same estimand caveat as CHAMLA_CONFIRMED_CENTRAL applies (these are confirmed cases, not ascertainment-corrected total cases).

source
BVDOutbreakSize.FROZEN_FIT Constant
julia
FROZEN_FIT

Value of a fit column identifying a row as a frozen-fit forecast, the joint model re-fit and evaluated at a past cut-off (forecast_frozen.csv). That asset carries no fit column of its own, so the label is applied at scoring time.

source
BVDOutbreakSize.ITURI_DAILY_TRAVEL Constant
julia
ITURI_DAILY_TRAVEL

Default prior mean for the daily outbound traveller volume from Ituri Province across seven points of entry.

source
BVDOutbreakSize.ITURI_DAILY_TRAVEL_SD Constant
julia
ITURI_DAILY_TRAVEL_SD

Default prior SD for the daily outbound traveller volume, covering point-of-entry-to-point-of-entry variation and reporting uncertainty in the underlying mobility survey.

source
BVDOutbreakSize.ITURI_POPULATION Constant
julia
ITURI_POPULATION

Source population for the Ituri Province (McCabe et al., Table 1).

source
BVDOutbreakSize.JOINT_FIT Constant
julia
JOINT_FIT

Value of a fit column identifying a row as the joint model's forecast or estimate. A FROZEN_FIT row is drawn in the same role, being the same model re-fit at a past cut-off rather than a separate fit.

source
BVDOutbreakSize.M_PRIOR_BASE Constant
julia
M_PRIOR_BASE

Base centre (at M_PRIOR_BASE_DATE) for the ADVANCING doubling-count prior centre used by the backfill fits via m_prior_centre: m_0 = M_PRIOR_BASE + (as_of − M_PRIOR_BASE_DATE) / M_PRIOR_DOUBLING_DAYS. The doubling count m counts ONLY the cryptic-phase doublings (the origin to the renewal-process start): the cryptic duration is m·τ and the total outbreak age is T = m·τ + τ_obs, with τ_obs the observed window. The genetic seeding bound pulls the lower tail of the outbreak age to sit at or before the most recent common ancestor. The MAIN fit's m prior centre is set directly in exponential_growth_model from the field-intelligence + genetic elicitaiton (#533); whether the backfill's centre should advance at all is tracked by #534.

source
BVDOutbreakSize.M_PRIOR_BASE_DATE Constant
julia
M_PRIOR_BASE_DATE

Base date for the doubling-count prior centre: McCabe et al.'s first report (18 May 2026), whose Method 2 central scenario of 501 cases implies m ≈ log2(501) ≈ 9. Used by m_prior_centre.

source
BVDOutbreakSize.M_PRIOR_DOUBLING_DAYS Constant
julia
M_PRIOR_DOUBLING_DAYS

Median doubling time (days) for the size and growth priors, from the BEAST X analysis (mbalaplacide2026, Exponential growth model, 11.7 d, 95% HPD 6.8-17.5). The doubling-count prior centre advances by one doubling per M_PRIOR_DOUBLING_DAYS of elapsed time to the cut-off.

source
BVDOutbreakSize.RENEWAL_START_LEAD Constant
julia
RENEWAL_START_LEAD

Days the renewal start (the day the reproduction-number walk starts, where the analytic cryptic phase hands off to the recursion) sits AFTER the genetic TMRCA day, past the TMRCA's molecular-clock uncertainty. Placing the renewal start a 14-day lead after the TMRCA — rather than exactly on it — leaves the observed span τ_obs = n − renewal_start strictly shorter than tmrca_days, so the genetic censored bound on the total age T = m·τ + τ_obs stays informative (it pulls the origin to sit at or before the MRCA, bounding the cryptic duration m·τ from below). Two weeks past the TMRCA leaves room for the TMRCA's own molecular-clock uncertainty before sustained transmission is treated as confidently established.

source
BVDOutbreakSize.REPORT_SCENARIOS Constant
julia
REPORT_SCENARIOS

Published point estimates of cumulative cases C_T from McCabe et al. (Imperial College London, 20 May 2026 update), as (label, value) tuples in the order they appear in Tables 1 and 2. These are the scenario means; the matching 95% confidence intervals are carried by REPORT_SCENARIOS_CI.

source
BVDOutbreakSize.REPORT_SCENARIOS_CI Constant
julia
REPORT_SCENARIOS_CI

Published McCabe et al. scenario estimates WITH their reported 95% confidence intervals, for three vintages: the 18 May 2026 report (McCabe and others, May 2026), the 20 May 2026 update (McCabe and others, May 2026) and the peer-reviewed Lancet Infectious Diseases publication (McCabe et al., 2026), whose inputs are as of 27 May 2026. Each entry is a (date, label, mean, lower, upper) tuple, where date is that vintage's own cut-off date. Method 1 (geographic spread from exported cases and travel volume) is unchanged between the two Imperial reports, so it is recorded once under the 20 May vintage. Method 2 (back-calculation from deaths) differs between the vintages: the 18 May report used 88 deaths and CFR 24/30/40%, the 20 May update used 131 deaths and the corrected CFR 26/33/40%. Confidence intervals are exact negative-binomial (Method 1) and Poisson likelihood-profile (Method 2), as reported in Tables 1 and 2 of each report.

The 27 May Lancet vintage uses 240 deaths and three Uganda imports, and varies the epidemic doubling time T_d (7/10/14 d) for both methods rather than the earlier geographic window w / onset-to-death τ; its back-calculation fixes the onset-to-death gamma (mean 11.37 d, SD 5.41) and assumes 30% of deaths are attributable to Ebola. The published paper SWAPS the method numbers (its "method 1" is the back-calculation and its "method 2" the geographic spread); we keep this package's convention (M1 = geographic spread, negative-binomial CIs; M2 = back-calculation, Poisson CIs), which the paper's reported CI types confirm.

source
BVDOutbreakSize.RT_INTERVENTION_RAMP Constant
julia
RT_INTERVENTION_RAMP

Time scale in days of the logistic ramp over which the outbreak-response intervention takes effect on R_t, centred on the breakpoint (sigmoid_ramp). A ramped rather than instantaneous step: the response damps transmission over weeks, not on a single day. The single source of truth for this ramp, referenced by both the model (the rt_walk_model prior and sigmoid_ramp) and every reconstruct_rt caller that rebuilds the daily R_t off the chain, so the reconstruction cannot drift from the value the model fitted.

source
BVDOutbreakSize.RT_WALK_LEAD Constant
julia
RT_WALK_LEAD

Days BEFORE the first situation report (breakpoint) at which the reproduction-number random walk is allowed to start moving, rather than holding R_t flat at R0 right up to the report. A month lets the walk capture the transmission dynamics in the period leading up to the first report — the response decline can begin before the outbreak is first reported — while staying floored at the renewal start so the walk never precedes the seeded trajectory.

source
BVDOutbreakSize.accumulate_occupancy Method
julia
accumulate_occupancy(
    A_bvd::AbstractVector,
    A_bg::AbstractVector,
    deaths::AbstractVector,
    recover::AbstractVector,
    ruleout::AbstractVector,
    κ::Real,
    conf_hazard::AbstractVector
) -> NamedTuple{(:demand, :O_bvd, :O_conf, :O_susp, :abscond), <:NTuple{5, Any}}
julia
accumulate_occupancy(A_bvd, A_bg, deaths, recover, ruleout, κ, conf_hazard)

Treatment-centre occupancy built as a forward day-by-day running balance of admission, discharge and abscond events, carved into a confirmed and a suspect sub-stock by a confirmation overlay.

The clinical layer is label-independent and carried as two sub-stocks that sum to the total demand by construction. The occupied BVD true-case stock O_bvd accumulates A_bvd minus the BVD discharges (deaths + recover) and its share of the abscond outflow; the occupied non-case stock O_bg accumulates A_bg minus the rule-out exits (ruleout) and its share of the abscond outflow:

and the total demand is their sum D(t) = O_bvd(t) + O_bg(t) exactly. The abscond outflow κ · O_susp(t-1) drains the suspect (unconfirmed) pool only — confirmed patients do not abscond — and is split proportionally between its unconfirmed-BVD portion O_bvd(t-1) − O_conf(t-1) and the non-case portion O_bg(t-1), the two parts of the suspect pool O_susp(t-1):

The confirmation overlay relabels occupied true cases at the daily hazard conf_hazard(t) = τ_test · p_pos(t) borrowed from the lab pipeline, so the confirmed sub-stock is the confirmed subset of O_bvd, never an extra compartment, draining at the confirmed share of the BVD discharges:

so O_conf ≤ O_bvd ≤ D. The suspect sub-stock is the remainder O_susp(t) = D(t) − O_conf(t) = (O_bvd(t) − O_conf(t)) + O_bg(t) ≥ 0, the not-yet-confirmed BVD occupancy plus the non-case rule-out occupancy, so O_conf + O_susp = D closes without a clamp. Each sub-stock is floored at zero, O_conf is clamped into [0, O_bvd], and the abscond denominator O_susp(t-1) is floored with eps so a zero suspect pool gives a zero split. Returns (; demand, O_bvd, O_conf, O_susp, abscond), each a length-n vector.

source
BVDOutbreakSize.admission_headroom Method
julia
admission_headroom(
    adm_days,
    adm_obs,
    capacity_history,
    isolation_history
) -> Any
julia
admission_headroom(adm_days, capacity_history, isolation_history)

Per-admission-day right-censoring bound for the admissions likelihood, built from data alone, the admissions analogue of censoring_cap. Each admission day takes the recorded free-bed headroom C_fix(t) − occupancy(t-1): the nearest recorded implied-capacity value (capacity_history) less the previous day's observed occupancy (isolation_history, forward-filled). Days before any occupancy record and days with no recorded capacity get a large no-op bound. The bound is kept strictly above each day's observed admissions (floored at obs + 0.5), so a day whose admissions exceeded the headroom is scored as a plain count rather than on the censoring boundary. Returns a length-length(adm_days) Float64 vector.

source
BVDOutbreakSize.background_cfr_model Method
julia
background_cfr_model(
;
    cfr_prior
) -> DynamicPPL.Model{typeof(background_cfr_model), (), (:cfr_prior,), (), Tuple{}, Tuple{Distributions.Beta{Float64}}, DynamicPPL.DefaultContext, false}

Background case-fatality ratio cfr_bg for the non-BVD suspected-death background (deaths_model). The renewal joint ties the non-BVD suspected-death background to the (already identified) non-BVD suspected- CASE background λ_bg (test_positivity_model) rather than giving deaths a free, outbreak-size-degenerate background rate of their own: the per-day background suspected deaths are cfr_bg · λ_bg_v, a background CFR applied to the per-day non-BVD suspected-case rate. A non-BVD suspected case (other severe febrile or haemorrhagic illness that meets the suspect definition) carries its own fatality risk, and cfr_bg is the share of that background pool that is reported as a suspected death.

Tying the death background to the case background removes the degeneracy that keeps a free λ_bg_death switched off: the case background is pinned by the laboratory positivity link, so scaling it by cfr_bg gives the death background a level and time profile without a second free rate competing with outbreak size. The default Beta(2, 6) (mean ≈ 0.25, 90% ≈ 0.05–0.52) is weakly informative and centred below the BVD CFR (non-BVD suspect illness is on average less lethal than BVD). Pass cfr_prior to override. Returns (; cfr_bg).

source
BVDOutbreakSize.background_pooling_model Method
julia
background_pooling_model(
;
    pooling_prior
) -> DynamicPPL.Model{typeof(background_pooling_model), (), (:pooling_prior,), (), Tuple{}, Tuple{Distributions.Truncated{Distributions.Normal{Float64}, Distributions.Continuous, Float64, Float64, Nothing}}, DynamicPPL.DefaultContext, false}

Shared pooling SD σ_bg for the per-vintage background random effect (background_re_model). Sampled once at the composer level and passed to both the suspected-case and suspected-death backgrounds, so the two streams share one time-variation scale rather than each estimating its own from few vintages. The prior is a tight half-normal truncated(Normal(0, 0.3); lower = 0), deliberately small: the background is degenerate with outbreak size, so a wide random effect would let individual windows absorb arbitrary suspected counts and re-open the second posterior mode in which the background explains the majority of suspected cases. Regularising σ_bg toward zero keeps the time variation a perturbation of the informative scalar baselines. Returns (; σ_bg).

source
BVDOutbreakSize.background_re_model Method
julia
background_re_model(
    nv::Integer,
    σ_bg::Real;
    baseline_prior
) -> Any

Per-vintage non-BVD background rate as a partially-pooled, non-centred random effect, the time-varying generalisation of the scalar λ_bg / λ_bg_death. Used by the suspected-case (reported_cases_model) and suspected-death (deaths_model) streams when their background_re switch is on. The same non-BVD reporting environment plausibly drives both streams, so the two backgrounds can share this submodel's hyperparameters.

The baseline λ_mu is the scalar background rate on its natural half-normal scale, with the same informative default as the scalar λ_bg (truncated(Normal(0, 1.0); lower = 0) for cases; pass a tighter baseline_prior for deaths). The per-vintage rate is a multiplicative log-normal deviation from this baseline,

with σ_bg the pooling SD, passed in rather than sampled here so the suspected-case and suspected-death streams can SHARE one pooling SD (the same non-BVD reporting environment drives both); see background_pooling_model, which samples it once at the composer level. The deviation is multiplicative so the per-vintage rate stays positive without a clamp and σ_bg → 0 recovers the scalar baseline exactly (every λ_v = λ_mu). Each stream still samples its own baseline λ_mu and per-vintage deviations z. nv is the number of vintage windows. Returns (; λ, λ_mu, σ_bg, z) with λ a length-nv vector of per-vintage rates.

source
BVDOutbreakSize.background_walk_model Method
julia
background_walk_model(
    n::Integer,
    σ_rw::Real;
    onset,
    onset_ramp,
    week,
    baseline_prior
) -> Any

Non-BVD background rate as a SMOOTH weekly lognormal random walk over the surveillance window, the replacement for the per-vintage step random effect (background_re_model). The log-rate follows a non-centred random walk on WEEKLY knots and is linearly interpolated to the daily grid, the same parameterisation as the reproduction-number walk (rt_walk_model): the background is a slow drift, so a knot per week carries the time variation with far fewer innovations than a daily walk, which avoids the high-dimensional funnel a daily walk over a long window opens. The series is gated to zero before the surveillance onset — the non-BVD background does not exist before surveillance began — and ramps in over the first onset_ramp days of the window. With knot values \log\lambda and knot days d,

σ_rw (passed in, shared across the suspected-case and suspected-death streams via background_pooling_model) is the per-knot innovation SD on the log scale; a TIGHT prior keeps the background fairly CONSTANT (a gentle drift, not week-to-week jumps), which both regularises the well-known background/outbreak-size degeneracy (closing the high-background second posterior mode that breaks convergence) and keeps the series smooth (so a death background scaled from it carries no steps). Knots run only over the surveillance window [onset, n], so the number of innovations is small. onset ≤ 1 runs it over the whole grid. Pass week to change the knot spacing. Returns (; λ, λ_mu, σ_bg) with λ the length-n daily series (zero before onset).

source
BVDOutbreakSize.bed_capacity_model Method
julia
bed_capacity_model(
;
    capacity_prior
) -> DynamicPPL.Model{typeof(bed_capacity_model), (), (:capacity_prior,), (), Tuple{}, Tuple{Distributions.LogNormal{Float64}}, DynamicPPL.DefaultContext, false}

Isolation/treatment-bed capacity for the supply-limited occupancy stream (treatment_flow_model). Samples the number of beds available, capacity, the ceiling the latent bed demand saturates against. Bed occupancy has been supply-driven (demand has outstripped supply, with occupancy catching up as capacity is expanded), so the modelled occupancy is the demand passed through a soft cap at capacity rather than tracking demand directly.

The default LogNormal(log 450, 0.42) is weakly informative and positive by construction (no truncation boundary), with median 450 and a ≈0.44 coefficient of variation, centred on the bed count implied by the reported occupancy rates (the "Taux d'occupation" gives capacity = occupancy / rate ≈ 400–452 over 9–13 June). The capacity is identified by the implied-capacity series the isolation submodel fits, so the prior only has to bracket it. A single national capacity is a limitation: it cannot represent local saturation (one province full while another has slack), which is the level the supply constraint operates at, and it averages over a growing capacity. Pass capacity_prior to override. Returns (; capacity).

source
BVDOutbreakSize.bed_capacity_walk_model Method
julia
bed_capacity_walk_model(
    n::Integer;
    start,
    week,
    baseline_prior,
    innovation_prior
) -> Any

Time-varying isolation/treatment-bed capacity over the daily grid, a multiplicative random walk: the supply-limited occupancy stream (treatment_flow_model) uses C(t) as the ceiling the latent bed demand saturates against on each day. Capacity is not fixed — beds are being added (SitRep 030 records mattress and bed deliveries and new treatment centres opening) — so a single scalar capacity (bed_capacity_model) cannot track the growth or be projected forward; the walk does both.

The walk is a non-centred cumulative log-deviation from a baseline bed count C0 on WEEKLY knots, linearly interpolated to the daily grid (the same parameterisation as the reproduction-number and background walks): with knot values \log C and knot days d, C(t) = C0 · exp(\text{interp}(σ_cap · cumsum(z))) with z ~ Normal(0, 1) per knot and a tight innovation SD σ_cap, keeping capacity a gentle drift rather than per-day jumps. Knots need far fewer innovations than a daily walk, avoiding the high-dimensional funnel. The baseline carries the same weakly-informative LogNormal(log 450, 0.42) prior as the scalar model (median 450 beds, ≈0.44 CV), so C0 is sampled on the log scale and the whole capacity log C(t) = log C0 + walk is fully log-scale; the implied-capacity series the isolation submodel fits pins C(t) on the days a rate is published.

Knots run only from start, the first day with occupancy or capacity data; capacity is flat at C0 before it. Off-window capacity carries no likelihood, so walking it adds unidentified innovations that leave the posterior poorly conditioned, and start keeps the knots to the days the data speaks to. Pass start = 1 for knots over the whole grid, or week to change the knot spacing. A single national capacity remains a limitation: it cannot represent local saturation (one province full while another has slack), the level the supply constraint actually operates at. Pass baseline_prior / innovation_prior to override. Returns (; C, C0, σ_cap) with C a length-n vector.

source
BVDOutbreakSize.bias_sample Method
julia
bias_sample(
    observed::Real,
    predicted::AbstractVector{<:Real}
) -> Any

Sample-based forecast bias for a single observation against a predictive sample, following scoringutils::bias_sample. With n_lt and n_eq the counts of predictive draws strictly below and exactly equal to the observation, the bias is

   

over the m draws. It lies in  : negative when the predictive distribution sits below the observation (under-prediction), positive when it sits above (over-prediction), and zero when the observation falls at the predictive median. The equal-count term handles ties in count data so a mass of draws exactly at the observation does not bias the score.

source
BVDOutbreakSize.bin_increments Method
julia
bin_increments(
    daily::AbstractVector,
    days::AbstractVector{<:Integer}
) -> Any

Modelled between-vintage increments of a daily series daily, summed directly into the bins delimited by the vintage day indices days (1-based into the grid, ascending). The first increment is the cumulative count up to the first vintage day (sum(daily[1:days[1]])); each later increment is the inter-vintage sum sum(daily[days[i-1]+1:days[i]]), so only the first bin needs the cumulative. This avoids the cumulative-then-difference round trip — the daily series is binned once into the quantities the likelihood scores. Day indices are clamped to the grid. Pure and AD-transparent; the output element type follows daily.

source
BVDOutbreakSize.break_step_centres Method
julia
break_step_centres(
    window_days,
    increments,
    break_days,
    gross
) -> Tuple{Vector{Int64}, Vector{Float64}}
julia
break_step_centres(window_days, increments, break_days, gross)

Break days that land on a scored window, paired with the DATA-DERIVED centre for each one's fitted step. The centre is observed increment − gross: the part of that vintage's step the report itself attributes to integrating a harmonised provincial base rather than to the day's own notifications, where gross is the printed 24h new-confirmed count. On 22 July 2026 that is 369 − 97 = 272 cases and 236 − 62 = 174 deaths.

Centring on published data rather than zero is what makes the step identifiable: a zero-centred step with a wide prior has to discover the harmonisation magnitude from a single observation, whereas the report states it. The sampled deviation around the centre then carries only the residual uncertainty about how much of the discrepancy is truly retrospective.

Break days not matching a window are dropped so no inert step is sampled. A missing/short gross is treated as a gross of zero, so the centre becomes the WHOLE increment: the entire vintage step is attributed to the artefact. That is the conservative fallback rather than a neutral one, and it is deliberate — the de-anchor drops the day's positivity denominator, and a step centred near zero over a de-anchored day is the pathological configuration (measured on confirmed_only_model: 94 divergences and a min bulk ESS of 15, against 20 and 522 with no break day, because nothing absorbs the backlog and the fit books it as incidence). Supply the printed count so the split is data-derived; omitting it errs towards artefact, never towards incidence.

increments may itself be missing, the generator path where no cumulative counts are supplied to difference: the day still gets a step, so a predictive keeps the fitted chain's dimensions, but there is no published discrepancy to centre it on and the centre falls back to zero. Returns (days, centres), both possibly empty.

source
BVDOutbreakSize.bvd_joint Function
julia
bvd_joint(
    n::Integer,
    exported_cases::Union{Missing, Integer},
    total_deaths::Union{Missing, Integer};
    ...
) -> Any
bvd_joint(
    n::Integer,
    exported_cases::Union{Missing, Integer},
    total_deaths::Union{Missing, Integer},
    reported_cases::Union{Missing, Integer};
    ...
) -> Any
bvd_joint(
    n::Integer,
    exported_cases::Union{Missing, Integer},
    total_deaths::Union{Missing, Integer},
    reported_cases::Union{Missing, Integer},
    exports_deaths::Union{Missing, Integer};
    ...
) -> Any
bvd_joint(
    n::Integer,
    exported_cases::Union{Missing, Integer},
    total_deaths::Union{Missing, Integer},
    reported_cases::Union{Missing, Integer},
    exports_deaths::Union{Missing, Integer},
    confirmed_cases::Union{Missing, Integer};
    ...
) -> Any
bvd_joint(
    n::Integer,
    exported_cases::Union{Missing, Integer},
    total_deaths::Union{Missing, Integer},
    reported_cases::Union{Missing, Integer},
    exports_deaths::Union{Missing, Integer},
    confirmed_cases::Union{Missing, Integer},
    tests_analysed::Union{Missing, Integer};
    confirmed_deaths,
    recovered_cases,
    deaths_history,
    reported_history,
    confirmed_history,
    confirmed_deaths_history,
    lab_history,
    lab_daily_history,
    suspected_daily_history,
    suspected_daily_deaths_history,
    isolation_history,
    bed_capacity_history,
    recovered_history,
    treatment_admissions_history,
    treatment_deaths_history,
    treatment_ruleout_history,
    treatment_absconded_history,
    treatment_confirmed_incare_history,
    treatment_suspect_incare_history,
    occupancy_break_days,
    confirmed_break_days,
    confirmed_break_gross_cases,
    confirmed_break_gross_deaths,
    confirmed_break_sd,
    export_case_days,
    export_death_days,
    breakpoint,
    source_population,
    infection,
    onset_incidence,
    exports,
    deaths,
    cases,
    confirmed,
    confirmed_deaths_stream,
    treatment,
    recovered,
    dispersion,
    ascertainment,
    background_re,
    confirmed_positivity_link,
    genetic,
    onset_to_sample,
    tmrca_days,
    tmrca_days_sd,
    renewal_start_lead,
    rt_walk_lead
) -> Any

Joint composer over all data streams. Runs the generating infection process once on a daily grid of length n (day n is the cut-off), stages it to daily onset incidence, then conditions on the DRC suspected cases, deaths and the laboratory pipeline (the analysed-specimen volume as a per-vintage time series and the confirmed positives as a Binomial of the observed analysed denominator), the confirmed deaths, the Uganda exports and deaths-among-exports, and the optional genetic seeding bound on the outbreak age. Each stream argument may be missing to drop it, so the model doubles as a prior- and posterior-predictive generator.

onset-to-report kernel with the suspected-case stream. A single analysed-specimen volume is fit through a report-to-analysed delay and the tested fraction; the confirmed positives are scored as a Binomial of the observed specimens-analysed denominator (lab_history) with a partially-pooled per-window positivity, so the confirmed counts no longer pass through the multiplicative ascertainment ridge. After the national cumulative analysed series stops, the reporting format gives a 24h analysed count on some days (lab_daily_history); these are fitted as per-day analysed volumes and also anchor that day's confirmed positives as a Binomial of the observed denominator. The early and unanchored windows (days with no published denominator) use the modelled analysed volume as the denominator, with the positivity (hence λ_bg) carried over from the windows that do have data (see confirmed_cases_model). The optional suspected_daily_history adds the post-26 May daily new-suspect inflow ("nouveaux cas suspects du jour"), scored against the modelled daily suspected series at each report day where the frozen cumulative suspected stream stops, on days disjoint from it. The optional suspected_daily_deaths_history adds the deaths analogue, the post-26 May daily new suspected deaths ("cas suspects du jour N (M deces)"), scored against the modelled daily suspected-death series where the frozen cumulative suspected-death stream stops. The confirmed deaths mirror the confirmed-case laboratory pipeline: a death "analysed" volume (the suspected deaths carried to laboratory receipt and thinned by the death testing fraction tau_death) scored through a death-pool composition positivity p = s·q_death + (1−spec)(1−q_death), with q_death the BVD share of the suspected deaths (see confirmed_deaths_model). The suspected deaths carry a death ascertainment p_death and a non-BVD background tied to the case background by a background CFR cfr_bg (see deaths_model). The optional isolation_history adds the daily isolation/treatment-bed occupancy ("Patients en isolement"), a prevalence stream fitted as the suspect inflow (BVD treatment stay plus non-BVD rule-out stay) carried through a length-of-stay survival into a daily stock (see treatment_flow_model). The optional recovered_history adds the recovered-among-confirmed stream ("cumul guéris"), survivors among the modelled daily confirmed cases scaled by the recovery probability and lagged by a confirmation-to-recovery delay (see recovered_model).

breakpoint is the intervention day passed to the reproduction-number walk (e.g. the first WHO situation report); genetic injects the genetic seeding submodel when tmrca_days is given. Tracked deterministics: C_T (cumulative infections by the cut-off), the single established reproduction number R0 (= the first R_t), r and doubling_time (current growth), r0 (the R0-implied cryptic growth rate), T (outbreak age), R_T (current reproduction number), the per-stream expected counts, the testing fraction tau_test, the background rate lambda_bg, the death ascertainment death_ascertainment, the background CFR background_cfr, the death testing fraction tau_death, the implied per-suspected (suspected_positivity) and per-test (test_positivity) positivities, and the death-pool BVD composition (death_composition) and death-confirmation positivity (death_confirmation).

source
BVDOutbreakSize.cases_only_model Method
julia
cases_only_model(
    n::Integer,
    reported_cases::Union{Missing, Integer};
    reported_history,
    suspected_daily_history,
    breakpoint,
    infection,
    onset_incidence,
    cases,
    dispersion,
    ascertainment
) -> Any

Cases-only composer (reported-cases ascertainment). Runs the infection process and onset staging, samples dispersion and pooled ascertainment, then conditions on the reported-cases likelihood. See reported_cases_model.

source
BVDOutbreakSize.cdf_nmax Method
julia
cdf_nmax(dist; q, cap, minlag) -> Any
julia
cdf_nmax(dist; q = 0.98, cap = 120, minlag = 5)

Maximum lag for discretising a delay dist: the smallest integer covering q of its CDF (the qth quantile rounded up), clamped to [minlag, cap]. Sizing the truncation by the distribution rather than a hand-set constant keeps a consistent tail mass (98% by default) across every delay. This is a deterministic function of the PRIOR-centre distribution, evaluated ONCE outside the Turing model when each delay submodel is constructed, so the PMF length is fixed and AD-safe.

source
BVDOutbreakSize.censored_delay_model Method
julia
censored_delay_model(nmax::Integer; mean_prior, sd_prior)

Generic delay submodel parameterised by mean and SD, discretised to a daily PMF over lags 0 … nmax by double interval censoring of a moment-matched LogNormal (see lognormal_meansd and discretise_censored). The LogNormal CDF differentiates cleanly under Mooncake, so this is the AD-safe discretisation route for every delay in the renewal convolutions. The mean and SD carry weakly-informative priors, so the delay is estimated rather than fixed. Returns (; pmf, dist, mean, sd).

source
BVDOutbreakSize.censored_occupancy_model Method
julia
censored_occupancy_model(
    means::AbstractVector,
    ceilings::AbstractVector,
    obs::Union{Missing, AbstractVector{<:Integer}},
    k::Real
) -> Any

Right-censored occupancy likelihood. Each day's observed bed count obs[i] is a NegBinomial around the latent bed DEMAND means[i], right-censored at the effective capacity ceilings[i]: below the ceiling the count reflects demand directly, and once demand reaches the ceiling the count is censored there. Unlike a smooth saturation of the mean, the censored tail probability still depends on the demand above the ceiling, so demand stays identified when beds are full rather than the occupancy going flat in demand. A missing obs samples (the predictive path). Shares the surveillance dispersion k.

source
BVDOutbreakSize.censoring_cap Method
julia
censoring_cap(iso_days, iso_obs, capacity_history) -> Any
julia
censoring_cap(iso_days, iso_obs, capacity_history)

Per-report-day right-censoring bound for the isolation-occupancy likelihood, built from DATA only (no sampled parameter), so the censored NegativeBinomial never sits against a moving -Inf wall. Each isolation day takes the nearest recorded implied-capacity value (capacity_history, the occupancy / reported occupancy-rate series), floored at that day's observed occupancy so the bound is never below the count (a count above the cap has zero probability under the censored NB, the discontinuity that drives the divergences). With the cap fixed, the censored likelihood is smooth in the sampled demand: below the cap the count identifies demand directly, and at the cap it contributes the one-sided demand ≥ capacity tail. A capacity_history with no counts (empty, or days-only as the predictive generator supplies) returns a large finite cap far above any bed count, so the censoring is a no-op and the likelihood is the plain NB (a literal Inf cannot be used because safe_rate maps non-finite values to eps); a missing iso_obs skips the occupancy floor.

The latent bed demand itself is left UNCENSORED: the cap enters only as the observation bound, so demand above capacity is carried by the renewal / length-of-stay demand model and its priors, not by the bound. Demand above a saturated capacity is only partially identified from occupancy (occupancy can say "demand was at least the beds filled", not how much more), so the bed shortfall is a prior/model-informed quantity, not pinned by the occupancy data — see treatment_flow_model.

source
BVDOutbreakSize.cfr_model Method
julia
cfr_model(
;
    cfr_prior
) -> DynamicPPL.Model{typeof(cfr_model), (), (:cfr_prior,), (), Tuple{}, Tuple{Distributions.Beta{Float64}}, DynamicPPL.DefaultContext, false}

Case-fatality ratio prior. Default Beta(6.6, 13.4) has mean ≈ 0.33, matching the CDC summary for past BVD outbreaks. Used by the deaths and deaths-among-exports streams.

source
BVDOutbreakSize.clinical_stay_survival Method
julia
clinical_stay_survival(
    death_pmf::AbstractVector,
    recover_pmf::AbstractVector,
    cfr::Real
) -> Any
julia
clinical_stay_survival(death_pmf, recover_pmf, cfr)

Clinical-stay survival of the BVD discharge mixture, indexed from cohort age 0, matching the discharge balance accumulate_occupancy runs. An admitted true case leaves by death (weight cfr, the in-care fatality CFR_iso) on the admission→death stay or by recovery (weight 1 − cfr) on the admission→recovery stay, so the cumulative discharge fraction by age d is G(d) = Σ_{j=0}^{d} (cfr · death_pmf[j+1] + (1 − cfr) · recover_pmf[j+1]) and the survival is its complement

so S_clin[d+1] is the probability the case has not yet been discharged by the end of day d. This is the per-cohort weight the running-balance BVD stock carries: with no absconds O_bvd(t) = Σ_{u ≤ t} A_bvd(u) · S_clin(t − u). The discharge-complement P(stay > d), rather than the inclusive P(stay ≥ d) of convolve_survival, keeps the two-clock confirmed sub-stock ≤ O_bvd by construction. The two PMFs need not share a length; each contributes zero beyond its support, so the result takes the longer length.

source
BVDOutbreakSize.combined_callback Method
julia
combined_callback(callbacks...) -> Any

Compose several nuts_sample step callbacks into one. Each argument is either a callback with the AbstractMCMC step signature or nothing; nothing entries are dropped. The composite invokes the surviving callbacks in order on every step. Returns the single callback unchanged when only one survives, and nothing when none do (so nuts_sample sees no callback at all rather than a no-op wrapper).

See also: fit_callback, progress_callback, tensorboard_callback.

source
BVDOutbreakSize.comparison_table Method
julia
comparison_table(
    C_draws::AbstractVector;
    scenarios
) -> DataFrames.DataFrame

For each published C_T scenario, the narrowest joint posterior credible interval (30, 60 or 90%) that contains it, or "outside 90%".

source
BVDOutbreakSize.confirmed_break_offset Method
julia
confirmed_break_offset(late_days, break_days, b) -> Any
julia
confirmed_break_offset(late_days, break_days, b)

Per-late-window confirmed harmonisation offset from the fitted break steps b on the manually specified break_days (grid day-indices). Unlike cumulative_occupancy_offset this is NOT cumulative: the confirmed likelihood fits between-vintage INCREMENTS, so a one-off retrospective base integration inflates exactly one increment — every later vintage differences against the new, higher base and is unaffected. Each entry is therefore the step for that window alone, zero on every non-break window.

Added to the modelled confirmed mean of the break window in confirmed_cases_model and confirmed_deaths_model, so the fit can explain an increment that is mostly reattached backlog without inflating the latent incidence that drives Rt. Each b_j is sampled there around the published discrepancy (break_step_centres), symmetric because a harmonisation can revise down as well as up. Returns a length-length(late_days) vector of eltype(b) (zeros when there are no break days, a no-op).

source
BVDOutbreakSize.confirmed_cases_model Method
julia
confirmed_cases_model(
    confirmed_history,
    confirmed_cases::Union{Missing, Integer},
    onsets::AbstractVector,
    k::Real,
    p_drc::Real,
    bg_daily::AbstractVector,
    τ_test::Real,
    bvd_reports_daily::AbstractVector;
    lab_history,
    lab_daily_history,
    tests_analysed,
    receipt,
    positivity,
    positivity_link,
    severity_enrichment,
    sensitivity,
    specificity,
    overdispersion,
    fit_unanchored,
    confirmed_break_days,
    confirmed_break_gross,
    confirmed_break_sd
) -> Any

Laboratory pipeline likelihood. Two streams driven by the shared renewal onsets and the suspected-case pipeline from reported_cases_model:

  • Specimens analysed. The suspected daily pipeline (p_drc-scaled BVD onset-to-report signal plus the non-BVD background λ_bg) is carried through a sampled report-to-analysed delay and thinned by the tested fraction τ_test, giving the expected daily analysed-specimen volume. Its between-vintage increments are fitted against lab_history (specimens analysed) as observed ~ data with a NegativeBinomial sharing the surveillance dispersion k, identifying τ_test and the delay. This single suspected->analysed volume is also the denominator proxy reused in the early and unanchored late windows below, so the volume that is fitted is the same quantity used as the denominator where no analysed count is observed. The specimens-received series is not modelled: analysed is the throughput that actually produces confirmed cases, and received overshoots it by the laboratory backlog.

  • Confirmed positives. The confirmed counts are scored as an overdispersed BetaBinomial of the observed specimens-analysed denominator in each laboratory window (confirmed_positivity_windows, safe_betabinomial), with a partially-pooled per-window positivity (confirmed_positivity_model) and a shared intra-window overdispersion (confirmed_overdispersion_model) so the confirmed intervals capture the extra-Binomial day-to-day laboratory variation rather than the far-too-tight plain Binomial spread. Conditioning the positives on the observed analysed denominator (rather than a modelled count scaled by p_drc · s_test · τ_test) removes the multiplicative ascertainment ridge that basin-split the joint, so the outbreak size is pinned by the deaths and exports streams while the laboratory positivity is free to track the noisy per-vintage data. The early confirmed vintages with no per-vintage analysed denominator (18-23 May) are scored as NegativeBinomial counts against the modelled laboratory volume with the same pooled positivity, so all the confirmed data is used and the early per-vintage shape informs the fit.

The per-window positivity has two links, set by positivity_link. The default :composition ties the tested BVD share to the suspect-pool composition φ_v = (p_drc·BVD)_v / ((p_drc·BVD)_v + λ_bg_v), severity- upsampled by a decaying enrichment δ0, then mapped to the tested-positive probability through the assay sensitivity and specificity, p = s · q + (1 − spec)(1 − q). The false-positive term (1 − spec)(1 − q) makes the confirmed counts respond to the non-BVD share 1 − q, so the laboratory positivity identifies the background λ_bg rather than absorbing it into a free curve — the structural link that lets the lab data pin λ_bg. The alternative :free link uses a free partially-pooled per-window random effect (confirmed_positivity_model) decoupled from λ_bg; it leaves λ_bg weakly identified and is kept for sensitivity analysis.

The observed-window positives are conditioned on the observed analysed denominator, so the Binomial conditioning that removes the multiplicative ascertainment ridge is preserved; only the early and unanchored late windows use the modelled analysed volume as the denominator.

The tested fraction τ_test and background rate λ_bg come from reported_cases_model so the suspected and laboratory streams share them. Exposes the per-window positivity, the expected analysed and confirmed totals at the cut-off, and the cut-off positivity as derived quantities.

source
BVDOutbreakSize.confirmed_cfr_table Method
julia
confirmed_cfr_table(res; digits) -> DataFrames.DataFrame

One-row DataFrame summarising the confirmed-CFR comparison from a delay_corrected_confirmed_cfr result res: the median and equal-tailed 90% credible interval of the delay-corrected confirmed CFR and the structural (infection-based) CFR, the median uncorrected modelled confirmed ratio, and the naive observed confirmed ratio. Percentages rounded to digits decimal places.

source
BVDOutbreakSize.confirmed_deaths_model Method
julia
confirmed_deaths_model(
    confirmed_deaths::Union{Missing, Integer},
    total_deaths::Union{Missing, Integer},
    deaths_daily::AbstractVector,
    bvd_deaths_daily::AbstractVector,
    bg_death_daily::AbstractVector,
    k::Real;
    confirmed_deaths_history,
    confirmed_break_days,
    confirmed_break_gross,
    confirmed_break_sd,
    receipt_pmf,
    capacity_start,
    case_analysed_daily,
    case_suspected_daily,
    scaling,
    testing,
    sensitivity,
    specificity
) -> Any

Laboratory-confirmed-deaths likelihood, the death analogue of the confirmed-case laboratory pipeline (confirmed_cases_model). The confirmed cases score a modelled analysed-specimen volume (the suspected-case pipeline carried to laboratory receipt and thinned by the testing fraction) times a composition-linked assay positivity. The death side has no published analysed denominator, so the confirmed-death increments are NegBinomial(k) counts of the modelled death volume, the same modelled-volume route the early and post-lab confirmed-case windows use.

Three pieces:

  • Death analysed volume. Deaths are tested out of the same laboratory as cases, so the death volume tracks the modelled case analysed volume case_analysed_daily at the per-day suspected death-to-case ratio, times a testing-intensity scaling, v = scaling · case_analysed_daily · susp_death / susp_case, with susp_death and susp_case the suspected deaths and cases carried to receipt by the confirmed cases' delay. The case volume carries the laboratory capacity onset, so the death volume inherits it. The scaling (death_testing_scaling_model) is the per-suspect testing-intensity difference between deaths and living suspects; the death-to-case ratio carries the suspect-pool severity and the suspected-death level. The death-only composer has no case stream and falls back to a death testing fraction (death_testing_fraction_model).

  • Death-pool composition. The BVD share of the suspected deaths at receipt, q_death = bvd_death / (bvd_death + bg_death) per day, from the death series' own BVD and background components. The death background, tied to the case background by cfr_bg (see deaths_model and background_cfr_model), keeps q_death below one.

  • Assay positivity. p = s · q_death + (1 − spec)(1 − q_death) with PCR sensitivity s (test_sensitivity_model) and specificity spec (test_specificity_model), the same form as the confirmed-case positivity, drawn from the same priors as separate death-stream parameters.

Returns the cut-off realised death testing fraction τ_death, the testing scaling, the cut-off death-pool composition q_death, the confirmation positivity and the expected confirmed-death count.

source
BVDOutbreakSize.confirmed_deaths_only_model Function
julia
confirmed_deaths_only_model(
    n::Integer,
    confirmed_deaths::Union{Missing, Integer};
    ...
) -> Any
confirmed_deaths_only_model(
    n::Integer,
    confirmed_deaths::Union{Missing, Integer},
    total_deaths::Union{Missing, Integer};
    deaths_history,
    confirmed_deaths_history,
    confirmed_break_days,
    confirmed_break_gross_deaths,
    confirmed_break_sd,
    breakpoint,
    infection,
    onset_incidence,
    deaths,
    cases,
    confirmed_deaths_stream,
    dispersion,
    ascertainment
) -> Any

Confirmed-deaths-only composer. Runs the infection process and onset staging, samples dispersion and pooled ascertainment, runs the reported- cases stream (in predictive mode, to supply the non-BVD background the death background is scaled from) and the suspected-deaths stream, then conditions on the confirmed-death likelihood alone. See confirmed_deaths_model.

source
BVDOutbreakSize.confirmed_only_model Method
julia
confirmed_only_model(
    n::Integer,
    confirmed_cases::Union{Missing, Integer};
    confirmed_history,
    lab_history,
    lab_daily_history,
    tests_analysed,
    breakpoint,
    confirmed_break_days,
    confirmed_break_gross_cases,
    confirmed_break_sd,
    infection,
    onset_incidence,
    cases,
    confirmed,
    dispersion,
    ascertainment,
    confirmed_positivity_link
) -> Any

Confirmed-cases-only composer (laboratory pipeline in isolation). Runs the infection process and onset staging, samples dispersion and pooled ascertainment, then runs the suspected-case stream (in predictive mode, to draw the shared background rate, testing fraction and onset-to-report kernel) and conditions on the laboratory pipeline alone: the confirmed positives (a Binomial of the observed analysed denominator in lab_history) and the modelled analysed-specimen volume. See confirmed_cases_model and reported_cases_model.

Exposes the cut-off expected confirmed count as expected_confirmed_T, the same un-prefixed name bvd_joint uses, so the confirmed stream can be forecast from this fit (forecast_stream).

source
BVDOutbreakSize.confirmed_overdispersion_model Method
julia
confirmed_overdispersion_model(
;
    overdispersion_prior
) -> DynamicPPL.Model{typeof(confirmed_overdispersion_model), (), (:overdispersion_prior,), (), Tuple{}, Tuple{Distributions.Beta{Float64}}, DynamicPPL.DefaultContext, false}

Confirmed-positives overdispersion prior. The confirmed positives in each laboratory window are scored as an overdispersed BetaBinomial of the observed analysed denominator (see safe_betabinomial and confirmed_cases_model) rather than a plain Binomial, because the per-window positivity p_pos is a smooth pooled / composition-linked curve that does not capture the day-to-day laboratory batching and within-window positivity heterogeneity the confirmed counts carry. A plain Binomial on denominators of several hundred specimens gives predictive intervals far too tight, so the confirmed stream is systematically under-covered. The intra-window correlation ρ ∈ (0, 1) inflates the window variance to n·p·(1 − p)·(1 + (n − 1)·ρ), with ρ → 0 recovering the Binomial. The default Beta(1, 24) (mean ≈ 0.04, 90% ≈ 0.002–0.12) is weakly informative: it favours a small overdispersion and shrinks toward the Binomial when the data support it, while a single scalar identified across the laboratory windows lets the confirmed positives themselves set the spread. Returns (; ρ).

source
BVDOutbreakSize.confirmed_positives_model Function
julia
confirmed_positives_model(
    positives::Union{Missing, AbstractVector{<:Integer}},
    analysed::AbstractVector{<:Integer},
    p_pos::AbstractVector
) -> Any
confirmed_positives_model(
    positives::Union{Missing, AbstractVector{<:Integer}},
    analysed::AbstractVector{<:Integer},
    p_pos::AbstractVector,
    ρ::Real
) -> Any

Per-window confirmed-positives likelihood, expressed as a vector likelihood scored against the observed analysed denominators. Given the per-window analysed counts analysed, positivities p_pos and the intra-window overdispersion ρ, scores the observed positives with one BetaBinomial(analysed[i], p_pos[i], ρ) per window (see safe_betabinomial). The overdispersion adds the extra-Binomial variation the confirmed counts carry; ρ → 0 recovers the plain Binomial(analysed[i], p_pos[i]). positives is the model argument on the LHS of ~, so a supplied vector is observed data DynamicPPL conditions on (mirroring vintage_increments_model) and a missing argument is sampled, making the submodel a predictive generator. Under a prefixed submodel attachment the predict keys are <prefix>.positives[i]. Returns the observed (or sampled) positives.

source
BVDOutbreakSize.confirmed_positivity_model Method
julia
confirmed_positivity_model(
    nv::Integer;
    baseline_prior,
    pooling_prior
) -> Any

Per-vintage laboratory positivity for the confirmed-case stream, in partially-pooled non-centred form. The confirmed positives in each laboratory window are scored as a Binomial of the observed specimens-analysed denominator (see confirmed_cases_model), so the positivity is the probability a tested specimen is confirmed. The per-window logit positivity shares a baseline q_mu and is perturbed by non-centred deviations z_q scaled by the pooling SD σ_q, so a window with little data is shrunk toward the baseline while a window with a strong signal can depart from it. σ_q → 0 recovers a single shared positivity. The baseline prior is centred on the cut-off cumulative positivity (≈ 0.28, i.e. 210 / 755 on the 28 May data) on the logit scale. Conditioning on the observed denominator and giving the positivity its own random effect decouples the confirmed counts from the multiplicative ascertainment ridge (p_drc · s_test · τ_test) that basin-split the joint, so the outbreak size is pinned by the deaths and exports streams rather than forced through the laboratory positivity. Returns (; p_pos, q_mu, σ_q) with p_pos a length-nv vector.

source
BVDOutbreakSize.confirmed_positivity_windows Function
julia
confirmed_positivity_windows(
    confirmed_history,
    lab_history
) -> Union{NamedTuple{(:obs_days, :obs_positives, :obs_analysed, :early_days, :early_increments, :early_start, :late_days, :late_increments, :late_analysed, :late_start), <:Tuple{Vector{Int64}, Vector{Int64}, Vector{Int64}, Any, Any, Any, Vector{Int64}, Vector{Int64}, Vector{Int64}, Int64}}, NamedTuple{(:obs_days, :obs_positives, :obs_analysed, :early_days, :early_increments, :early_start, :late_days, :late_increments, :late_analysed, :late_start), <:Tuple{Vector{Int64}, Vector{Int64}, Vector{Int64}, Vector{Int64}, Vector{Int64}, Any, Vector{Int64}, Vector{Int64}, Vector{Int64}, Any}}}
confirmed_positivity_windows(
    confirmed_history,
    lab_history,
    lab_daily_history
) -> Union{NamedTuple{(:obs_days, :obs_positives, :obs_analysed, :early_days, :early_increments, :early_start, :late_days, :late_increments, :late_analysed, :late_start), <:Tuple{Vector{Int64}, Vector{Int64}, Vector{Int64}, Any, Any, Any, Vector{Int64}, Vector{Int64}, Vector{Int64}, Int64}}, NamedTuple{(:obs_days, :obs_positives, :obs_analysed, :early_days, :early_increments, :early_start, :late_days, :late_increments, :late_analysed, :late_start), <:Tuple{Vector{Int64}, Vector{Int64}, Vector{Int64}, Vector{Int64}, Vector{Int64}, Any, Vector{Int64}, Vector{Int64}, Vector{Int64}, Any}}}
confirmed_positivity_windows(
    confirmed_history,
    lab_history,
    lab_daily_history,
    confirmed_break_days
) -> Union{NamedTuple{(:obs_days, :obs_positives, :obs_analysed, :early_days, :early_increments, :early_start, :late_days, :late_increments, :late_analysed, :late_start), <:Tuple{Vector{Int64}, Vector{Int64}, Vector{Int64}, Any, Any, Any, Vector{Int64}, Vector{Int64}, Vector{Int64}, Int64}}, NamedTuple{(:obs_days, :obs_positives, :obs_analysed, :early_days, :early_increments, :early_start, :late_days, :late_increments, :late_analysed, :late_start), <:Tuple{Vector{Int64}, Vector{Int64}, Vector{Int64}, Vector{Int64}, Vector{Int64}, Any, Vector{Int64}, Vector{Int64}, Vector{Int64}, Any}}}

Align the confirmed-case counts onto the laboratory windows, splitting them into three non-overlapping groups so all the confirmed data is used:

  • Observed windows, where an analysed-specimen increment is available (the laboratory series only differences cleanly from its second vintage onward; the first cumulative value is the baseline). Each window's analysed increment is the Binomial denominator and the matching confirmed increment the positives. A zero analysed increment (the 24-25 May analysis stall) or a window whose positives exceed its denominator is merged forward, so every observed window has obs_analysed > 0 and 0 ≤ obs_positives ≤ obs_analysed.

  • Early windows, the confirmed vintages up to and including the first laboratory date (18-23 May), which have no per-vintage analysed denominator. Their confirmed increments are returned so the model can score them against the modelled laboratory volume, with the per-window positivity partially pooled with the observed windows.

  • Late windows, the confirmed vintages strictly after the last laboratory date (the days after national testing stops, INSP's confirmed-only format with no national analysed-specimen denominators). Like the early windows, their confirmed increments are scored against the modelled laboratory volume with the pooled positivity. Their day grid carries a late_start day (the last laboratory day) so the model bins the modelled volume over each late window's own day range, pinned at late_start, rather than from day 0 (which would double-count the observed window volume). A late day that publishes a 24h analysed count (lab_daily_history, the post-cutoff daily denominators the unanchored windows otherwise lack) is flagged in late_analysed so the model can score it as a Binomial of that observed denominator instead, anchoring its positivity to data. A day listed in confirmed_break_days is the exception: its denominator is dropped (late_analysed set to 0) so it stays a modelled-volume window. On such a day INSP has retrospectively integrated a harmonised provincial base, so most of the confirmed increment is reattached backlog rather than positives out of that day's specimens, and pairing the two would score a wildly inflated positivity (22 July 2026: 369 of 414 analysed, 89% implied against the 23% actually reported). The increment is still scored; the backlog is absorbed by a fitted level step in confirmed_cases_model (see confirmed_break_offset).

The three groups partition the confirmed counts at the first and last laboratory dates, so no confirmed case is counted twice. Returns (; obs_days, obs_positives, obs_analysed, early_days, early_increments, late_days, late_increments, late_analysed, late_start) of grid day-indices and per-window counts; late_analysed[i] is the observed 24h analysed denominator for late day i (0 when none was published). The observed and late groups are empty when no laboratory history is present and every confirmed vintage becomes an early window. Pure integer bookkeeping on the observed data, so it carries no gradient.

source
BVDOutbreakSize.convolve_delay Method
julia
convolve_delay(
    x::AbstractVector,
    delay::AbstractVector
) -> Any

Convolve a daily trajectory x (infections or onsets) with a delay PMF delay (indexed from lag 0), returning the expected daily counts of the delayed event on the same daily grid: entry t sums x[t−d] · delay[d+1] over lags d that stay in range. This is the discrete convolution that replaces the continuous onset-to-event integrals of the integral model, and maps infections to onsets, onsets to deaths, onsets to reports and onsets to detected exports. Type-stable and AD-transparent.

The scalar double loop is deliberate: a vectorised lag-AXPY rewrite (scripts/bench_convolve.jl) was no faster under Mooncake (≈0.95x — the reverse pass over the scalar loop is already efficient), so the simpler form stays. The per-gradient cost the joint actually pays sits in the delay discretisation (the censored-distribution CDF evaluations), which the delay nmax trims target instead.

source
BVDOutbreakSize.convolve_pmf Method
julia
convolve_pmf(a::AbstractVector, b::AbstractVector) -> Any

Discrete convolution of two delay PMFs a and b (both indexed from delay 0 at element 1), giving the PMF of the summed delay a ⊕ b. The result has length length(a) + length(b) - 1; its mass equals sum(a) * sum(b), so normalised inputs give a normalised output. Used to build the infection→detection delay (incubation ⊕ onset-to-detection) and the infection→death delay (incubation ⊕ onset-to-death) for the exports streams from their component PMFs. Type-stable and AD-transparent.

source
BVDOutbreakSize.convolve_survival Method
julia
convolve_survival(
    x::AbstractVector,
    los::AbstractVector
) -> Any

Survival-weighted convolution for an occupancy (prevalence) stream. Given a daily admission series x and a length-of-stay PMF los (indexed from lag 0, so los[1] = P(LOS = 0)), return the daily occupancy

so an admission on day s occupies a bed on days s, s+1, … until it is discharged: the admission day is always counted (S(0) = 1) and a stay of LOS days contributes to LOS + 1 daily occupancies. This is the prevalence analogue of convolve_delay's incidence convolution — the length-of-stay survival replaces the onset-to-event delay PMF, turning an inflow into a stock. The survival weights are the reverse-cumulative tail sums of the PMF (S(τ) = Σ_{j ≥ τ} los[j]), so for a normalised PMF S(0) = 1, and the occupancy is convolve_delay(x, S). Type-stable and AD-transparent; the element type follows the inputs.

source
BVDOutbreakSize.crps_decomposition Method
julia
crps_decomposition(
    obs::Real,
    samples::AbstractVector{<:Real}
) -> NamedTuple{(:dispersion, :overprediction, :underprediction), <:Tuple{Any, Any, Any}}

Sample-based CRPS decomposition of a predictive samples ensemble at a single observation obs, into (; dispersion, overprediction, underprediction), following the convention scoringutils uses for the weighted interval score, applied here to an ensemble's own order statistics rather than a fixed set of quantiles.

dispersion is the ensemble's spread on its own terms: pair the i-th smallest and i-th largest draw as a central interval and sum each pair's width, weighted so the sum matches the CRPS the ensemble would score against its own median, the best location it could be scored against. overprediction and underprediction are the extra cost of scoring against obs rather than that best case, split by which side of each pair obs falls on: overprediction when a pair sits entirely above obs (the ensemble reads too high), underprediction when a pair sits entirely below it (the ensemble reads too low). All three are non-negative and sum exactly to crps_sample at the same obs and samples, since the construction is an exact re-partition of the same order-statistic sum crps_sample evaluates, not an approximation.

With no draws (isempty(samples)), every component is NaN.

source
BVDOutbreakSize.crps_sample Method
julia
crps_sample(
    obs::Real,
    samples::AbstractVector{<:Real}
) -> Any

Continuous ranked probability score of a predictive samples ensemble at a single observation obs. Lower is better; zero for a perfect point forecast. For a point-mass (constant) ensemble the CRPS reduces to the absolute error abs(obs - samples[1]).

source
BVDOutbreakSize.cumulative_occupancy_offset Method
julia
cumulative_occupancy_offset(iso_days, break_days, b) -> Any
julia
cumulative_occupancy_offset(iso_days, break_days, b)

Per-isolation-day cumulative occupancy reclassification offset Δ from the fitted break steps b on the manually specified break_days (grid day- indices). Δ(t) sums b_j over the break days at or before iso_days[t], zero before the first break day. Added to the modelled occupancy mean in the censored-occupancy likelihood so the modelled occupancy tracks a between-report measurement-basis discontinuity in the observed series (e.g. a Tableau 6-sum → page-1 headline transition) and the residual stays smooth. Each b_j is sampled in treatment_flow_model, so Δ is a fitted quantity: the fit partitions each step into reporting artifact vs real demand change. Returns a length-length(iso_days) vector of eltype(b) (zeros when there are no break days, a no-op).

source
BVDOutbreakSize.dated_event_bins Method
julia
dated_event_bins(
    event_days::AbstractVector{<:Integer},
    n::Integer
) -> Tuple{Any, Any}

Group a sorted event-day list event_days (grid day-indices, ascending, one entry per dated event, possibly with repeats) into its unique days and the per-day occupancy count, clamped to [1, n]. Returns (days, counts) with days the unique detection/death days and counts[i] the number of events on days[i]. Used by exports_model and the export-deaths likelihood to place one Poisson term per detection/death day with an observed count equal to that day's occupancy, so simultaneous imports on one day share a single edge. event_days must be non-empty.

source
BVDOutbreakSize.dated_poisson_model Method
julia
dated_poisson_model(
    means::AbstractVector,
    counts::Union{Missing, AbstractVector{<:Integer}}
) -> Any

Per-day Poisson likelihood for a dated event series. Scores the observed per-day counts obs against the modelled per-day means means with one Poisson term each, NaN/Inf-safe via safe_rate. When obs is missing the counts are sampled, the predictive-generator path; the indexed counts[i] keeps the predict keys (<prefix>.counts[i]) replicable. Used by exports_model and the export-deaths likelihood for the dated Uganda export series.

source
BVDOutbreakSize.death_ascertainment_model Method
julia
death_ascertainment_model(
;
    ascertainment_prior
) -> DynamicPPL.Model{typeof(death_ascertainment_model), (), (:ascertainment_prior,), (), Tuple{}, Tuple{Distributions.Normal{Float64}}, DynamicPPL.DefaultContext, false}

Ascertainment of the suspected-death stream (deaths_model): the fraction p_death of true BVD deaths that enter the INSP suspected-death count by the cut-off. The suspected-death definition is symptomatic-then- deceased, so a fatal BVD infection only counts once the death is reported, and not every BVD death is captured. The expected BVD suspected deaths are therefore p_death · CFR of the onset-to-death-convolved infections, the death analogue of the suspected-case ascertainment p_drc (pooled_ascertainment_model).

The default Normal(logit(0.9), 0.5) on the logit scale is deliberately informative and centred on a HIGH ascertainment: a death is a salient event in an Ebola response and is reported more reliably than a living suspected case (p_drc ≈ 0.75). The SD 0.5 gives a 90% prior interval of roughly 0.80–0.95, admitting moderate under-ascertainment without letting the death stream slide to an implausibly low capture. p_death is weakly identified on its own (it trades off with the CFR for the suspected-death level), so it leans on this prior; the export-death stream and the CFR prior pin the CFR separately. Pass ascertainment_prior to override. Returns (; p_death, logit_p_death).

source
BVDOutbreakSize.death_background_model Method
julia
death_background_model(
;
    lambda_prior
) -> DynamicPPL.Model{typeof(death_background_model), (), (:lambda_prior,), (), Tuple{}, Tuple{Distributions.Truncated{Distributions.Normal{Float64}, Distributions.Continuous, Float64, Float64, Nothing}}, DynamicPPL.DefaultContext, false}

Non-BVD background rate for the suspected-death stream (deaths_model), the death analogue of the suspected-case background λ_bg (test_positivity_model). The DRC sitrep suspected-death definition is symptomatic-then-deceased, so a death need not be a true BVD death; this submodel samples the per-day non-BVD background death rate λ_bg_death. Its cumulative contribution over the grid is λ_bg_death · n.

The default truncated(Normal(0, 0.25); lower = 0) is deliberately informative, mirroring the case background: deaths are far fewer than suspected cases (≈ 246 suspected deaths vs ≈ 1077 suspected cases at the last stable suspected vintages), so the background rate is scaled down accordingly. With SD 0.25 the median background is ≈ 0.17/day (a modest minority of the suspected-death total over the grid) while still admitting a genuine non-BVD signal. The background is degenerate with outbreak size, so a diffuse prior would let it absorb arbitrarily many suspected deaths. Pass lambda_prior to override. Returns (; λ_bg_death).

source
BVDOutbreakSize.death_testing_fraction_model Method
julia
death_testing_fraction_model(
;
    fraction_prior
) -> DynamicPPL.Model{typeof(death_testing_fraction_model), (), (:fraction_prior,), (), Tuple{}, Tuple{Distributions.Beta{Float64}}, DynamicPPL.DefaultContext, false}

Death testing fraction τ_death, the fallback for the death-only composer (confirmed_deaths_only_model), which has no case stream to set the death testing volume from. It thins the suspected deaths to a death "analysed" volume at the case testing rate, drawing τ_death from the same prior as the case testing fraction (Beta(5, 2), mean ≈ 0.71). The full joint instead scales the modelled case analysed volume (see confirmed_deaths_model and death_testing_scaling_model) and does not draw this submodel. Pass fraction_prior to override. Returns (; τ_death).

source
BVDOutbreakSize.death_testing_scaling_model Method
julia
death_testing_scaling_model(
;
    scaling_prior
) -> DynamicPPL.Model{typeof(death_testing_scaling_model), (), (:scaling_prior,), (), Tuple{}, Tuple{Distributions.LogNormal{Float64}}, DynamicPPL.DefaultContext, false}

Death testing-intensity scaling for the confirmed-death volume in the joint (confirmed_deaths_model). The death analysed volume is the modelled case analysed volume carried at the per-day suspected death-to-case ratio, times this scaling. That ratio already carries the suspect-pool severity and the suspected-death level, so the scaling is the per-suspect testing-intensity difference between deaths and living suspects alone. No death-testing data grounds it, so it is a tight log-normal centred on one (LogNormal(0, 0.25), median 1, 90% ≈ 0.66–1.51): deaths are tested at the case intensity unless the confirmed-death counts pull the scaling off one. Pass scaling_prior to override. Returns (; scaling).

source
BVDOutbreakSize.deaths_model Method
julia
deaths_model(
    deaths_history,
    total_deaths::Union{Missing, Integer},
    onsets::AbstractVector,
    k::Real;
    suspected_daily_deaths_history,
    cfr,
    ascertainment,
    case_bg_daily,
    background_cfr,
    death_background,
    background_re,
    onset_to_death
) -> Any

DRC suspected-deaths likelihood, per-vintage time series. Convolves the daily onsets with the sampled onset-to-death delay, scales by the CFR and the death ascertainment p_death, and reads the modelled cumulative deaths at each vintage day off the daily series, fitting the between-vintage increments as observed ~ data with a NegativeBinomial sharing the surveillance dispersion k (surveillance_dispersion_model). The death history ends at the cut-off, so the cut-off total is the final increment and is not scored separately. Samples the onset-to-death delay, the CFR and the death ascertainment via injected submodels. The onset-to- death prior is the convolution of the two atomic line-list components (onset-to-admission and admission-to-death; the bdbv-linelist-analysis submodule), implied mean ≈ 12.8 d, SD ≈ 7.0 d, the same source the integral model used.

The expected BVD suspected deaths are p_death · CFR of the onset-to-death- convolved infections: a fatal BVD infection is counted as a suspected death only when ascertained, the death analogue of the suspected-case ascertainment p_drc (see death_ascertainment_model). The default death ascertainment prior is informative and high (centre ≈ 0.9), reflecting that a death is more reliably reported than a living suspect.

Non-BVD background suspected deaths are added on top. The joint passes the per-day non-BVD suspected-case background case_bg_daily and a background CFR submodel (background_cfr_model): the background deaths are cfr_bg times that case background, lagged by the onset-to-death delay so a background death follows its background case the way the BVD deaths follow the onsets. The death background therefore inherits a level and time profile from the identified case background without a second free, outbreak-size-degenerate rate. The death_background (death_background_model) scalar, the per-vintage background_re and the pure-BVD stream are sensitivity fallbacks.

An optional suspected_daily_deaths_history adds the post-26 May daily new suspected deaths ("cas suspects du jour N (M deces)"): per-day counts of suspected deaths scored against the modelled daily suspected-death series deaths_daily at each report day (a single-day mean, not a between-vintage increment) with NegativeBinomials sharing k. This is the deaths analogue of the suspected-case daily inflow (reported_cases_model): it continues the suspected-death signal where the cumulative deaths_history stops once INSP stopped publishing a national suspected-death total. The inflow is a genuine per-day count that never falls, so it fits directly; its days fall strictly after the cumulative series ends, so the two suspected-death likelihoods cover disjoint days. Empty by default.

Returns the cut-off expected count, the daily death series (total and the BVD and background components), the onset-to-death PMF, the CFR, the death ascertainment and the background CFR for reuse by confirmed_deaths_model and exports_deaths_model.

source
BVDOutbreakSize.deaths_only_model Method
julia
deaths_only_model(
    n::Integer,
    total_deaths::Union{Missing, Integer};
    deaths_history,
    suspected_daily_deaths_history,
    breakpoint,
    infection,
    onset_incidence,
    deaths,
    dispersion
) -> Any

Deaths-only composer (back-calculation analogue). Runs the infection process and onset staging, samples dispersion, then conditions on the deaths likelihood only. See deaths_model.

source
BVDOutbreakSize.default_adtype Method
julia
default_adtype() -> ADTypes.AutoMooncake{Mooncake.Config}

Mooncake reverse-mode AD with default Mooncake.Config(). Used as the NUTS adtype keyword.

source
BVDOutbreakSize.delay_corrected_cfr Method
julia
delay_corrected_cfr(
    c_daily::AbstractVector,
    Kc::AbstractVector,
    Kd::AbstractVector,
    deaths_total::Real
) -> Any

Delay-corrected confirmed case-fatality ratio for one posterior draw.

c_daily is the modelled daily confirmed-case incidence over the day grid (day length(c_daily) is the cut-off), Kc the onset-to-confirmation delay PMF (lag 0 at index 1), Kd the onset-to-death-confirmation delay PMF, and deaths_total the cumulative confirmed deaths at the cut-off. The corrected denominator is Σ_t c_daily[t] · P(outcome resolved by the cut-off | confirmed at t), smaller than the raw cumulative confirmed cases, so the corrected ratio deaths_total / denominator lifts the naive ratio toward the eventual confirmed CFR. Returns NaN when no confirmed cases have had time to resolve.

source
BVDOutbreakSize.delay_corrected_confirmed_cfr Method
julia
delay_corrected_confirmed_cfr(
    chn;
    obs_confirmed,
    obs_confirmed_deaths
)

Delay-corrected confirmed case-fatality ratio across the posterior, read off a joint bvd_joint chain. For each draw the modelled daily confirmed-case incidence (rescaled so its total matches the scored expected confirmed cases), the onset-to-confirmation and onset-to-death-confirmation delay PMFs, and the cumulative confirmed deaths give the corrected ratio (delay_corrected_cfr).

obs_confirmed and obs_confirmed_deaths are the observed cumulative laboratory-confirmed cases and confirmed deaths at the cut-off, used for the naive observed confirmed ratio.

Returns a NamedTuple with the per-draw vectors corrected (delay-corrected confirmed CFR), modelled_naive (uncorrected modelled confirmed deaths over confirmed cases) and structural (the model's infection/onset-level CFR), and the scalar naive_observed = obs_confirmed_deaths / obs_confirmed.

source
BVDOutbreakSize.diagnostics_table Method
julia
diagnostics_table(fits::Pair{String}...) -> Any

DataFrame of fit-quality diagnostics with one row per fit. Pass each fit as "label" => chain. Columns :fit, :max_rhat, :min_ess_bulk, :divergences.

source
BVDOutbreakSize.discretise_censored Method
julia
discretise_censored(dist, nmax::Integer) -> Any

Daily probability mass function for the continuous delay dist over lags 0, 1, …, nmax, discretised by double interval censoring (uniform primary event over a one-day window, then unit-interval censoring of the secondary event) via CensoredDistributions.double_interval_censored. This is the discrete analogue of the continuous onset-to-event densities used by the integral model, and is the discretisation route the renewal convolutions rely on. For a LogNormal primary the CDF differentiates cleanly under Mooncake, so this is AD-safe; extreme warmup proposals that drive the quadrature to a non-finite or zero total fall back to a uniform PMF so the downstream convolution stays finite (the proposal is still rejected through its low log-likelihood). Returns a vector whose element type follows the delay parameters.

source
BVDOutbreakSize.doubling_time Method
julia
doubling_time(r) -> Any

Doubling time log(2) / r implied by an exponential growth rate r, the renewal model's reported analogue of the integral model's sampled doubling time. Returns a non-finite value as r crosses zero, matching the limit of an unbounded doubling time at zero growth.

source
BVDOutbreakSize.drop_degenerate_fit_column Method
julia
drop_degenerate_fit_column(
    table::DataFrames.DataFrame
) -> DataFrames.DataFrame

table (from forecast_score_overview, forecast_score_by_horizon or forecast_score_by_release) with its fit column dropped, for a table whose fit column is single-valued by construction rather than merely by the data currently on hand — the frozen-fit evaluation, which scores the current joint model alone at past cut-offs, is the only such table in this report.

Every other column (crps, dispersion, overprediction, underprediction, coverage_50, coverage_90, …) is left untouched; this drops one column and nothing else. table is returned with fit still present when it is empty, since an empty table carries no evidence either way. State which model a de-columned table refers to in the surrounding prose, so the table stays self-describing once the column is gone.

Errors when table's fit column carries more than one distinct value, rather than silently dropping it: a column that vanishes only when it happens to be constant and reappears once a second model is scored is worse than a constant column that never varies, so this is only safe to call on a table whose single-fit shape is structural (as the frozen evaluation's is), never on a table that could legitimately grow a second model later.

source
BVDOutbreakSize.drop_individual_fit_columns Method
julia
drop_individual_fit_columns(
    table::DataFrames.DataFrame
) -> DataFrames.DataFrame

table (a table from forecast_score_overview, forecast_score_by_horizon or forecast_score_by_release) with the individual-fit comparison columns dropped rather than kept as an all-missing column. Use this to display a table built from an evaluation that never carries an individual single-stream fit, such as a frozen-fit evaluation, which scores only the joint model at past cut-offs.

source
BVDOutbreakSize.enzyme_adtype Function
julia
enzyme_adtype()

Enzyme reverse-mode AD type, an opt-in alternative to the default default_adtype (Mooncake). Defined by the package's Enzyme weak-dependency extension (ext/BVDOutbreakSizeEnzymeExt.jl); calling it without Enzyme loaded raises a MethodError. Load Enzyme to activate the extension. The SpecialFunctions.gamma EnzymeRule that the Beta and NegativeBinomial normalising constants reach is supplied by CensoredDistributions' own Enzyme extension. Enzyme differentiates the single-stream composers and matches Mooncake; differentiating the full joint is platform-dependent (it can hit an upstream Enzyme/LLVM compile failure on some systems, see test/test_enzyme.jl), so Mooncake remains the package default for fitting.

source
BVDOutbreakSize.euler_lotka_r Method
julia
euler_lotka_r(R, g::AbstractVector; steps) -> Any

Exponential growth rate r implied by a reproduction number R and a generation-interval PMF g (indexed from lag 1), solving the Euler–Lotka identity R · Σ_s g_s e^{−r s} = 1. Starts from the small-r approximation r ≈ (R − 1) / (R · ḡ) with the mean generation time, then refines with steps Newton iterations. The loop is unrolled over a fixed step count and uses only arithmetic and exp, so it is AD-transparent under Mooncake. Mirrors the R_to_r seeding helper in EpiAware.jl and the implied-growth initialisation in the EpiNow2 Stan model, replacing the doubling-time parameterisation of the integral model.

source
BVDOutbreakSize.expand_vintage_rate Method
julia
expand_vintage_rate(
    rate::AbstractVector,
    days::AbstractVector{<:Integer},
    n::Integer
) -> Any

Expand a per-vintage rate vector rate onto a length-n daily grid, assigning each day the rate of the vintage window it falls in. The windows are delimited by the ascending day indices days (1-based into the grid); day t ≤ days[1] takes rate[1], a day in (days[i-1], days[i]] takes rate[i], and any day beyond the last vintage takes the last rate (a flat carry-forward of the final window). When days is empty the whole grid takes rate[1] if present, else zero, so a scalar background is recovered. Pure and AD-transparent; the element type follows rate. Used to turn the per-vintage background random effect (background_re_model) into the additive daily background the suspected-case and suspected-death streams consume.

source
BVDOutbreakSize.exponential_growth_model Method
julia
exponential_growth_model(
;
    r_prior,
    m_prior
) -> DynamicPPL.Model{typeof(exponential_growth_model), (), (:r_prior, :m_prior), (), Tuple{}, Tuple{Distributions.LogNormal{Float64}, Distributions.Truncated{Distributions.Normal{Float64}, Distributions.Continuous, Float64, Float64, Nothing}}, DynamicPPL.DefaultContext, false}

Molecular-clock growth-and-size prior for the renewal cryptic phase. SAMPLES the exponential growth rate r directly (the primary epidemiological assumption, placed on the genetic doubling time) and the doubling count m, then exposes

as deterministics. T = m·τ is the CRYPTIC-PHASE duration (origin → renewal start): m counts the doublings during the cryptic phase, so the cryptic phase grows a single import to 2^m infections AT the renewal start, and the magnitude is independent of r. The composer (infection_model) adds the observation span τ_obs = n − renewal_start to get the TOTAL outbreak age T_total = m·τ + τ_obs, which carries the genetic seeding bound (genetic_seeding_model).

The growth rate carries the prior r ~ LogNormal(log(log2 / M_PRIOR_DOUBLING_DAYS), 0.28), with median doubling time (11.7 d) matching the BEAST X estimate (mbalaplacide2026, Exponential growth model, 95% HPD 6.8–17.5). The log-SD 0.28 reads the asymmetric HPD as roughly a 95% interval (log-SD ≈ 0.24 from the HPD ratio) and slightly inflates it to cover the skew towards shorter doubling times, so the prior is modestly wider than the source but unbiased. The first reproduction number is then derived FORWARD from this r and OUR generation interval through Euler–Lotka (R0 = r_to_R0(r, g) in infection_model), so the cryptic exponential phase and the established renewal share ONE growth source — the sampled growth rate — and the established reproduction number is consistent with the genetic growth under our generation interval rather than pinned by a separate R0 prior.

m ~ truncated(Normal(5, 4); lower = 0) is deliberately WIDE. The centre and spread are elicited from the field epidemiology and the genetics: field work in Mongbwalu traced a sustained transmission chain back to a death on 25 January 2026 and identified 500+ suspected cases between mid-January and mid-May (kupferschmidt2026), and the genetic TMRCA (mbalaplacide2026) is a lower bound on the outbreak age consistent with an origin that early. Since m counts only the CRYPTIC doublings (not the cut-off case total), with the ≈11.7-day doubling a centre of 5 spans 5 · τ ≈ 58.5 cryptic days, placing the implied prior origin at the end of January (at the documented first deaths); the SD of 4 lets the data and the genetic seeding bound pull the origin earlier (into December) or later without over-committing (#533).

In the renewal, 2^m is the prior seed at the renewal start, which the renewal recursion grows forward under R_t. Pass m_prior to override (e.g. an m_prior whose centre advances via m_prior_centre for a later cut-off; the backfill's advancing centre is tracked separately by #534). Returns (; τ, r, m, T, C_T).

source
BVDOutbreakSize.exports_deaths_model Method
julia
exports_deaths_model(
    exports_deaths::Union{Missing, Integer},
    travelled_prevalence::AbstractVector,
    CFR::Real,
    od_pmf::AbstractVector,
    incubation_pmf::AbstractVector;
    export_death_days,
    pre_death_exports
) -> Any

Deaths-among-exported-cases likelihood, dated per-day. Deaths accrue among the TRAVELLED at-risk person-time q · prevalence from exports_model, BEFORE the export-case ascertainment p_uganda: a death among an exported case would be reported whether or not the case was ascertained as an import, so the death model is not thinned by p_uganda (unlike the export-case count). The day-t expected export-death increment is the CFR-scaled convolution of that travelled prevalence with the infection→death PMF,

so the cumulative export-death intensity Λ_d(t) is its running sum, the discrete analogue of the integral model's ∫ C(s)·S_det·F_death ds. The onset-to-death PMF od_pmf shared from deaths_model is convolved with the incubation PMF to give the infection→death distribution f_d, keyed to infection like the prevalence.

The observed Uganda export deaths are a dated series export_death_days (grid day-indices, ascending), fitted as an inhomogeneous Poisson exactly as exports_model fits the imports: one Poisson term per death day (the increment of Λ_d between consecutive death-day edges), a pre_death_exports ~ Poisson(Λ_d(δ₁−1)) zero term bounding the first death day δ₁, and the last_offset truncation that stops the clock at the last observed death day. With an empty export_death_days the model falls back to the cut-off cumulative Poisson exports_deaths ~ Poisson(Λ_d(n)).

source
BVDOutbreakSize.exports_deaths_only_model Method
julia
exports_deaths_only_model(
    n::Integer,
    exports_deaths::Union{Missing, Integer};
    export_death_days,
    breakpoint,
    source_population,
    infection,
    onset_incidence,
    deaths,
    exports,
    dispersion,
    ascertainment
) -> Any

Deaths-among-exports-only composer. Runs the infection process and onset staging, samples ascertainment and the deaths submodel (for the CFR and onset-to-death delay), then conditions on the export-deaths likelihood. See exports_deaths_model.

source
BVDOutbreakSize.exports_joint_only_model Method
julia
exports_joint_only_model(
    n::Integer,
    exported_cases::Union{Missing, Integer},
    exports_deaths::Union{Missing, Integer};
    export_case_days,
    export_death_days,
    breakpoint,
    source_population,
    infection,
    onset_incidence,
    deaths,
    exports,
    dispersion,
    ascertainment
) -> Any

Exports-joint composer: the Uganda export CASES and DEATHS fit together as one geographic-spread stream. Runs the infection process and onset staging, samples ascertainment and the deaths submodel (for the CFR and onset-to-death delay), then conditions on BOTH the export-case and export-death likelihoods over the one travel-gated at-risk prevalence, so the imports and the import deaths inform the outbreak size jointly rather than as two separate single-stream fits. Either count may be missing to drop it. See exports_model and exports_deaths_model.

source
BVDOutbreakSize.exports_model Method
julia
exports_model(
    exported_cases::Union{Missing, Integer},
    infections::AbstractVector,
    p_uganda::Real;
    export_case_days,
    pre_detection_exports,
    incubation_pmf,
    source_population,
    traveller,
    onset_to_detection
)

Uganda exports likelihood (geographic spread). The exports stream is travel-gated, so the at-risk clock starts at infection: a traveller moves and is exported during incubation (pre-symptomatic) and stays at risk of being exported and detected abroad only until the infection→detection delay has elapsed. The expected detected exports by the cut-off therefore accumulate the per-capita travel rate q = daily_travellers / source_population over the at-risk PERSON-TIME, not over single-day onset events:

with C(s) the cumulative infections and detected(s) the cumulative infections that have already completed the infection→detection delay by day s. The infection→detection delay is the sampled onset-to-detection delay convolved with the shared incubation PMF (so incubation sits inside it, keyed to infection like C(s)); detected is the running sum of convolve_delay(infections, f_det). Summing the daily at-risk prevalence is the discrete person-time integral, the discrete analogue of the integral model's at-risk person-time export integral; the earlier onset-incidence form summed q · onsets, charging each case only a single day of travel risk and so under-counting exports by roughly the mean at-risk dwell time. Samples the traveller volume and the onset-to-detection delay via injected submodels. The onset-to-detection prior is centred on the Ebola onset-to-hospitalisation delay (mean 5.0 d, SD 4.7 d; WHO Ebola Response Team 2014, NEJM), the delay from symptom onset to detection at a point of entry abroad.

Dated per-day likelihood

The observed Uganda imports are a dated series — export_case_days gives the grid day-index of each detection (sorted ascending) — fitted as an inhomogeneous Poisson process rather than a single cumulative count. The cumulative export intensity is Λ(t) = sum(export_prevalence[1:t]), so the per-day expected export increment Λ(t) − Λ(t−1) is just the day-t at-risk person-time export_prevalence[t]. The likelihood places one Poisson term per import at its detection day, the increment between consecutive detection-day edges (via bin_increments on the daily prevalence series), with two extra terms:

  • pre_detection_exports ~ Poisson(Λ(d₁−1)) observed at zero, the first-detection timing bound: no export is expected before the earliest detection day d₁. The first import's increment is measured from Λ(d₁−1), so the pre-detection weight and the import increments partition Λ(t_last) and the model still conditions on the same total as a cumulative single-Poisson would.

  • last_offset truncation: the travel-gated export clock stops at the last observed import t_last = day of the most recent detection. Days after the last import carry no informative zero (cross-border movement shifts over the outbreak and the most recent days are right-truncated by reporting lag), so prevalence past t_last does not accrue. With an empty export_case_days the model falls back to the cut-off cumulative Poisson exported_cases ~ Poisson(Λ(T)).

Returns the expected cumulative count at t_last, the per-capita travel rate and the daily at-risk prevalence for reuse by exports_deaths_model.

source
BVDOutbreakSize.exports_only_model Method
julia
exports_only_model(
    n::Integer,
    exported_cases::Union{Missing, Integer};
    export_case_days,
    breakpoint,
    source_population,
    infection,
    onset_incidence,
    exports,
    ascertainment
) -> Any

Exports-only composer (geographic-spread analogue). Runs the infection process and onset staging, samples ascertainment, then conditions on the exports likelihood only. See exports_model.

source
BVDOutbreakSize.fit_callback Method
julia
fit_callback(
    name::AbstractString;
    logdir,
    spec
) -> Union{Nothing, BVDOutbreakSize.var"#27#28"{BVDOutbreakSize.var"#29#30"{String, Int64, Base.RefValue{Int64}, ReentrantLock}}, BVDOutbreakSize.var"#combined_callback##0#combined_callback##1"}

Build the logging callback for a named model fit, selected by the BVD_FIT_LOG environment variable (or an explicit spec). This is the wiring the report build uses so every fit streams its progress without each call site repeating the callback construction.

Recognised spec values (case-insensitive), defaulting to "all" when BVD_FIT_LOG is unset:

  • "all" — both the dependency-free progress_callback (a <name>.log file under logdir) and the tensorboard_callback (a tensorboard/<name> run directory under logdir).

  • "progress" — the file progress stream only.

  • "tensorboard" (or "tb") — the TensorBoard stream only.

  • "none" — no logging; returns nothing. CI sets this to keep release builds quiet (BVD_FIT_LOG=none).

TensorBoard logging needs TensorBoardLogger loaded (it activates the tensorboard_callback method through the package extension). When it is requested but not loaded, the TensorBoard stream is skipped with a warning rather than erroring, so a build without using TensorBoardLogger still gets the file progress stream.

See also: combined_callback, nuts_sample.

source
BVDOutbreakSize.fit_diagnostics Method
julia
fit_diagnostics(
    chn
) -> NamedTuple{(:max_rhat, :min_ess_bulk, :n_divergent), <:Tuple{Float64, Float64, Any}}

NUTS fit-quality summary for one chain: the worst (maximum) R-hat and the smallest bulk effective sample size across parameters, and the number of divergent transitions.

source
BVDOutbreakSize.fit_parallel Method
julia
fit_parallel(thunks::AbstractVector; chains) -> Any
julia
fit_parallel(thunks; chains = 2)

Run independent model fits — each a zero-argument thunk returning a chain — with model-level parallelism bounded by the available threads. At most Threads.nthreads() ÷ chains fits run at once (so each fit keeps chains threads for its own chains), clamped to the number of fits. This is self-limiting and CI-safe: with two threads (e.g. CI's JULIA_NUM_THREADS=2, the default chains) it runs the fits SEQUENTIALLY, identical to a plain loop and with the same peak memory; on a many-core machine with more threads it fans the fits out (eight threads → four fits at once). Each fit seeds its own RNG, so the results do not depend on the schedule. Returns the chains in input order.

source
BVDOutbreakSize.forecast_archive Method
julia
forecast_archive(fcs; made_date, thin)
julia
forecast_archive(fcs; made_date, thin = 1) -> DataFrame

Long-format archive of one or more forecast_reported results made from a single cut-off, for the observed streams scored across releases. fcs is an iterable of (horizon, fc) pairs and made_date is the cut-off Date the forecasts were made from. Returns one row per (stream, horizon, draw) with columns made_date, horizon, target_date (made_date plus the horizon), stream, draw and value.

Only the incident and level quantities are archived: confirmed cases and confirmed deaths new over the horizon, recovered new over the horizon and the supply-limited isolation beds occupancy. When the forecast carries the confirmed/suspect ward split (confirmed_occupancy / suspect_occupancy, the occupancy partitioned by the cut-off confirmed share), the two ward occupancy levels are archived too as treatment beds (confirmed) and isolation beds (suspected), each scored against its own Tableau 6 occupancy sub-stock. The total isolation beds occupancy stays as its own stream. The cumulative totals are not archived because they are revised across data vintages, so scoring is done on the incident and level quantities instead. Streams a forecast does not carry (a single-stream fit, or a fit predating the ward split, say) are skipped. thin keeps every thin-th draw so the archive stays compact when it is saved as a release asset.

source
BVDOutbreakSize.forecast_reported Method
julia
forecast_reported(
    chn;
    horizon,
    obs_cases,
    obs_deaths,
    obs_confirmed,
    obs_confirmed_deaths,
    obs_recovered,
    seed
)

One-week-ahead (default horizon = 7 days) posterior-predictive forecast. For each draw, continue the reproduction number over the horizon (letting it keep evolving by carrying the walk's terminal drift forward and mapping back to a per-day growth rate), project the new counts each stream adds over the horizon from its cut-off daily incidence, add them to the cut-off cumulative and replicate. Returns a DataFrame with one row per draw and columns:

  • :cases_cum, :deaths_cum — replicated cumulative suspected reported cases and deaths by the cut-off plus the horizon: the observed cut-off cumulative plus the replicated new counts over the horizon, so the cumulative never falls below the cut-off even when the reproduction number is below one. The cut-off daily incidence projected forward is the last increment of the stream's cumulative trajectory when the chain carries it (:cumulative_confirmed for the laboratory cases), otherwise inferred from the cut-off cumulative total under exponential growth.

  • :confirmed_cum, :confirmed_deaths_cum — laboratory-confirmed case and confirmed-death counterparts, present when obs_confirmed and obs_confirmed_deaths are supplied.

  • :cases_new, … :confirmed_deaths_new — new counts over the coming week (*_cum minus the corresponding observed count at the cut-off, floored at zero).

  • :bed_demand, :isolation_level — the projected isolation/treatment-bed DEMAND (need under unconstrained supply) and the supply-limited occupancy it produces against the bed capacity, both at the horizon, present when the chain carries expected_bed_demand_T and bed_capacity. The demand grows by the horizon factor like the inflow; the occupancy is that demand capped at the capacity, min(demand, C) (matching the fitted occupancy), so bed_demand − isolation_level is the projected bed shortfall. Replicated with the isolation stream's own dispersion.

  • :admissions_fc, :incare_deaths_fc, :ruleouts_fc — the projected one-week-ahead daily isolation/treatment flows (new admissions, in-care deaths and rule-outs), present when the chain carries expected_admissions_T, expected_incare_deaths_T and expected_ruleouts_T. Each cut-off daily flow grows by the horizon factor and is replicated through the isolation stream's dispersion, mirroring the bed-demand projection.

  • :recovered_cum, :recovered_new — cumulative recovered-among-confirmed by the horizon (the cut-off cumulative plus the projected new this week) and the new this week, present when the chain carries expected_recovered_T. The cut-off cumulative is obs_recovered when supplied, otherwise the fitted expected_recovered_T.

  • :infections_new, :onsets_new, :deaths_latent_new — new latent infections, symptom onsets and deaths projected over the horizon under the evolving growth rate (deterministic per-draw quantities, so they carry parameter uncertainty only). These are the unobserved counterparts of the observed-stream forecasts above.

  • :rt_forecast — the reproduction number at the end of the horizon, the walk's terminal drift continued forward (it evolves rather than freezing at the cut-off R_T).

Reads :r, :T, :expected_reports_T, :expected_deaths_T, :expected_infections_T, :R_T, :k, the reproduction-number walk and generation-interval parameters (to let the reproduction number keep evolving over the horizon), the cumulative-onset and cumulative-death trajectories (for the latent onset and death forecasts) and (for the laboratory streams) :expected_confirmed_T, :cumulative_confirmed and :expected_confirmed_deaths_T from chn. The suspected-case and suspected-death daily incidence at the cut-off is taken from :cumulative_reports / :cumulative_deaths_total when carried, otherwise inferred from the cut-off cumulative total and :T. When the walk parameters are not carried (single-stream fits) the cut-off growth rate is held constant instead. Exports are not forecast: cross-border travel is unlikely to continue at its baseline rate, so the forward travel rate the export model relies on no longer holds. The reproduction number is allowed to keep evolving over the horizon, but no further interventions and no saturation are imposed.

source
BVDOutbreakSize.forecast_score_by_horizon Method
julia
forecast_score_by_horizon(
    scores::DataFrames.DataFrame;
    joint_fit,
    baseline_fit
) -> DataFrames.DataFrame

The same columns as forecast_score_overview, one row per (stream, horizon, fit) rather than pooled across every horizon, so a fit that beats the baseline on average but not at every cut-off is visible.

source
BVDOutbreakSize.forecast_score_by_release Method
julia
forecast_score_by_release(
    scores::DataFrames.DataFrame;
    joint_fit,
    baseline_fit
) -> DataFrames.DataFrame

The same columns as forecast_score_overview, one row per (stream, fit, made_date), averaged across horizons rather than pooled over releases too, baseline rows excluded, so the by-release detail table stays compact.

source
BVDOutbreakSize.forecast_score_overview Method
julia
forecast_score_overview(
    scores::DataFrames.DataFrame;
    joint_fit,
    baseline_fit
) -> DataFrames.DataFrame

One row per (stream, fit), pooled over every horizon and release in scores (a data/forecast_scores.csv-shaped table, as written by scripts/score_releases.jl), baseline rows excluded. This is the headline scoring table: n, the mean CRPS and log-scale CRPS, the CRPS decomposition (dispersion, overprediction, underprediction), the 50% and 90% coverage and bias, the relative skill against the stream's persistence baseline on both scales, and, on the joint row of a stream that has an individual single-stream fit, the relative skill against that fit on both scales.

Every relative skill is the ratio of the two aggregate mean scores over the matched set of forecasts both sides scored, not a mean of per-forecast ratios, so one forecast whose comparator scored zero cannot make the ratio infinite. A ratio is missing, never Inf or NaN, when the comparator's mean is zero or the ratio itself is not finite.

Returns a typed zero-row frame when scores is empty, so the report renders before any release carries a forecast.

source
BVDOutbreakSize.forecast_stream Method
julia
forecast_stream(
    chn,
    stream::Symbol;
    horizon,
    obs_value,
    n,
    breakpoint,
    rt_start,
    rt_walk_start,
    seed
)
julia
forecast_stream(chn, stream; horizon, obs_value, n, breakpoint) -> Vector

Posterior-predictive forecast of a SINGLE observed stream from either a single-stream fit or the joint, returning one replicate per draw. Where forecast_reported projects every stream the joint carries at once, this projects one named stream and resolves its chain keys for both fit kinds, so each single-stream fit can be scored on forecasting its own dataset against the joint and against a baseline.

stream is one of :reported_cases, :suspected_deaths, :confirmed_cases, :confirmed_deaths, :isolation_beds and :exports. The incident streams (everything but :isolation_beds) return the NEW count accrued over the horizon, matching forecast_archive's convention; :isolation_beds returns the supply-limited occupancy LEVEL at the horizon (the projected demand replicate capped at the bed capacity, min(demand, C), as the fitted occupancy is).

obs_value is the stream's observed count at the cut-off: the cumulative total for the incident streams (the base the projected new counts accrue on top of, mirroring forecast_reported), or the observed occupancy for :isolation_beds. Since the incident streams return the increment rather than the cumulative, and the level stream is projected from the fitted stock, it anchors the reported quantity rather than changing it.

The reproduction number keeps evolving over the horizon exactly as in forecast_reported. The joint carries the cut-off R_T, r and T as un-prefixed deterministics; a single-stream fit carries none of them, so they are rebuilt from the reproduction-number walk (reconstruct_rt) and the generation interval, which every chain does carry. That needs the grid length n and the intervention breakpoint — data rather than chain contents. Pass both for a single-stream chain (with rt_start / rt_walk_start if the fit used non-default ones); without them a single-stream chain cannot be projected and an error says so. They are unnecessary for the joint, which is read directly.

Exports are forecast here for completeness of the per-stream comparison, but the caveat forecast_reported documents still applies: the export model relies on a forward travel rate that cross-border movement is unlikely to keep once the outbreak is known, so the export projection assumes a baseline travel rate that is unlikely to hold.

source
BVDOutbreakSize.forecast_table Method
julia
forecast_table(
    fc::DataFrames.DataFrame;
    digits
) -> DataFrames.DataFrame

Summarise a forecast_reported result into a DataFrame with one row per confirmed stream (laboratory-confirmed cases and confirmed deaths) and quantity (cumulative total by the cut-off plus the horizon, or new this week), reporting the equal-tailed 30/60/90% credible interval endpoints (lower_90 … upper_90) used by the other summary tables. The suspected reported-case and suspected-death streams are no longer reported, so they are not shown as forecast targets.

source
BVDOutbreakSize.forecast_vs_truth Method
julia
forecast_vs_truth(
    fc::DataFrames.DataFrame;
    observed,
    baseline,
    isolation,
    digits
)

Validate a forecast_reported projection against the counts that were later observed. observed is a NamedTuple mapping each stream's cumulative column (:confirmed_cum, :cases_cum, …) to its observed cumulative count at the forecast target date; baseline maps the same columns to the cumulative count at the forecast origin (default 0). A stream is scored only when its cumulative column is in the forecast and a key for it is in observed. The scored streams are the reported cases, suspected deaths, laboratory-confirmed cases, confirmed deaths and recovered — the same set plot_forecast_vs_truth draws — so the call site can pass the same observed/baseline NamedTuples it builds for the plot.

Each scored stream gets two rows, mirroring the plot's two panels and the Quantity split of forecast_table: a cumulative by T+7 row scoring the projected cumulative against observed, and a new this week row scoring the projected new count against max(observed − baseline, 0). When isolation (the observed bed occupancy at the target date) is supplied and the forecast carries the beds, the projected supply-limited occupancy is scored against it too as a single level row. Returns a DataFrame with the observed count, the equal-tailed 30/60/90% predictive intervals (the same endpoints as the other summary tables), and whether the observed count falls inside the 90% interval.

Note that at a one-week-back freeze the bed capacity is weakly informed (the reported occupancy rate starts only on 9 June), so the projected bed occupancy rides the capacity random walk back to the freeze date and its interval is wide.

source
BVDOutbreakSize.forecast_vs_truth_trajectory Method
julia
forecast_vs_truth_trajectory(chn; targets, seed)

Roll the one-week-ahead forecast across an observed cumulative trajectory. targets is a vector of (label, horizon_days, observed_cumulative) triples: for each, the fitted current growth rate r is projected horizon_days past the cut-off and the predicted cumulative reported cases compared against observed_cumulative. Returns a DataFrame with one row per target giving the horizon, the observed count, the equal-tailed 30/60/90% predictive intervals, and whether the observed count falls inside the 90% interval. Unlike forecast_vs_truth, which scores only the endpoint, this scores the whole observed trajectory across the horizon. Reads :r, :expected_reports_T and :k from chn.

source
BVDOutbreakSize.freeze_observations Method
julia
freeze_observations(
    cutoff_date::Union{Dates.Date, AbstractString};
    path,
    seeding_lead
) -> NamedTuple{(:n, :cutoff, :seeding, :exported_cases, :exports_deaths, :export_case_days, :export_death_days, :total_deaths, :reported_cases, :confirmed_cases, :confirmed_deaths, :tests_analysed, :reported_history, :confirmed_history, :confirmed_deaths_history, :deaths_history, :lab_history, :lab_daily_history, :suspected_daily_history, :suspected_daily_deaths_history, :isolation_history, :bed_capacity_history, :recovered_history, :recovered_cases, :treatment_admissions_history, :treatment_deaths_history, :treatment_ruleout_history, :treatment_absconded_history, :treatment_confirmed_incare_history, :treatment_suspect_incare_history, :occupancy_break_days, :confirmed_break_days, :confirmed_break_gross_cases, :confirmed_break_gross_deaths, :tests_received_history, :tmrca_days, :who_first_sitrep_days), <:Tuple{Int64, Any, Any, Any, Any, Any, Any, Any, Any, Any, Any, Any, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, Any, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, Any, Any, Any, Any, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, Int64, Any}}
julia
freeze_observations(cutoff_date; path = default manifest)

Load the observation manifest FROZEN to cutoff_date: the cut-off is moved to cutoff_date and every dated history is truncated to the vintages available by then, so the returned named tuple is what the renewal model would have seen on that date. The cut-off scalar totals (reported_cases, total_deaths, confirmed_cases, ...) are taken from the truncated histories rather than the manifest's full-data scalars. Use it to re-evaluate the renewal estimate at a past report date (for example a McCabe et al. situation-report cut-off) for a like-for-like, matched-in-time comparison.

cutoff_date accepts a Date or an ISO date string. It must be on or after the earliest history vintage in the manifest (the renewal DRC series begins 18 May 2026); an earlier date leaves the suspected streams empty and is not a meaningful renewal fit.

source
BVDOutbreakSize.gamma_delay_model Method
julia
gamma_delay_model(nmax::Integer; alpha_prior, theta_prior)

Natural-parameter Gamma delay submodel. Samples a Gamma SHAPE α and SCALE θ directly from the priors, builds Gamma(α, θ), and discretises to a daily PMF over lags 0 … nmax by double interval censoring (discretise_censored), keeping the lag-0 bin (an onset-to-event delay can be same-day, unlike the generation interval). This carries a line-list delay reanalysis through on its NATURAL parameters with the reported posterior uncertainty, rather than moment-matching a LogNormal from mean/SD priors. Gamma shape/scale differentiate cleanly under Mooncake. Returns (; pmf, dist, mean, sd, alpha, theta).

source
BVDOutbreakSize.gamma_median_wh Method
julia
gamma_median_wh(mean::Real, sd::Real) -> Any

Wilson–Hilferty approximation to the continuous median of a Gamma as a function of its mean and sd: median ≈ mean·(1 − sd²/(9·mean²))³. Smooth in the mean and SD with no quantile inversion, so it enters a gradient-based likelihood cleanly, and accurate to a few percent for the shapes here. Used to match the confirmed onset-to-sample convolution's continuous median to the cohort's reported median.

source
BVDOutbreakSize.gate_before Method
julia
gate_before(v::AbstractVector, start::Integer) -> Any

Zero a daily series v before grid day start (1-based), modelling a process that did not exist before start. Used to gate the laboratory analysed-specimen CAPACITY before testing began: no specimens are analysed before the testing system exists, so the modelled analysed volume (and the confirmed counts derived from it) must not accrue over the pre-surveillance cryptic phase. The suspected-case and suspected-death streams are NOT gated — those counts did accumulate over the cryptic phase, so their first per-vintage bin legitimately rolls from grid day 1. start ≤ 1 returns v unchanged. Pure and AD-transparent; the element type follows v. Callers concretise any Vector{Any} (predict mode) before gating, so zero(eltype(v)) is well defined.

source
BVDOutbreakSize.generation_interval_model Method
julia
generation_interval_model(
    nmax::Integer;
    alpha_prior,
    theta_prior
) -> Any

Generation-interval submodel. Samples a Gamma SHAPE α and SCALE θ, parameterised directly from the source's reported generation-time distribution rather than moment-matching a LogNormal from mean/SD priors. The source is the Ebola virus disease serial interval as a generation-time proxy (mean 15.3 d, SD 9.3 d; WHO Ebola Response Team 2014, NEJM), which maps once to a Gamma shape α ≈ 2.71 and scale θ ≈ 5.65 (α = (mean/sd)², θ = sd²/mean). The priors are centred on those values: α ~ Normal⁺(2.71, 0.7) and θ ~ Normal⁺(5.65, 1.5), lower-truncated to keep the Gamma well defined. The SDs propagate the source's reported uncertainty (the NEJM serial-interval mean carries a 95% CI of 13.0–17.6 d, an SD on the mean of ≈1.17 d) — this is the source's reported uncertainty, NOT a self-assigned spread. Gamma shape/scale differentiate cleanly under Mooncake, so this is AD-stable.

Discretised through the same double-interval-censoring route as the other delays (discretise_censored); the lag-0 bin is dropped and the remainder renormalised, left-truncating the generation interval at one day so an infectee is infected strictly after its infector. Returns (; g, gi_mean, gi_sd, gi_alpha, gi_theta).

source
BVDOutbreakSize.genetic_seeding_model Method
julia
genetic_seeding_model(
    T::Real,
    tmrca_days::Union{Missing, Real};
    tmrca_days_sd
) -> Any

One-sided molecular-clock seeding bound on the outbreak age T (see infection_model). The TMRCA is treated as a right-censored, noisy reading of the seeding time, so deeper or wider sampling only pushes it older; the likelihood contributes P(read ≥ tmrca_days). Passing tmrca_days = missing makes the submodel a no-op.

source
BVDOutbreakSize.independent_ascertainment_model Method
julia
independent_ascertainment_model(
;
    drc_prior,
    uganda_prior
) -> DynamicPPL.Model{typeof(independent_ascertainment_model), (), (:drc_prior, :uganda_prior), (), Tuple{}, Tuple{Distributions.Normal{Float64}, Distributions.Normal{Float64}}, DynamicPPL.DefaultContext, false}

Independent ascertainment fractions for the DRC and Uganda surveillance systems. The two countries run different surveillance systems — DRC passive community surveillance and Uganda point-of-entry / hospital detection — so each ascertainment fraction has its own logit-scale prior with no shared parameter. An alternative to the composer-default pooled_ascertainment_model for sensitivity analyses that do not share strength between the two systems.

Both fractions default to a logit-Normal prior centred on a reporting fraction of 0.75 with SD 0.6 (95% support roughly 0.48–0.91), reflecting the active case-finding and contact tracing of a declared Ebola response rather than baseline passive surveillance. Pass drc_prior / uganda_prior to set the two systems' priors separately.

source
BVDOutbreakSize.infection_model Method
julia
infection_model(
    n::Integer;
    breakpoint,
    rt_start,
    rt_walk_start,
    rt,
    gi,
    growth,
    gi_nmax
) -> Any

Generating infection process for the two-phase renewal seeding. Samples the generation interval and the cryptic exponential growth rate r (the prior sits on r, the molecular-clock growth, in exponential_growth_model), then derives the SINGLE established reproduction number R0 (= the first R_t) FORWARD from that r and the generation interval through Euler–Lotka (R0 = r_to_R0(r, g)) and passes log R0 as the walk base to the reproduction-number submodel. The cryptic phase and the established renewal therefore share ONE growth source — the sampled growth rate r — rather than the cryptic phase carrying a separate clock-rate prior or the walk asserting a separate R0 prior.

The renewal runs only over the observation window [renewal_start, cut-off], where the renewal_start is the genetic-TMRCA grid day rt_start (the day the reproduction-number walk starts; before it R_t is held flat). The cryptic exponential phase from the origin to the renewal start is analytic and off the renewal grid except for the days needed as recursion history. The seed AT the renewal start is the cryptic-phase realised size 2^m (seed_at_renewal_start), where m counts the doublings during the cryptic phase: the magnitude is r-INDEPENDENT, so r (hence the derived R0) leaves the seed magnitude entirely and appears only in the renewal growth. An earlier formulation back-scaled 2^m e^{−r·τ_obs}, putting r into both the seed and the renewal growth so the two cancelled for a fixed realised size — a flat ridge along which R0 slid to 1. The grid days 1 … renewal_start before the renewal start are filled smoothly by the cryptic exponential curve at rate r ending at 2^m (seed_infections), giving the recursion a full generation interval of differentiable history; the renewal recursion (renewal_infections) then grows the trajectory over renewal_start+1 … n under the time-varying R_t.

The TOTAL outbreak age is T = m·τ + τ_obs (cryptic duration plus the observation span τ_obs = n − renewal_start); the genetic seeding bound is applied to this total T at the composer. The renewal start sits a small lead AFTER the genetic TMRCA day (past the TMRCA uncertainty, where sustained transmission is confident), so τ_obs = n − renewal_start < tmrca_days and the censored bound tmrca ~ censored(Normal(T, sd); upper = tmrca_days) stays INFORMATIVE: it pulls the origin to sit at or before the MRCA, so the cryptic duration m·τ cannot be too short. The genetic bound therefore defines the cryptic-phase length through T.

The realized cut-off size is C_T = cumulative[n]. The breakpoint is forwarded to the reproduction-number submodel. Exposes the daily infections and cumulative sum, the total prior outbreak age T, cryptic doubling count m/τ and prior size scale C_T_prior, the realized cut-off size C_T, the established reproduction number R0 and its implied cryptic rate r0 (with doubling_time_initial), the current growth r/doubling_time derived from the cut-off reproduction number Rt[n] through forward Euler–Lotka (so r is sign-consistent with R_T := Rt[n] by construction), and the diagnostic-only seeding_age. Returns (; infections, cumulative, Rt, g, seed_at_renewal_start, m, τ, R0, r0, r, doubling_time_initial, T, C_T, C_T_prior, doubling_time, seeding_age).

source
BVDOutbreakSize.interpolate_knots Method
julia
interpolate_knots(
    knot_vals::AbstractVector,
    days::AbstractVector{<:Integer},
    n::Integer
) -> Any

Linearly interpolate the knot values knot_vals, placed on the day indices days, onto the full daily grid 1:n, returning the length-n series. Piecewise-linear between bracketing knots, so the series bends only at the knots and is otherwise straight; applied on the log-R_t scale this gives weekly random-walk knots with within-week linear interpolation. Type-stable and AD-transparent (the output element type follows knot_vals).

Outside the knot span the series is held FLAT at the nearest knot value (the interpolation fraction is clamped to [0, 1]), not extrapolated: days before the first knot take knot_vals[1] and days after the last take knot_vals[end]. This is what lets the reproduction-number walk start at a day > 1 and hold R_t flat at the established R0 (the first knot value) over every earlier day, rather than running the first segment's slope backwards off the start of the grid.

source
BVDOutbreakSize.isolation_admission_model Method
julia
isolation_admission_model(
;
    p_prior
) -> DynamicPPL.Model{typeof(isolation_admission_model), (), (:p_prior,), (), Tuple{}, Tuple{Distributions.Beta{Float64}}, DynamicPPL.DefaultContext, false}

Base treatment-admission probability for the isolation-occupancy stream (treatment_flow_model). Samples p_iso, the fraction of ascertained suspected cases that are admitted to and retained in an isolation/treatment bed at the base (non-BVD rule-out) intensity, so the modelled bed occupancy is p_iso times the survival-convolution of the admission inflow. BVD suspects are admitted at a higher rate skewed up from this base by a severity log-odds (isolation_severity_model), since triage admits the sicker patients and BVD presents more severely.

The default Beta(2, 2) is weakly informative on (0, 1) with mean ½ and no mass piled at the bounds. p_iso is partially confounded with the length-of-stay mean for the occupancy level (Little's law: mean occupancy ≈ p_iso · admissions · (E[LOS] + 1)), so the length-of-stay prior carries the duration and p_iso absorbs the admission/retention fraction; the length-of-stay also sets the lag and smoothing of occupancy relative to the inflow, which the daily occupancy series identifies. Pass p_prior to override. Returns (; p_iso).

source
BVDOutbreakSize.isolation_severity_model Method
julia
isolation_severity_model(
;
    logodds_prior
) -> DynamicPPL.Model{typeof(isolation_severity_model), (), (:logodds_prior,), (), Tuple{}, Tuple{Distributions.Truncated{Distributions.Normal{Float64}, Distributions.Continuous, Float64, Float64, Nothing}}, DynamicPPL.DefaultContext, false}

Severity skew for isolation admission (treatment_flow_model). Samples δ_iso ≥ 0, the log-odds by which a BVD suspect is more likely to be admitted to and retained in an isolation bed than a non-BVD rule-out at the same base intensity p_iso (isolation_admission_model), so the BVD admission probability is logistic(logit(p_iso) + δ_iso). Admission cannot condition on the unobserved BVD status of a suspect; the skew instead represents the net effect of severity-based triage, where the sicker patients are isolated and BVD presents more severely, enriching BVD among the admitted. The non-negative truncation keeps a BVD suspect at least as likely to be admitted as a rule-out.

The isolation stream observes only total occupancy, so the skew is weakly identified from it alone; its effect is to enrich the long-stay BVD component of demand, which the occupancy persistence informs only mildly, so the half-normal truncated(Normal(0, 0.75); lower = 0) carries most of the weight (δ_iso = 0 recovers a shared admission rate). Pass logodds_prior to override. Returns (; δ_iso).

source
BVDOutbreakSize.knot_days Method
julia
knot_days(n::Integer; week, start) -> Any

Day indices of the weekly reproduction-number knots over an n-day grid. The first knot sits on day start (default 1) and the last knot on day n, with regular knots every week days, so a knot is pinned to start and to the end of the grid. With start > 1 the reproduction number is held flat (at the first knot's value) for all days before start (interpolate_knots clamps below the first knot), so the random walk only varies R_t from start onward — used to fix R_t over the pre-establishment seeding window before the genetic TMRCA bound. Returns a sorted vector of unique day indices.

source
BVDOutbreakSize.lab_delay_model Function
julia
lab_delay_model(
;
    ...
) -> DynamicPPL.Model{typeof(lab_delay_model), (:nmax,), (:mean_prior, :sd_prior), (), Tuple{Int64}, Tuple{Distributions.Truncated{Distributions.Normal{Float64}, Distributions.Continuous, Float64, Float64, Nothing}, Distributions.Truncated{Distributions.Normal{Float64}, Distributions.Continuous, Float64, Float64, Nothing}}, DynamicPPL.DefaultContext, false}
lab_delay_model(nmax::Integer; mean_prior, sd_prior) -> Any

Report-to-laboratory-confirmation (lab-turnaround) delay submodel. The delay from a suspected case being reported to its specimen being laboratory confirmed, discretised to a daily PMF over lags 0 … nmax by censored_delay_model so it convolves cleanly onto the renewal onsets. The mean and SD carry weakly-informative priors centred on a short turnaround with a heavy right tail allowing for specimen shipment to a confirmatory lab. No per-sample outbreak data grounds this delay, so the likelihood does not identify the turnaround mean or SD; the priors are therefore kept tight around the documented turnaround belief (mean ≈ 4.5 d, SD ≈ 4 d) rather than wide, since a wide prior on an unidentified nuisance delay only makes the sampler wander it (it was the worst-mixing quantity in the joint, dragging the confirmation PMFs convolved from it). Returns (; pmf, dist, mean, sd).

source
BVDOutbreakSize.late_confirmed_model Function
julia
late_confirmed_model(
    increments::Union{Missing, AbstractVector},
    modelled::AbstractVector,
    analysed::AbstractVector{<:Integer},
    p_pos::AbstractVector,
    k::Real
) -> Any
late_confirmed_model(
    increments::Union{Missing, AbstractVector},
    modelled::AbstractVector,
    analysed::AbstractVector{<:Integer},
    p_pos::AbstractVector,
    k::Real,
    ρ::Real
) -> Any

Late confirmed-vintage likelihood, one per-window confirmed increment over the days after the last cumulative laboratory date. Each window scores its increment two ways depending on whether a 24h analysed denominator was published for that day (analysed[i] > 0): an anchored day is an overdispersed BetaBinomial(analysed[i], p_pos[i], ρ) of the observed denominator (like an observed window, see safe_betabinomial), a unanchored day a NegativeBinomial of the modelled laboratory volume modelled[i] sharing the surveillance dispersion k. Keeping both in one submodel preserves a single per-window predict-key vector (<prefix>.increments[i]) over all late days in order, so the posterior-predictive trajectory reconstructs without interleaving.

increments is the model argument on the LHS of ~: a supplied vector is observed data DynamicPPL conditions on and a missing argument is sampled (the predictive-generator path). A Vector{Union{Missing, Int}} with some entries missing scores only the present ones, used to observe the anchored days while leaving the unanchored days latent under the no-extrapolation probe.

source
BVDOutbreakSize.load_observations Function
julia
load_observations(
;
    ...
) -> NamedTuple{(:n, :cutoff, :seeding, :exported_cases, :exports_deaths, :export_case_days, :export_death_days, :total_deaths, :reported_cases, :confirmed_cases, :confirmed_deaths, :tests_analysed, :reported_history, :confirmed_history, :confirmed_deaths_history, :deaths_history, :lab_history, :lab_daily_history, :suspected_daily_history, :suspected_daily_deaths_history, :isolation_history, :bed_capacity_history, :recovered_history, :recovered_cases, :treatment_admissions_history, :treatment_deaths_history, :treatment_ruleout_history, :treatment_absconded_history, :treatment_confirmed_incare_history, :treatment_suspect_incare_history, :occupancy_break_days, :confirmed_break_days, :confirmed_break_gross_cases, :confirmed_break_gross_deaths, :tests_received_history, :tmrca_days, :who_first_sitrep_days), <:Tuple{Int64, Any, Any, Any, Any, Any, Any, Any, Any, Any, Any, Any, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, Any, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, Any, Any, Any, Any, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, Int64, Any}}
load_observations(
    path::AbstractString;
    seeding_lead,
    cutoff_date
) -> NamedTuple{(:n, :cutoff, :seeding, :exported_cases, :exports_deaths, :export_case_days, :export_death_days, :total_deaths, :reported_cases, :confirmed_cases, :confirmed_deaths, :tests_analysed, :reported_history, :confirmed_history, :confirmed_deaths_history, :deaths_history, :lab_history, :lab_daily_history, :suspected_daily_history, :suspected_daily_deaths_history, :isolation_history, :bed_capacity_history, :recovered_history, :recovered_cases, :treatment_admissions_history, :treatment_deaths_history, :treatment_ruleout_history, :treatment_absconded_history, :treatment_confirmed_incare_history, :treatment_suspect_incare_history, :occupancy_break_days, :confirmed_break_days, :confirmed_break_gross_cases, :confirmed_break_gross_deaths, :tests_received_history, :tmrca_days, :who_first_sitrep_days), <:Tuple{Int64, Any, Any, Any, Any, Any, Any, Any, Any, Any, Any, Any, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, Any, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, Any, Any, Any, Any, NamedTuple{(:days, :counts), <:Tuple{Any, Any}}, Int64, Any}}

Load the BVD observation manifest from path (a dated TOML file) and return a named tuple for the renewal model. Calendar dates are converted to 1-based grid day-indices (day 1 is the seeding day, day n the cut-off) using the cut-off (as_of_date) and a seeding day placed seeding_lead days before the genetic TMRCA date.

Returns the grid length n, the cutoff and seeding dates, the per-stream cumulative totals at the cut-off (reported_cases, total_deaths, confirmed_cases, confirmed_deaths, tests_analysed, exported_cases, exports_deaths), the dated Uganda export series as grid day-indices (export_case_days, export_death_days, each a sorted list of detection/death days on or before the cut-off, fitted with a per-day Poisson likelihood), the per-vintage histories as (; days, counts) with days the grid day-indices (reported_history, confirmed_history, confirmed_deaths_history, deaths_history, lab_history from the cumulative analysed-specimen series, lab_daily_history from the post-cutoff 24h analysed counts on the trusted post-cutoff days, suspected_daily_history from the post-cutoff daily new-suspect inflow ("nouveaux cas suspects du jour"), suspected_daily_deaths_history from the post-cutoff daily new suspected deaths ("cas suspects du jour N (M deces)", the deaths analogue), isolation_history from the post-cutoff daily isolation/hospitalisation occupancy ("Patients en isolement", a daily bed count fitted by the length-of-stay submodel), bed_capacity_history from the implied bed capacity (occupancy / reported occupancy rate), recovered_history from the cumulative recovered-among-confirmed series ("cumul guéris"), treatment_confirmed_incare_history and treatment_suspect_incare_history from the Tableau 6 occupancy split (dont confirmes (NC+AC) and dont suspects, two prevalence sub-stocks that sum to the total occupancy), tests_received_history), the genetic TMRCA bound tmrca_days (days before the cut-off), and who_first_sitrep_days (days from the first situation report, the earliest reported-case vintage, to the cut-off). The intervention breakpoint grid day is n - who_first_sitrep_days. A cut-off scalar with no explicit TOML block is derived from the final vintage of the matching history.

source
BVDOutbreakSize.log_crps_sample Method
julia
log_crps_sample(
    obs::Real,
    samples::AbstractVector{<:Real}
) -> Any

CRPS on the log scale: both obs and every element of samples are log1p-transformed before scoring. This is scoring on the log scale, i.e. crps_sample(log1p(obs), log1p.(samples)) — it is not the logarithm of crps_sample. Log-scale scoring downweights the influence of large counts, appropriate when forecast accuracy matters proportionally rather than in absolute terms (e.g. case counts spanning orders of magnitude).

source
BVDOutbreakSize.lognormal_meansd Method
julia
lognormal_meansd(mean, sd) -> Distributions.LogNormal

LogNormal with the given mean and standard deviation sd, by moment matching var = mean^2 (exp(σ^2) − 1). The inputs are passed through safe_rate first so a NaN-prone warmup proposal cannot push σ = sqrt(log1p(·)) into NaN territory and trip the LogNormal domain check. Used by every delay submodel so a delay is parameterised by its mean and SD rather than the log-scale parameters.

source
BVDOutbreakSize.m_prior_centre Method
julia
m_prior_centre(
    as_of_date::Union{Dates.Date, AbstractString};
    base_date,
    m_base,
    doubling_days
) -> Float64
julia
m_prior_centre(as_of_date; base_date, m_base, doubling_days)

Centre for the doubling-count prior m, based on m_base doublings at base_date and advancing by one doubling per doubling_days of elapsed time to as_of_date:

The base is McCabe et al.'s first report (18 May 2026; Method 2 central 501 cases ⇒ m ≈ 9), advancing at the outbreak-specific doubling time (M_PRIOR_DOUBLING_DAYS, the BEAST X estimate of mbalaplacide2026, mean 11.7 d), so the prior stays centred on the plausible outbreak size as the cut-off moves. C_T = 2^m is the cumulative infection count; 9 is a weakly-informative centre of the same order. Passed into exponential_growth_model as the centre of the wide m prior.

source
BVDOutbreakSize.markdown_table Method
julia
markdown_table(
    df::DataFrames.DataFrame
) -> Union{Base.AnnotatedString{String}, String}

GitHub-flavoured markdown table for a DataFrame, one header row from the column names and one body row per data row. Used to persist a rendered summary table to disk so a static documentation page can embed it without re-running the fit.

source
BVDOutbreakSize.nejm_onset_to_sample Method
julia
nejm_onset_to_sample(
;
    mean,
    mean_se,
    median,
    median_se
) -> @NamedTuple{mean_obs::Float64, mean_se::Float64, median_obs::Float64, median_se::Float64}

Onset-to-sample prior configuration from the NEJM DRC 2026 BVD cohort (Akilimali et al. 2026, doi:10.1056/NEJMc2608070). The confirmed-positive onset-to-sample interval (N = 129) was estimated as a continuous Gamma through the epidist marginal model correcting for double interval censoring and right truncation, chosen over lognormal and Weibull by LOOIC. The cohort reports a continuous mean of 7.4 d (95% CrI 5.3–13.5) and median of 4.8 d (95% CrI 3.46–7.84).

The confirmed onset→report→receipt convolution is grounded on its mean and median. The mean is the sum of the two legs' means and the variance the sum of their variances; the median follows by gamma_median_wh. Each is fitted to the reported value as a Normal observation whose SD is the reported 95% CrI half-width over 1.96 (mean_se, median_se), so the cohort's uncertainty enters directly and the constraint is soft. Returns a NamedTuple (; mean_obs, mean_se, median_obs, median_se) for the onset_to_sample argument of bvd_joint.

source
BVDOutbreakSize.nuts_sample Method
julia
nuts_sample(
    model;
    samples,
    chains,
    target_accept,
    max_depth,
    n_adapts,
    seed,
    progress,
    adtype,
    init,
    check_model,
    callback,
    warmup,
    kwargs...
) -> Any

NUTS on model, parallel chains via MCMCThreads. Chains initialise from the prior (InitFromPrior()) to keep the sampler in regions with reasonable physical interpretation. Pass init = Turing.DynamicPPL.InitFromUniform() to fall back to unconstrained uniform initialisation.

target_accept defaults to 0.85. The earlier integral model needed 0.95 to keep the multimodal small-outbreak geometry from diverging, but the renewal joint conditions the confirmed counts on the observed analysed denominator (removing the multiplicative ascertainment ridge) and samples the random-walk and ascertainment blocks in non-centred form, so the posterior geometry is benign (the sanity fit converges with ≈1 divergence). A lower target acceptance shortens the average NUTS trajectory, cutting leapfrog steps (and so gradient evaluations) per iteration, so 0.85 trims the per-iteration gradient cost while staying above the conventional 0.8 floor; raise it back toward 0.9–0.99 if a model variant reintroduces divergences. The default is two chains (500 post-warmup draws each, 1000 total) rather than four shorter ones, mirroring the integral model (#211). The total was halved from 2000 draws to trade some effective sample size for a roughly halved sampling wall-clock.

check_model = false disables Turing's pre-sampling model check, which rejects any model with a sampled discrete variable even when its value feeds nothing downstream. The per-vintage DRC streams are now scored as observed ~ data, so a composer conditioning on them passes the check with the default check_model = true. The escape is needed only by exports_deaths_only_model, which runs the exports submodel in predictive mode (exported_cases ~ Poisson with a missing count) purely for the export onsets, leaving a sampled discrete Poisson draw. The continuous parameters are unaffected.

Pass callback to stream live fit progress (iteration, log-density, divergences) instead of waiting for the whole fit. Use progress_callback for a dependency-free file/stdout stream, or tensorboard_callback for a TensorBoard backend (requires using TensorBoardLogger). The callback is forwarded to sample only when non-nothing. Any additional kwargs are passed through to sample.

n_adapts sets the NUTS warmup length (step-size and mass-matrix adaptation), run in addition to samples and discarded by default. It defaults to min(200, samples ÷ 2), capping the per-fit warmup below Turing's default of min(1000, samples ÷ 2) to speed the report build. At the default samples = 500 the cap gives 200 (below samples ÷ 2 = 250); it tightens further only when a caller drops samples below 400.

A callback fires only on the samples that are kept, and NUTS discards its adaptation phase by default, so warmup is silent. Set warmup = true to keep the adaptation steps (discard_adapt = false), which streams them to the callback so step-size adaptation and early divergences are visible live. Those warmup draws are then also retained in the returned chain, so the first n_adapts draws are adaptation steps rather than posterior samples; raise samples accordingly or drop them before summarising.

source
BVDOutbreakSize.onset_incidence_model Method
julia
onset_incidence_model(
    infections::AbstractVector;
    incubation,
    incubation_nmax
) -> Any

Onset-incidence submodel: convolve the renewal infections with the sampled incubation-period PMF to get daily symptom-onset incidence. Computed once per draw and reused by every downstream observation stream, so the staging infections → onsets → each observed event is explicit. The incubation delay submodel is injected. The incubation period cannot be fitted from the BDBV line list (no exposure dates), so the line-list reanalysis recommends the MacNeil et al. (2010) Bundibugyo estimate from the 2007 Uganda outbreak: mean 6.3 d (95% CI 5.2-7.3, n = 24). The mean prior Normal(6.3, 0.54) reproduces MacNeil's reported 95% CI (SD = CI half-width / 1.96); MacNeil give no interval on the spread, so the SD prior is a weakly-informative modelling choice centred on the CV-implied spread (≈ 3.5 d). Returns (; onsets, incubation_pmf, incubation_mean, incubation_sd).

source
BVDOutbreakSize.onset_to_death_model Method
julia
onset_to_death_model(
    nmax::Integer;
    oa_alpha_prior,
    oa_theta_prior,
    ad_alpha_prior,
    ad_theta_prior
)

Onset-to-death delay as the CONVOLUTION of two natural-parameter Gamma atomic delays — onset→admission (oa) and admission→death (ad) — each sampled through gamma_delay_model and combined by convolving their PMFs. This matches the companion line-list reanalysis, which fits the atomic components and convolves them rather than fitting onset→death directly, so no moment-matching is needed: each atomic delay keeps its own Gamma shape and scale prior with the reanalysis's reported uncertainty. The convolved PMF is truncated back to lags 0 … nmax and renormalised. Returns (; pmf, mean, sd, oa_mean, ad_mean).

source
BVDOutbreakSize.onset_to_sample_logweight Method
julia
onset_to_sample_logweight(
    report_mean::Real,
    report_sd::Real,
    receipt_mean::Real,
    receipt_sd::Real,
    cfg
) -> Any

Log-density grounding the confirmed onset-to-sample convolution on the cohort: soft Normal fits of the convolution's continuous mean (the sum of the report and receipt leg means) and continuous median (from gamma_median_wh of the summed leg variances) to the reported mean_obs/median_obs with SDs mean_se/median_se. The fixed Gaussian normalising constants are dropped.

source
BVDOutbreakSize.onsets_over_time Method
julia
onsets_over_time(chn; n, seeding)

Per-date posterior summary of the latent symptom-onset trajectory, the "symptomatic cases" curve plotted to show the outbreak over time. One row per grid day from seeding (grid day 1) to the cut-off (grid day n), giving the equal-tailed 30%, 60% and 90% credible intervals (the same intervals as summary_table and streams_table) of both the daily new symptom onsets and the cumulative symptom onsets to that date. The chain must carry the vector deterministic cumulative_onsets (one trajectory per draw), as the joint fit does.

Columns: date, then for each of new_onsets and cumulative_onsets the six endpoints _lower_90, _lower_60, _lower_30, _upper_30, _upper_60, _upper_90.

source
BVDOutbreakSize.plot_cfr_prior Method
julia
plot_cfr_prior(
    prior::Distributions.Distribution
) -> Makie.Figure

Density of a prior over the case-fatality ratio (CFR) on [0, 1], plotted on the sub-range [0, 0.7]. The CDC central estimate of 55/169 ≈ 0.33 is drawn as a solid vertical rule, and the report's 26% and 40% scenario bounds as dashed rules, so the prior can be read against the published CFR scenarios.

source
BVDOutbreakSize.plot_confirmed_cfr Method
julia
plot_confirmed_cfr(res) -> Makie.Figure

Posterior densities of the delay-corrected confirmed case-fatality ratio and the structural (infection-based) CFR from a delay_corrected_confirmed_cfr result res, with the naive observed confirmed ratio drawn as a solid vertical rule and the median uncorrected modelled confirmed ratio as a dashed rule. The gap between the naive rule and the corrected density shows the real-time delay debiasing; the gap to the structural density shows the residual case/death ascertainment difference. Plotted on the CFR percentage scale.

source
BVDOutbreakSize.plot_correlation_heatmap Method
julia
plot_correlation_heatmap(
    chn,
    params::AbstractVector{Symbol};
    labels
) -> Makie.Figure

Posterior correlation heatmap over the named scalar quantities params (tracked deterministics or sampled parameters). Each cell is the Pearson correlation of two quantities' posterior draws, drawn on a symmetric red–blue scale with the value printed in the cell. Unlike the block-grouped plot_pair corners — which keep the infection, delay and surveillance parameters in separate plots — this surfaces the whole joint identifiability structure in one panel, including the CROSS-block degeneracies those corners split apart: the size–ascertainment seesaw (C_T vs p_drc), the weaker size–fatality tilt (C_T vs CFR), and the pooled p_drcp_uganda link. labels maps a parameter symbol to a display string, interpreted as LaTeX math (without the $ delimiters), so "p_\mathrm{drc}" renders with a subscript and "\lambda_\mathrm{bg}" as λ_bg; a parameter absent from labels falls back to its symbol name. Returns the Figure.

source
BVDOutbreakSize.plot_cumulative_cases Method
julia
plot_cumulative_cases(
    streams::Pair{String, <:AbstractVector}...;
    scenarios,
    xmax,
    xlabel,
    title
) -> AlgebraOfGraphics.FigureGrid

Overlaid posterior densities of C_T from one or more fits, built through AlgebraOfGraphics. The 15 published scenario point estimates are drawn as faint dashed Makie vlines on top of the AoG figure.

source
BVDOutbreakSize.plot_cumulative_trajectories Method
julia
plot_cumulative_trajectories(chn; n, seeding)

Headline 3x2 cumulative figure. Rows are cumulative infections, cumulative symptom onsets and cumulative deaths, all modelled BVD-only latent renewal quantities (the deaths row is the BVD death series, excluding the non-BVD background, so it stays as smooth as the infection and onset rows). The left column is the modelled expected cumulative trajectory over the grid as 50% and 90% ribbons; the right column is the posterior density of the current cut-off cumulative. The chain must carry the vector deterministics cumulative_infections, cumulative_onsets and cumulative_expected_deaths (one per draw). seeding is the calendar date of grid day 1, so day d is seeding + (d - 1). No observed data is overlaid: each row is a latent quantity that sits upstream of ascertainment, confirmation and reporting delays, so the observed counts are not on the same scale.

source
BVDOutbreakSize.plot_density_overlay Method
julia
plot_density_overlay(
    streams::Pair{String, <:AbstractVector}...;
    xlabel,
    title
) -> AlgebraOfGraphics.FigureGrid

Overlaid posterior densities of an arbitrary scalar quantity from one or more fits, built through AlgebraOfGraphics. Pass each fit as "label" => draws; xlabel and title set the axis text.

source
BVDOutbreakSize.plot_estimate_comparison Method
julia
plot_estimate_comparison(
    rows::AbstractVector;
    xlabel,
    xmax,
    groups,
    group_colours
) -> Makie.Figure

Horizontal point-and-interval comparison of cumulative-case estimates from several sources. rows is a vector of (label, central, lower, upper) tuples, drawn top to bottom with the central estimate as a point and [lower, upper] as a bar. A row whose lower and upper match its central is a deterministic point estimate and is drawn as a bare marker with no bar.

groups is an optional vector of group keys, one per row, matched against group_colours (a vector of key => colour pairs) to colour each row's marker and bar and build a legend, so several sources read apart at a glance. Without groups the rows share a single colour.

source
BVDOutbreakSize.plot_evolution_by_group Method
julia
plot_evolution_by_group(
    groups::AbstractVector;
    trajectories,
    xlabel,
    ylabel,
    title,
    released_label,
    trajectory_label,
    refline,
    ncols,
    shared_yrange,
    empty_note
) -> Makie.Figure

Faceted estimate evolution, one panel per group, for comparing how the same quantity moved across releases under each of several fits.

groups is a vector of label => released pairs, where released is the (cutoff_date, median, lo30, hi30, lo60, hi60, lo90, hi90) tuple vector plot_estimate_evolution takes. Panels share one calendar mapping, and are laid out over ncols columns. Groups with no estimates are dropped rather than drawn as an empty panel, so a dataset with no fit of its own does not imply a comparison that cannot exist. refline draws a faint horizontal rule in every panel, e.g. Rt = 1.

trajectories optionally maps a group's label to that group's own current-data, current-model trajectory, a (dates, lo30, hi30, lo60, hi60, lo90, hi90) tuple in the shape plot_estimate_evolution takes, drawn with the same band styling. A group absent from trajectories still draws its released points with no band, so a dataset with no current-data fit of its own is not left blank.

shared_yrange (default true) draws every panel on one y range, sized to the largest of them, so groups whose quantity sits on a comparable scale read directly against each other. Set it to false when the groups span very different scales (e.g. outbreak size, where one dataset's count can be orders of magnitude larger than another's), so a wide-scale group does not squash every other panel's band to an invisible sliver; each panel then uses its own range, floored at 1.0.

Returns a figure carrying empty_note in place of the panels when no group has any estimate.

source
BVDOutbreakSize.plot_forecast Method
julia
plot_forecast(fc::DataFrames.DataFrame) -> Makie.Figure

One-week-ahead forecast of the observed count streams from forecast_reported: the new count each stream adds over the horizon. Panels cover reported cases, suspected deaths, laboratory-confirmed cases, confirmed deaths and recovered, each drawn only when the forecast carries that stream's *_new column, so a fit that observes fewer streams shows fewer panels. Each panel histograms the projected new count with its 90% predictive interval shaded. The latent counterparts are shown by plot_forecast_latent.

source
BVDOutbreakSize.plot_forecast_beds Method
julia
plot_forecast_beds(fc::DataFrames.DataFrame) -> Makie.Figure

One-week-ahead isolation/treatment-bed forecast from forecast_reported: the projected bed DEMAND (the need a week ahead, under unconstrained supply) against the supply-limited occupancy (the beds actually filled), and the shortfall between them. The left panel overlays the two predictive distributions, so the gap between the need and the supply-limited occupancy is the unmet demand; the right panel histograms the shortfall directly. Drawn only when the forecast carries the bed streams (bed_demand and isolation_level).

Because the model carries a SINGLE national bed capacity it cannot represent LOCAL saturation — on 13 June Ituri was at 93.9% occupancy while Sud-Kivu was at 21.9% — so the national shortfall understates the local unmet need (beds free in one province cannot serve patients who need them in another).

source
BVDOutbreakSize.plot_forecast_beds_vs_truth Method
julia
plot_forecast_beds_vs_truth(
    fc::DataFrames.DataFrame;
    isolation,
    individual
)

Validate a forecast_reported bed projection against the beds actually occupied a week later. Histograms the projected supply-limited isolation-bed occupancy with the 90% predictive interval shaded and the isolation count observed at the target date drawn as a dashed black rule, so last week's bed forecast is scored against what the beds held. Drawn only when the forecast carries isolation_level.

individual, when given, is a second predictive sample — the frozen individual (treatment-only) model's own forecast draws at the same cut-off, from forecast_stream — overlaid as a dotted density so the joint and the individual model's bed forecasts are both visible against the observed occupancy, rather than the joint alone.

At a one-week-back freeze the bed capacity has no implied-capacity anchor (the reported occupancy rate starts only on 9 June), so the projected occupancy rides the capacity random walk back to the freeze date and the interval is wide — the bed forecast is the weakest of the validated streams.

source
BVDOutbreakSize.plot_forecast_flows Method
julia
plot_forecast_flows(
    fc::DataFrames.DataFrame
) -> Makie.Figure

One-week-ahead forecast of the daily isolation/treatment flows from forecast_reported: the projected new admissions, in-care deaths and rule-outs a day at the horizon. Each panel histograms the projected daily count with its 90% predictive interval shaded, drawn only when the forecast carries the flow streams (admissions_fc, incare_deaths_fc, ruleouts_fc). These are the daily-flow counterparts of the bed-stock forecast in plot_forecast_beds.

source
BVDOutbreakSize.plot_forecast_latent Method
julia
plot_forecast_latent(
    fc::DataFrames.DataFrame
) -> Makie.Figure

One-week-ahead forecast of the unobserved (latent) quantities from forecast_reported: new infections, new symptom onsets and new deaths over the horizon, with the reproduction number left to keep evolving over the horizon. Each count panel histograms the projected new latent count with its 90% predictive interval shaded; the reproduction-number panel shows the posterior of the end-of-horizon forecast R_t with the no-growth line at one marked. These are the latent counterparts of the observed-stream forecast in plot_forecast.

source
BVDOutbreakSize.plot_forecast_overlay Method
julia
plot_forecast_overlay(
    overlay::DataFrames.DataFrame
) -> Makie.Figure

Forecasts-versus-now overlay: a grid of panels, one row per observed stream and one column per forecast horizon, each showing the forecasts made at every release (median with the 90% predictive interval as a vertical bar) against the value observed since (black points). overlay is the data/forecast_overlay.csv table with columns stream, made_date, horizon, fit, observed, median, lo90 and hi90.

The x-axis is the date the forecast was made, not the target date. For an incident stream the observed value is the new count over the forecast's own (made_date, target_date] window, so it depends on the made date: keying on the target date would stack several different observed windows at one x with no way to pair each forecast to its own truth. Within one horizon column each made date has a single target and a single observed value, so the pairing is unambiguous.

Forecasts are coloured by fit role (baseline, individual, joint) and dodged by a small fraction of the made-date spacing so the four series read apart at each made date without the jitter itself dominating the panel. A stream that carries only some roles, e.g. recovered with no individual fit, is drawn with only those, and the legend covers every role drawn in any panel.

Each panel is cropped to zero and about three times the larger of its own observed values and forecast medians, so one very wide predictive interval elsewhere in the table cannot squash every other panel to a line near the bottom. An interval or median that runs past that crop is clamped and marked with an open triangle at the top of the axis, so it reads as running off the top rather than vanishing. Every made date in the table gets its own x tick where there are few enough to stay legible, thinning to about a dozen ticks for a busier release history.

Returns a figure carrying a short note in place of the panels when no forecasts have been scored yet.

source
BVDOutbreakSize.plot_forecast_relative_skill Method
julia
plot_forecast_relative_skill(
    scores::DataFrames.DataFrame;
    value_col,
    ylabel,
    title,
    ncols,
    empty_message
) -> Makie.Figure

By-horizon relative-skill figure: one panel per stream, plotting relative skill against the persistence baseline (rel_to_baseline, or log_rel_to_baseline on the log scale) against the forecast horizon, with one series per fit role (individual, joint). scores is a forecast_score_by_horizon-shaped table, baseline rows already excluded, carrying stream, horizon, fit and the column named by value_col.

The skill axis is log-scaled, so a fit twice as good and a fit twice as bad sit the same distance from the reference line at one, drawn as a dashed horizontal rule; a series below the line beats the baseline on average at that horizon. A (stream, horizon, fit) cell whose skill is missing or non-finite (the aggregation guard against a zero-mean baseline) is simply absent from its series rather than drawn as a break, and a stream with no individual fit, e.g. recovered, is drawn with the joint series alone.

Returns a figure carrying empty_message in place of the panels when scores has no rows, since an empty table can mean different things (no release has a stored forecast at all, versus every stored forecast's target has yet to resolve) and a reader should be told which, not left with one generic caption for both.

source
BVDOutbreakSize.plot_forecast_vs_truth Method
julia
plot_forecast_vs_truth(
    fc::DataFrames.DataFrame;
    observed,
    baseline,
    individual
)

Validation figure for a forecast_reported projection, laid out as a two-row grid with one column per scored stream: the top row shows the cumulative forecast distribution, the bottom row the new count forecast over the horizon. Each panel is a histogram with the 90% predictive interval shaded and the later-observed count drawn as a dashed black rule, so the forecast distribution is scored against the count that was actually observed.

Streams are drawn in the order reported cases, suspected deaths, laboratory-confirmed cases, confirmed deaths and recovered, each shown only when the forecast carries that stream's *_cum/*_new columns and an observed cumulative count is supplied for it. observed is a NamedTuple mapping a stream's cumulative column (:confirmed_cum, :cases_cum, …) to its observed cumulative count at the target date; a stream absent from observed is skipped. baseline maps the same columns to the cumulative count at the forecast origin (default 0), so the observed new count is max(observed − baseline, 0). individual is an optional NamedTuple mapping a stream's NEW-count column (:confirmed_new, :cases_new, …) to that stream's own frozen individual (single-stream) model's forecast draws of the new count, from forecast_stream; a stream present in individual gets a second, dotted density overlaid on both its panels (the cumulative panel from baseline + individual new-count draws), so the joint and the individual fit's forecasts are both visible rather than the joint alone. A stream absent from individual (e.g. recovered, which has no individual fit) draws the joint alone, unchanged. The latent counterparts are scored distribution-versus-distribution by plot_forecast_vs_truth_latent.

source
BVDOutbreakSize.plot_forecast_vs_truth_latent Method
julia
plot_forecast_vs_truth_latent(fc::DataFrames.DataFrame; now)

Latent-quantity validation figure: for each unobserved quantity (new infections, new symptom onsets, new deaths over the past week) overlay the distribution the frozen (last-week) fit forecast against the distribution the current fit now estimates for the same window. Both are latent, so the comparison is density versus density rather than density versus a single observed count. fc is the frozen forecast from forecast_reported (its *_new latent columns); now is a NamedTuple carrying the current fit's draws of the same new-count quantities, (; infections_new, onsets_new, deaths_latent_new).

source
BVDOutbreakSize.plot_no_onward_deaths Method
julia
plot_no_onward_deaths(df::DataFrames.DataFrame; obs_deaths)

Two-panel density of the no-onward-transmission counterfactual from predict_no_onward_deaths. The left panel shows the still expected deaths (:delta_deaths, the future deaths in cases already infected by T, net of the obs_deaths already observed). The right panel shows the projected total (:total_projected = obs_deaths + delta_deaths) with a dashed black rule at obs_deaths. Both are lower bounds: they assume every onward transmission stops at time T.

source
BVDOutbreakSize.plot_pair Method
julia
plot_pair(
    chn,
    params::AbstractVector{Symbol};
    thin,
    prior,
    labels
) -> Makie.Figure

PairPlots.jl corner plot over the named posterior parameters, thinned by thin. Pass prior (another chain holding the same parameters) to overlay the prior as a second series with a legend, so the data's contribution to each marginal is visible.

labels is an optional map from the raw chain symbol to a clean display name (e.g. Symbol("rt_state.sigma_rw") => "Rt step size"), applied to the axis labels only; the model's variable names are unchanged. Symbols absent from the map keep their raw name.

source
BVDOutbreakSize.plot_posterior_predictive Method
julia
plot_posterior_predictive(
    pp_exports::Union{Nothing, AbstractVector},
    pp_deaths::Union{Nothing, AbstractVector},
    obs_exports::Union{Nothing, Real},
    obs_deaths::Union{Nothing, Real};
    pp_cases,
    obs_cases,
    pp_exports_deaths,
    obs_exports_deaths,
    pp_confirmed_deaths,
    obs_confirmed_deaths,
    pp_confirmed,
    obs_confirmed,
    pp_tests,
    obs_tests,
    predictive_label
) -> Makie.Figure

Posterior predictive histogram with one panel per supplied data stream. Pass pp_exports/pp_deaths as nothing to suppress either of the first two panels, and supply pp_cases and/or pp_exports_deaths to add the reported-cases and deaths-among-exports panels. Observed values are drawn as red vlines. With four streams the panels are laid out as a 2×2 grid (exports cases, exports deaths, DRC deaths, DRC reported cases); fewer streams are placed in a single row.

source
BVDOutbreakSize.plot_posterior_predictive_grid Method
julia
plot_posterior_predictive_grid(
;
    individual,
    joint,
    observed
)

Two-row comparison of posterior-predictive distributions, one column per stream. Top row: replicates from the per-stream fits. Bottom row: replicates from the joint fit, conditioning on all observed streams. Observed values shown as red vertical lines.

Each NamedTuple carries a subset of (; exports, exports_deaths, deaths, cases, tests, confirmed); columns are drawn in that canonical order for whichever streams are present in individual (the confirmed/tests columns appear only when the laboratory pipeline is included). Each panel is a histogram of replicated counts; rows share the same x-axis (the stream's count) so the per-stream and joint predictives are directly comparable.

source
BVDOutbreakSize.plot_prior_predictive Method
julia
plot_prior_predictive(
    pp_exports::Union{Nothing, AbstractVector},
    pp_deaths::Union{Nothing, AbstractVector},
    obs_exports::Union{Nothing, Real},
    obs_deaths::Union{Nothing, Real};
    pp_cases,
    obs_cases,
    pp_confirmed,
    obs_confirmed,
    pp_tests,
    obs_tests
) -> Makie.Figure

Prior predictive variant of plot_posterior_predictive, with the panel labels switched to "Prior".

source
BVDOutbreakSize.plot_projection_comparison Method
julia
plot_projection_comparison(
;
    external,
    ours,
    observed,
    external_label,
    ours_label,
    observed_label,
    external_colour,
    ours_colour,
    ylabel,
    title
)

Calendar time-series comparison of cumulative-count projections against the data observed since. external is another group's published projection and ours is our own forward projection, each drawn as a central line with a shaded [lower, upper] band so the two fans read directly against each other; observed is the data observed so far, drawn as a marked line. Each is a vector of (date, ...) tuples with date an ISO string: external and ours are (date, central, lower, upper), observed is (date, value). The dates share one calendar x-axis, so the two projections read directly against what the outbreak actually did. Used to set our forward projection beside the Chamla et al. (Chamla et al., 2026) confirmed-case projection.

source
BVDOutbreakSize.plot_rt Method
julia
plot_rt(
    chn;
    n,
    breakpoint,
    as_of_date,
    seeding,
    rt_start,
    rt_walk_start,
    week,
    ramp,
    n_traj
)

Reconstruct the daily reproduction-number trajectory Rt per posterior draw from the sampled weekly random-walk parameters and plot it over the established-outbreak window. The saved chain stores only the cut-off R_T, so each draw's daily Rt is rebuilt by mirroring rt_walk_model: weekly knots (knot_days) follow a non-centred Gaussian walk (rt_state.log_R0 plus the cumulative sum of rt_state.sigma_rw .* rt_state.z), linearly interpolated to the day grid (interpolate_knots) and shifted by the sampled rt_state.intervention_effect along a logistic ramp (sigmoid_ramp) centred at the outbreak-response breakpoint.

The estimated window runs from rt_start (the renewal start, where the random walk begins) to the cut-off; only that period is drawn, the median with 50% and 90% ribbons, and about n_traj thinned sampled trajectories are overlaid thin and faint to show the per-draw spread. The intervention breakpoint, the end of the intervention scale-up (breakpoint + ramp) as a dotted rule, and the cut-off are marked. seeding is the calendar date of grid day 1 (so day d is seeding + (d - 1)).

source
BVDOutbreakSize.plot_rt_streams Method
julia
plot_rt_streams(
    streams::AbstractVector;
    joint,
    n,
    breakpoint,
    as_of_date,
    seeding,
    display_start,
    week,
    ramp,
    ncols,
    joint_colour
)

Faceted implied reproduction number, one panel per single-stream fit, each with the joint fit overlaid as the reference. Each fit's daily Rt is reconstructed from its own sampled random walk exactly as in plot_rt (see reconstruct_rt), so the figure shows what reproduction number each data stream implies on its own against the all-streams-together joint estimate.

Every panel draws 30/60/90% credible ribbons with no median line, matching the band style used across the report: the joint in grey first, then the stream in its colour on top (the panel title is in that colour). One panel per stream rather than a single overlay, so the wide per-stream ribbons stay legible. The y-axis is shared across panels and capped from the panels' 90% bands so a weakly-informed stream (the confirmed-only fit) does not stretch the scale; ribbon above the cap is clipped.

Each stream is a NamedTuple (; label, chn, rt_start, rt_walk_start, colour) and joint is (; label, chn, rt_start, rt_walk_start), where chn is that fit's chain and rt_start/rt_walk_start are the renewal start and random-walk start that fit used (the per-stream fits walk from day 1, the joint from the breakpoint lead). display_start is the shared grid day the panels draw from (the joint renewal start), so every stream reads over the same window. seeding is the calendar date of grid day 1, so day d is seeding + (d - 1). The intervention breakpoint, the end of the scale-up (breakpoint + ramp, dotted) and the cut-off are marked as in plot_rt.

source
BVDOutbreakSize.plot_scenario_comparison Method
julia
plot_scenario_comparison(
    scenarios::AbstractVector;
    ours,
    date_titles,
    method_names,
    method_colours,
    xlabel,
    title
) -> Makie.Figure

Faceted point-and-interval comparison of published scenario estimates, one panel per date of estimation so the figure spreads sideways instead of into one tall column. scenarios is REPORT_SCENARIOS_CI-shaped (date, label, central, lower, upper) with label of the form "M1|M2 <family>, <swept level>" (for example "M2 τ=14 d, CFR 26%"). Within a panel each (method, family) is one row, and the swept nuisance level (the CFR, window or doubling time) is dodged onto that single line so every scenario keeps its own interval while the sweep no longer adds rows. Method sets the colour. ours maps a date string to our matched (median, lower, upper) estimate, drawn as a grey reference band with a dashed median in that date's panel. date_titles are date => title pairs giving each panel its heading.

source
BVDOutbreakSize.plot_start_date_pair Method
julia
plot_start_date_pair(chn; as_of_date, thin)

One-row, two-panel figure summarising when the outbreak began. The left panel is the posterior density of the outbreak start date, the calendar date of the import that started the outbreak, obtained by rescaling the outbreak age T (origin to cut-off) to a calendar date (as_of_date minus T). The right panel is the joint (doubling_time, T) posterior pair plot: shorter doubling times correspond to faster early growth, which reaches the same epidemic size in less time (smaller T).

source
BVDOutbreakSize.plot_stream_calibration Method
julia
plot_stream_calibration(
    tbl::DataFrames.DataFrame
) -> Makie.Figure

Per-stream calibration of the one-step-ahead conditional posterior predictive, plotting the table from stream_calibration. Pass the table that function returns (its prettified columns Stream, 50% coverage, 90% coverage, Bias). The figure has two panels sharing a categorical y-axis of streams: the left panel marks each stream's empirical 50% and 90% coverage with vertical dashed reference lines at the nominal 0.5 and 0.9, so a well-calibrated stream sits on its line and a marker to the left of its line flags under-coverage; the right panel marks the mean forecast Bias (negative = the stream is under-predicted, positive = over-predicted) with a dashed line at zero. Streams are read off the shared row labels, so all ~14 stay legible without splitting the figure into columns.

source
BVDOutbreakSize.plot_stream_pairs Method
julia
plot_stream_pairs(
    modelled::NamedTuple,
    observed::NamedTuple
) -> Makie.Figure

Pairs plot of the per-stream modelled totals: a PairPlots corner of the per-draw modelled stream totals (modelled, a NamedTuple of one per-draw vector per stream, each summed to that stream's own observed support) with the observed totals (observed, a NamedTuple of scalars) drawn as crosshair reference lines. The diagonals show how much predictive density sits above or below each observed value (the over/under-shoot), and the off-diagonals whether those shoots move together across draws — the posterior-predictive view of data-stream conflict, the counterpart to the parameter-space plot_correlation_heatmap. Returns the Figure.

source
BVDOutbreakSize.plot_stream_trajectories Method
julia
plot_stream_trajectories(
    streams::AbstractVector;
    n,
    seeding,
    title
)

Overlaid cumulative-infection trajectories, one per single-stream fit, each projected out to the cut-off on day n even when that stream's data stops earlier. Each stream is drawn as 50% and 90% credible ribbons only, no median line, matching the ribbon style of plot_rt and plot_cumulative_trajectories. A dotted vertical rule on each stream's colour marks the date that stream's data stops reporting, so the projection beyond the data reads apart from the fitted span.

Each stream is a NamedTuple (; label, trajs, last_day, colour), where trajs is a vector of per-draw cumulative-infection vectors of length n (one per posterior draw) and last_day the 1-based grid day that stream's data last reports (or nothing to omit the rule). seeding is the calendar date of grid day 1, so day d is seeding + (d - 1).

source
BVDOutbreakSize.plot_vintage_conditional_ppc Method
julia
plot_vintage_conditional_ppc(
    panels::AbstractVector;
    xlabel,
    max_date
) -> Makie.Figure

Per-vintage conditional one-step-ahead posterior-predictive for the DRC streams. For each panel the predicted cumulative count at vintage v conditions on the observed cumulative at the previous vintage and adds only the posterior-predictive between-vintage increment,    with  . The increment is the per-bin replicate draw the model already samples in predictive mode, so each step carries the full posterior uncertainty of the new increment while grounding on what was actually observed at the preceding sitrep. This is the "filtered" one-step-ahead predictive: it isolates the model's per-step increment prediction and does not let errors compound across the series, unlike a running sum of the modelled increments. The result is summarised by vintage as shaded 30/60/90% predictive ribbons; the observed cumulative counts are overlaid as points. Each panel is a NamedTuple (; title, dates, replicates, observed), where replicates is a vector of per-draw increment vectors (one entry per vintage, oldest first) and observed the matching observed cumulative counts used as the conditioning baselines. colour is optional per panel. A panel may set cumulative = false to plot standalone per-day counts instead of a cumulative series (e.g. the post-26 May daily new-suspect inflow, or the 24h analysed volume): there is no previous-vintage baseline, so each replicate is plotted as its own daily count against the observed daily count, and the y-axis reads "Daily count".

max_date (an ISO date string or Date) truncates every panel to the vintages on or before that date, so streams that keep reporting past the others (the laboratory streams run to the cut-off while the suspected streams freeze earlier) are cut back to the shared last date. Without this the confirmed panel runs further along the date axis than the suspected panel and reads as though it overtakes it, when the two are simply shown to different end dates.

source
BVDOutbreakSize.plot_vintage_incidence_ppc Method
julia
plot_vintage_incidence_ppc(
    panels::AbstractVector;
    xlabel,
    max_date
) -> Makie.Figure

Per-vintage incidence posterior-predictive check: the same panels as plot_vintage_conditional_ppc but plotting the count between consecutive vintages rather than the running cumulative, so trends (a rise or a slowdown) read directly off the height of each bar-like step instead of the slope of a near-straight cumulative line. For a cumulative panel the observed incidence is the between-vintage increment (the first vintage is its own baseline); for a non-cumulative panel (a standalone per-day count such as the 24h analysed volume or the daily new-suspect inflow) it is the count itself. The replicates are already per-vintage increments, so they are the modelled incidence directly and are summarised as 30/60/90% credible ribbons with the observed incidence overlaid. panels and max_date match plot_vintage_conditional_ppc.

source
BVDOutbreakSize.pooled_ascertainment_model Method
julia
pooled_ascertainment_model(
;
    mu_prior,
    tau_prior
) -> DynamicPPL.Model{typeof(pooled_ascertainment_model), (), (:mu_prior, :tau_prior), (), Tuple{}, Tuple{Distributions.Normal{Float64}, Distributions.Truncated{Distributions.Normal{Float64}, Distributions.Continuous, Float64, Float64, Nothing}}, DynamicPPL.DefaultContext, false}

Partially pooled ascertainment fractions for the DRC and Uganda surveillance systems, sampled in non-centred form to avoid the funnel geometry. Both logit-scale fractions share a hyperprior with mean μ and pooling strength τ. Used by reported_cases_model, exports_model and exports_deaths_model; this is the composer default. The shared mean defaults to a reporting fraction of 0.75 (logit scale), reflecting the active case-finding of a declared Ebola response; a lower ascertainment inflates the inferred infections (and so the outbreak size C_T) for the same observed counts.

source
BVDOutbreakSize.pooled_dispersion_model Method
julia
pooled_dispersion_model(
    n_streams::Integer;
    mean_prior,
    sd_prior,
    centred
) -> Any

Partially-pooled negative-binomial dispersions for the n_streams passive-surveillance count streams in the joint model (suspected cases, suspected deaths, confirmed cases and confirmed deaths). Each stream draws its own dispersion from a shared population, so heterogeneous streams (a handful of deaths versus hundreds of suspects versus a daily laboratory volume) no longer share one global k that the dominant stream pulls around, while the sparse streams still borrow strength through the common hyper-parameters rather than going noisy on a fully independent draw.

The pooling is on the log(1/sqrt(k)) scale: a population mean μ_log, a pooling SD τ, and per-stream deviations, with k_s = 1 / inv_sqrt_k_s^2. The default is the centred form, log(1/sqrt(k))_s ~ Normal(μ_log, τ) drawn directly: the passive-surveillance streams are data-rich (hundreds of suspected/confirmed counts), so each stream's dispersion is strongly informed, and the non-centred form inv_sqrt_k_s = exp(μ_log + τ z_s) then funnels (z_s = (log_isk_s − μ_log)/τ diverges as τ → 0), stretching NUTS trajectories. On the joint, centring removes that funnel — worst dispersion bulk-ESS ≈ 102 → 156, divergences 5 → 2 and ~10% faster wall-clock at a 150×2 fit — for the same posterior over k. Pass centred = false for the non-centred form, which is the better choice when the streams are data-poor and prior-dominated. The population mean is centred on the shared 1/sqrt(k) prior of surveillance_dispersion_model (exp(μ_log) near 0.6), and the half-normal τ keeps the per-stream dispersions close unless the data pull them apart (τ = 0 collapses every stream to the population value, the shared-k model). Returns (; k, inv_sqrt_k, k_pop, μ_log, τ) with k a length-n_streams vector.

source
BVDOutbreakSize.posterior_summary Method
julia
posterior_summary(
    xs
) -> NamedTuple{(:lo90, :lo60, :lo30, :hi30, :hi60, :hi90), <:NTuple{6, Any}}

Return (lo90, lo60, lo30, hi30, hi60, hi90) equal-tailed credible interval endpoints from a vector of draws.

source
BVDOutbreakSize.predict_committed Method
julia
predict_committed(
    chn;
    obs_confirmed,
    obs_confirmed_deaths,
    obs_analysed,
    horizon_days,
    alg
) -> DataFrames.DataFrame

Per-draw committed totals under the counterfactual that every onward transmission stops at the cut-off T: infections stay flat at the current outbreak size C(T) and the already-infected pool continues to die and be confirmed over the horizon_days horizon (default one year). Reads the posterior chn and returns a DataFrame with one row per draw and columns:

  • :infections — committed infections, the current outbreak size C(T) = 2^m (:cumulative_infections); flat, since no new infections occur under the counterfactual.

  • :bvd_deaths — committed true BVD deaths, CFR · C(T). Every infection eventually contributes its death, so over a one-year horizon essentially all delayed onset-to-death events occur and the committed toll is the closed form CFR · C(T) (equivalently the realised deaths plus the committed-deaths tail of predict_no_onward_deaths).

  • :confirmed_cases — committed laboratory-confirmed cases when the chain carries the lab parameters (:s_test, :spec_test, :τ_forward, :α_recv, :θ_recv) and obs_confirmed / obs_analysed are supplied. Computed by draining the committed suspect-case backlog (flat infections, continuing background) over the horizon: obs_confirmed + p_pos · max(received(T + horizon) − obs_analysed, 0). The capacity limit is not applied because over a year the lab clears the finite committed backlog.

  • :confirmed_deaths — committed laboratory-confirmed deaths when the chain additionally carries :τ_death, :p_deaths, :λ_bg_death and obs_confirmed_deaths is supplied: the observed confirmed deaths plus the forwarded-positive suspect-death backlog drained over the horizon.

horizon_days sets how far the lab drains; the default 365 is long enough that the committed lab outcomes have effectively saturated. alg is the quadrature scheme, defaulting to GaussLegendre(n = 64).

source
BVDOutbreakSize.predict_no_onward_deaths Method
julia
predict_no_onward_deaths(chn; obs_deaths)

Per-draw projection of cumulative deaths under the counterfactual that every onward transmission stops at the cut-off. Reads :CFR, :C_T and :expected_deaths_T from the posterior chn and forms the committed future deaths

with C_T the cumulative infections and E[D_T] the deaths already expected by the cut-off, returning a DataFrame with one row per draw:

  • :delta_deaths additional future expected deaths beyond obs_deaths

  • :total_projected obs_deaths + delta_deaths

obs_deaths is the number of deaths already observed at the cut-off (e.g. obs.total_deaths from the bundled observations).

source
BVDOutbreakSize.progress_callback Method
julia
progress_callback(; path, every)

A lightweight, dependency-free streaming progress callback for nuts_sample. Returns a closure matching the AbstractMCMC callback signature

julia
callback(rng, model, sampler, transition, state, iteration; kwargs...)

which, every every iterations on each chain, appends one line to the file at path recording the iteration number, the log joint density, and a running count of divergent transitions.

The same closure is invoked from every thread MCMCThreads() spawns, so the divergence tally and file writes are shared across chains and guarded by a ReentrantLock. Tail the file live during a fit with tail -f <path>.

Step-level statistics are read through the sampler-agnostic AbstractMCMC.ParamsWithStats(model, sampler, transition, state; stats = true) interface rather than by reaching into transition fields, so the callback tracks Turing's transition format instead of a fixed field layout. The log density is taken from the logjoint statistic and the divergence flag from numerical_error. The whole body is wrapped in try/catch, so a transition that does not expose these statistics yields missing log-density / no divergence increment rather than crashing the fit. The running divergence count is a single total over all chains, reset implicitly each time a fresh callback is constructed.

See also: tensorboard_callback, nuts_sample.

source
BVDOutbreakSize.r_to_R0 Method
julia
r_to_R0(r, g::AbstractVector) -> Any

Reproduction number R implied by an exponential growth rate r and a generation-interval PMF g (indexed from lag 1), the FORWARD Euler–Lotka relation R = 1 / Σ_s g_s e^{−r s}. The inverse of euler_lotka_r: where that solves R · Σ_s g_s e^{−r s} = 1 for r given R, this returns R directly for a given r, so the prior can be placed on the growth rate and the established reproduction number derived from it under OUR generation interval. Uses only arithmetic and exp, so it is AD-transparent under Mooncake.

source
BVDOutbreakSize.reconstruct_rt Method
julia
reconstruct_rt(
    chn;
    n,
    breakpoint,
    rt_start,
    rt_walk_start,
    week,
    ramp
)

Reconstruct each posterior draw's daily reproduction-number trajectory Rt from the sampled weekly random-walk parameters, returning a ndraws × n matrix masked to each draw's established window (missing before rt_start). The saved chain stores only the cut-off R_T, so each draw's daily Rt is rebuilt by mirroring rt_walk_model: weekly knots (knot_days) from rt_walk_start follow a non-centred Gaussian walk (rt_state.log_R0 plus the cumulative sum of rt_state.sigma_rw .* rt_state.z), linearly interpolated to the day grid (interpolate_knots) and shifted by the sampled rt_state.intervention_effect along a logistic ramp (sigmoid_ramp) centred at the outbreak-response breakpoint. Shared by plot_rt and plot_rt_streams.

source
BVDOutbreakSize.recovered_model Method
julia
recovered_model(
    recovered_history,
    recovered_total::Union{Missing, Integer},
    confirmed_daily::AbstractVector,
    CFR::Real;
    recovery,
    dispersion,
    confirmation_to_recovery,
    k_external
) -> Any

DRC recovered-among-confirmed likelihood ("cumul guéris"), an incidence (scaled-convolution) stream — the renewal analogue of the convolution-and- scaling secondary-observation model of EpiNow2 (Abbott et al., 2020). Recoveries are the survivors among laboratory-confirmed cases: the modelled daily confirmed-case incidence confirmed_daily (from confirmed_cases_model) is scaled by the recovery probability p_recover (the confirmed-case survival fraction, recovery_probability_model) and convolved with a sampled confirmation-to-recovery delay,

The recovery fraction p_recover is grounded on the case-fatality ratio CFR (a recovered case is one that did not die), adjusted for the confirmed population by a sampled log-odds offset (see recovery_probability_model). A case is taken to be confirmed BEFORE it is recorded as recovered: the report's "cumul guéris" counts recoveries among confirmed cases, so the recovery follows confirmation by the confirmation-to-recovery delay. In principle a positive result could return after a patient has already recovered and been discharged, but we assume the reported total reflects confirmed cases carefully recorded as recovered, so the confirmation-then-recovery ordering holds.

The cumulative recovered series ends at the cut-off, so its between-vintage increments are fitted as observed ~ data with a NegativeBinomial whose dispersion is sampled here, NOT shared with the other streams (the recovered signal has its own observation noise). The convolution right-censors recoveries that have not yet resolved by the cut-off, so a small observed recovered count is consistent with a high eventual survival fraction and a long recovery delay. Empty by default; a missing cut-off total leaves the increments missing (the predictive-generator path). Returns the recovery probability, the recovery-delay mean, the dispersion, the daily recovered series and the cut-off total.

source
BVDOutbreakSize.recovery_probability_model Method
julia
recovery_probability_model(CFR::Real; offset_prior) -> Any

Recovery probability for the recovered-among-confirmed stream (recovered_model). The fraction of confirmed cases whose outcome is recovery rather than death is the confirmed-case survival fraction, the complement of the case-fatality ratio, so it is GROUNDED on the model's CFR rather than estimated from scratch. The confirmed cases are a slightly different population from the one the CFR is defined over (they have been laboratory-confirmed and brought into care), so the survival fraction is the complement of the CFR adjusted on the log-odds scale by a sampled offset,

with δ_rec ~ Normal(0, 0.5) centred at zero, so the default recovery fraction is exactly 1 − CFR and the data move it only as far as they support. The offset keeps p_recover in (0, 1) without a hard clamp and lets the confirmed-population survival differ modestly from the CFR complement. p_recover is partially confounded with the confirmation-to-recovery delay for the count of recoveries observed by the cut-off (a long delay right-censors recoveries that have not yet resolved), so the delay carries the timing and p_recover the eventual survival fraction. Pass offset_prior to override. Returns (; p_recover, recovery_offset).

source
BVDOutbreakSize.renewal_infections Method
julia
renewal_infections(
    Rt::AbstractVector,
    g::AbstractVector,
    seed::AbstractVector
) -> Any

Daily latent infections from the renewal equation I_t = R_t Σ_{s ≥ 1} I_{t−s} g_s, with generation-interval PMF g (indexed from lag 1), per-day reproduction numbers Rt (length n) and a pre-computed seed of length L < n filling the first L days (see seed_infections). The recursion runs for days L+1 … n, so Rt[1] (used to imply the seeding growth) and the seed are mutually consistent. Returns the length-n infection trajectory; the output element type is promoted from Rt, g and seed.

source
BVDOutbreakSize.reported_cases_model Method
julia
reported_cases_model(
    reported_history,
    reported_cases::Union{Missing, Integer},
    onsets::AbstractVector,
    k::Real,
    p_drc::Real;
    suspected_daily_history,
    positivity,
    background_re,
    onset_to_report
) -> Any

DRC suspected (reported) cases likelihood, per-vintage time series. The expected daily suspected cases are a BVD-driven onset-to-report convolution scaled by the DRC ascertainment fraction p_drc, plus an additive non-BVD background rate λ_bg per day (so a suspected case need not be a true BVD infection). Reads the modelled cumulative suspected cases at each vintage day off the daily series and fits the increments with a NegativeBinomial sharing k.

An optional suspected_daily_history adds the post-26 May daily new-suspect inflow ("nouveaux cas suspects du jour"): per-day counts of newly reported suspects scored against the modelled daily suspected series reports_daily at each report day (a single-day mean, not a between-vintage increment) with NegativeBinomials sharing k. This continues the suspected signal where the cumulative reported_history stops, once INSP began reclassifying suspects downward and the cumulative total fell. The inflow is a genuine per-day incidence that never falls, so it fits directly; it shares the suspect pipeline and dispersion with the cumulative stream and is empty by default. Its days fall strictly after the cumulative series ends, so the two suspected likelihoods cover disjoint days.

The background and testing fraction are sampled by an injected test_positivity_model, and the onset-to-report delay is injected, defaulting to a weakly-informative prior on the onset-to-notification delay (mean 4.5 d, SD 3.6 d), consistent with Ebola surveillance reporting delays.

Returns the onset-to-report PMF and the BVD onset-to-report daily series (unit ascertainment, no background) so confirmed_cases_model can reuse the same report kernel, the sampled background rate and testing fraction, and the implied per-suspected positivity (the BVD share of the expected suspected total) as a derived quantity for comparison with the sitrep.

source
BVDOutbreakSize.rt_walk_model Method
julia
rt_walk_model(
    n::Integer,
    log_R0_base::Real;
    week,
    breakpoint,
    rt_start,
    ramp,
    sigma_prior,
    effect_prior
) -> Any

Weekly piecewise-linear log-scale reproduction number over n days, with a smooth intervention ramp. Knots sit at weekly spacing (knot_days) and follow a Gaussian random walk in non-centred cumulative-sum form: standard-normal innovations are scaled by sigma_rw and accumulated, avoiding the funnel geometry of the centred recursion and matching the non-centred ascertainment block. Daily log-R_t is the linear interpolation between knots (interpolate_knots). An intervention at breakpoint (e.g. the first WHO situation report) adds a sampled effect intervention_effect shaped by a logistic ramp (sigmoid_ramp), so transmission changes gradually over the ramp window rather than instantly; breakpoint = missing drops the term. The ramp scale ramp defaults to 21 days, an about-three-week transition reflecting that a response (case finding, isolation, vaccination) takes weeks to bite rather than switching at a single date; pass ramp to widen or narrow it. Rt = exp.(log_Rt). Returns (; Rt, log_R, days, sigma_rw, log_R0, intervention_effect).

The walk base log_R0 is NOT sampled here. It is passed in as a DERIVED quantity: the first reproduction number is derived FORWARD from the sampled growth rate r and the generation interval through Euler–Lotka (R0 = r_to_R0(r, g) in infection_model). The prior therefore sits on the growth rate (see exponential_growth_model), grounded on Cuomo-Dannenburg & Ghafari's molecular-clock doubling time (centre 20 d, range 15.2–24.5 d), and the established reproduction number is whatever that growth implies under OUR generation interval rather than a separately asserted R0 prior. The genetic report gives R0 ≈ 1.31–1.55 under THEIR generation interval; deriving R0 forward from the shared growth rate under our generation interval is the consistent thing to do. This single growth source pins the ESTABLISHED reproduction number (the walk base at the genetic bound) AND, through the renewal seeding, the cryptic exponential phase, so the outbreak has ONE growth source. The grid days before the renewal start are filled by the analytic cryptic exponential and so are unused by the walk; the walk simply clamps to R0 before its first knot.

The random-walk step SD prior is a half-normal SD 0.1, so the weekly log-R_t is unlikely to change by more than about 20% (two SD ≈ 0.2) from one week to the next. The walk starts at the first situation report (rt_start is the breakpoint at the composer), so every knot sits in the observed window and the step flexibility is spent where the data support it, letting R_t bend to a slowdown or acceleration over the sitreps rather than drifting over the unobserved pre-report stretch.

The intervention effect is constrained to be non-positive (truncated(Normal(0, 0.4); upper = 0)): a declared WHO response (case finding, isolation, vaccination) can only reduce transmission or leave it unchanged, never increase it, so the ramp's effect on log-R_t is bounded at or below zero. This is stronger than the earlier symmetric Normal(0, 0.5) (under which the effect was unidentified and R_t drifted up unchecked) and the one-sided lean Normal(-0.3, 0.4): the response now has a definite non-increasing effect, with the half-normal admitting anything from no effect (mode) to a substantial decline. Because the breakpoint is only ≈11 days before the cut-off and the ramp is three weeks, the response damps R_t only partially by the cut-off.

source
BVDOutbreakSize.safe_betabinomial Method
julia
safe_betabinomial(
    n::Integer,
    p,
    ρ
) -> Distributions.BetaBinomial

NaN / Inf-safe overdispersed-Binomial (BetaBinomial) constructor parameterised by the trial count n, the mean positive probability p and an intra-window overdispersion ρ ∈ (0, 1). The BetaBinomial(n, α, β) has mean n·p and variance n·p·(1 − p)·(1 + (n − 1)·ρ) with α = s·p, β = s·(1 − p) and concentration s = (1 − ρ)/ρ, so ρ → 0 (s → ∞) recovers the plain Binomial(n, p) and larger ρ inflates the variance above the Binomial. This adds the extra-Binomial variation the confirmed positives carry (day-to-day laboratory batching and within-window positivity heterogeneity the pooled per-window p does not capture), mirroring how the count streams use the overdispersed NegativeBinomial rather than a Poisson. ρ is floored away from 0 (capping s) and p clamped into (0, 1) so the distribution stays defined under extreme NUTS proposals during warmup. Shared by the confirmed-positives windows.

source
BVDOutbreakSize.safe_nbinomial Method
julia
safe_nbinomial(k, μ) -> Distributions.NegativeBinomial

NaN / Inf-safe NegativeBinomial constructor parameterised by mean μ and dispersion k, with clamping on the success probability so extreme NUTS proposals during warmup do not trip the distribution domain check. Shared by the count-stream observation submodels.

source
BVDOutbreakSize.safe_rate Method
julia
safe_rate(x) -> Any

NaN / Inf-safe positive rate. The renewal recursion can transiently overflow on extreme NUTS warmup proposals (large R_t compounding), giving a non-finite expected count; a plain max(x, eps) would propagate the NaN (max(NaN, eps) = NaN) and trip the Poisson / NegativeBinomial domain check.

source
BVDOutbreakSize.score_draws Method
julia
score_draws(
    obs::Real,
    samples::AbstractVector{<:Real}
) -> NamedTuple{(:crps, :log_crps, :dispersion, :overprediction, :underprediction, :coverage_50, :coverage_90, :bias, :n), <:NTuple{9, Any}}

Score a predictive samples ensemble against a single observation obs, returning (; crps, log_crps, dispersion, overprediction, underprediction, coverage_50, coverage_90, bias, n):

  • crps: crps_sample, the ensemble CRPS.

  • log_crps: log_crps_sample, CRPS scored on the log scale.

  • dispersion, overprediction, underprediction: crps_decomposition, summing to crps.

  • coverage_50, coverage_90: whether obs falls inside the central 50% / 90% predictive interval (see _covered).

  • bias: bias_sample, signed forecast bias in [-1, 1].

  • n: number of predictive draws.

With no draws (isempty(samples)), the scores are NaN, the coverage flags are false, and n = 0.

source
BVDOutbreakSize.seed_at_renewal_start Method
julia
seed_at_renewal_start(C_T_prior) -> Any

Renewal-start seed magnitude for the two-phase renewal: the cumulative infection count reached by the analytic cryptic phase AT the renewal-start day. The renewal is two-phase: an analytic exponential cryptic phase from the origin to the renewal start (≈ the genetic TMRCA day, off the renewal grid), then the renewal recursion on [renewal_start, cut-off]. The doubling count m counts the doublings DURING the cryptic phase (origin → renewal start), so the cryptic phase grows a single import to

at the renewal start, INDEPENDENT of the growth rate r. The rate r shapes the cryptic exponential history feeding the recursion just before the renewal start (see seed_infections), but the magnitude at the renewal start is fixed by m alone. This deliberately keeps r (hence the single R0) OUT of the seed magnitude: an earlier formulation back-scaled a cut-off-referenced size 2^m e^{-rτ_obs}, which put r into both the seed and the renewal growth so the two cancelled for a fixed realised size — a flat ridge along which R0 slid to the edge. With the magnitude r-free the renewal grows 2^m forward over the observation window under the time-varying R_t, so the realised cut-off size is data-driven through R_t while the prior fixes only the renewal-start scale. The argument is C_T_prior = 2^m; returned unchanged, kept as a named helper for the seeding call site.

source
BVDOutbreakSize.seed_infections Method
julia
seed_infections(I0, r, len::Integer) -> Any

Seed the first len days of the infection trajectory as exponential growth I_t = I0 · e^{r (t − len)} at the implied growth rate r (see euler_lotka_r), so the seeding window is pinned at I0 on its last day and tails off backwards. This is the model-based initialisation used by EpiNow2 and EpiAware.jl in place of placing the whole seed on a single day, which would otherwise inject a transient the renewal recursion has to relax away from. Returns a length-len vector whose element type follows I0 and r.

source
BVDOutbreakSize.seed_model Method
julia
seed_model(
;
    i0_prior
) -> DynamicPPL.Model{typeof(seed_model), (), (:i0_prior,), (), Tuple{}, Tuple{Distributions.Truncated{Distributions.Normal{Float64}, Distributions.Continuous, Float64, Float64, Nothing}}, DynamicPPL.DefaultContext, false}

Seed submodel: the latent infection count I0 on the last day of the seeding window, representing the zoonotic introduction. Default prior is a truncated Normal centred on a single seed; the prior is injectable. The seeding window is filled by exponential growth at the implied rate in infection_model.

source
BVDOutbreakSize.seeding_age Method
julia
seeding_age(cumulative::AbstractVector, n::Integer) -> Any

Outbreak age in days: the elapsed time from the model-implied seeding day to the cut-off (day n), where the seeding day is the smooth crossing at which cumulative infections first reach one. The crossing is linearly interpolated between the two days that bracket a cumulative of one, so it is a continuous function of the trajectory; before the trajectory reaches one it returns n (the full grid). The renewal analogue of the integral model's sampled outbreak age T, used for the seeding-date plots and the genetic-TMRCA bound.

source
BVDOutbreakSize.select_daily_releases Method
julia
select_daily_releases(entries) -> Any

Select at most one results release per data day from entries, a collection of (tag, created, cutoff) triples: the release tag, its creation timestamp (a UTC DateTime, as reported by gh release list) and the data cut-off it was built from (as_of_date in the release's observations.toml). Both tagged releases (results-vX.Y.Z) and main-build releases (results-<run number>, published by every push to main) are eligible; any other tag is ignored.

Days are cut-off days, not creation days, because a release's forecast is a function of the data it saw. Two releases sharing a cut-off carry the same forecast however far apart they were published: the report re-renders whenever main moves, so the same data is republished under a new tag. Grouping on the creation timestamp would keep both and score that forecast twice.

Within a day a tagged release is preferred, which also settles the case of a version tag and the main push of one commit publishing at the same instant. Failing that the newest build of the day is kept. Releases sharing a timestamp are separated by version or run number, keeping the selection deterministic.

Returns the selected tags, newest cut-off first.

source
BVDOutbreakSize.severity_enrichment_model Method
julia
severity_enrichment_model(
;
    logodds_prior,
    decay_prior
) -> DynamicPPL.Model{typeof(severity_enrichment_model), (), (:logodds_prior, :decay_prior), (), Tuple{}, Tuple{Distributions.Truncated{Distributions.Normal{Float64}, Distributions.Continuous, Float64, Float64, Nothing}, Distributions.Truncated{Distributions.Normal{Float64}, Distributions.Continuous, Float64, Float64, Nothing}}, DynamicPPL.DefaultContext, false}

Severity-enrichment prior for the COMPOSITION-LINKED confirmed positivity (positivity_link = :composition in confirmed_cases_model). In that mode the per-window tested BVD share is not a free random effect; it is the suspect-pool composition φ_v = (p_drc · BVD)_v / ((p_drc · BVD)_v + λ_bg_v) over each laboratory window, UPSAMPLED by a severity enrichment that decays as testing widens:

with c_v the cumulative analysed volume at window v (the testing clock). The lab over-tests BVD early (severe cases are triaged first and are more likely BVD), the enrichment δ₀·e^{−c/decay} relaxing toward zero as testing widens, at which point the tested share equals the pool composition. This ties positivity to the background λ_bg, so the confirmed/positivity data identify the non-BVD background rather than it being absorbed by a free per-window random effect, correcting the model's treatment of suspected cases as a large overestimate of true BVD.

δ₀ is the early severity log-odds enrichment of BVD; lower-truncated at 0 because severity triage upsamples BVD, never down. The default truncated(Normal(1.5, 0.75); lower = 0) is deliberately moderate / bounded: even severity-triaged testing cannot be near-pure BVD (other haemorrhagic / severe febrile illness is also triaged), so for a pool composition φ ≈ 0.4 the early tested share is logistic(logit(0.4) + 1.5) ≈ 0.75. decay_scale is the relaxation timescale on the analysed- volume clock. Pass logodds_prior / decay_prior to override. Used by confirmed_cases_model in composition mode. Returns (; δ0, decay_scale).

source
BVDOutbreakSize.sigmoid_ramp Method
julia
sigmoid_ramp(
    n::Integer,
    day::Union{Missing, Real};
    ramp
) -> Any

Smooth intervention ramp over an n-day grid: the logistic curve 1 / (1 + e^{−(t − day) / ramp}) for each day t, rising from ≈0 well before day to ≈1 well after, with ramp setting the transition width in days. Multiplied by a sampled effect size and added to log-R_t, this gives an intervention (e.g. the first WHO situation report) a gradual ramped effect on transmission rather than an instantaneous step. Returns a length-n Float64 vector; day = missing gives an all-zero ramp (no intervention). Type-stable and AD-transparent in the effect size it multiplies.

source
BVDOutbreakSize.stream_calibration Method
julia
stream_calibration(
    panels::AbstractVector
) -> DataFrames.DataFrame

Per-stream posterior-predictive calibration for the per-vintage one-step-ahead conditional checks. Pass the same panels given to plot_vintage_conditional_ppc: each is a NamedTuple (; title, observed, replicates, …) with optional cumulative. For each stream the conditional predictive at every vintage is scored against the observed count, and the per-vintage scores are averaged into one row.

Columns: stream, the number of scored vintages n, the mean forecast bias (see bias_sample; negative = the stream is under-predicted, positive = over-predicted), and the empirical coverage_50/coverage_90 — the fraction of vintages whose observed count falls inside the central 50% and 90% predictive intervals. A well-calibrated stream has bias near zero and coverage near its nominal level; departures flag the streams the joint fit reproduces less well.

source
BVDOutbreakSize.streams_table Method
julia
streams_table(
    streams::Pair{String, <:AbstractVector}...;
    digits
) -> DataFrames.DataFrame

Side-by-side credible intervals for C_T from several fits. Pass each fit as "label" => draws_vector.

source
BVDOutbreakSize.summary_table Method
julia
summary_table(
    chn,
    params::AbstractVector{Symbol};
    digits,
    labels
) -> DataFrames.DataFrame

DataFrame with one row per posterior parameter and the columns Quantity, Lower 90%, Lower 60%, Lower 30%, Upper 30%, Upper 60%, Upper 90% giving the lower and upper endpoints of the equal-tailed 30%, 60% and 90% credible intervals.

labels is an optional map from the raw chain symbol to a clean display name (e.g. Symbol("rt_state.sigma_rw") => "Rt step size"), applied to the Quantity column only; the model's variable names are unchanged. Symbols absent from the map keep their raw name.

source
BVDOutbreakSize.surveillance_dispersion_model Method
julia
surveillance_dispersion_model(
;
    inv_sqrt_k_prior
) -> DynamicPPL.Model{typeof(surveillance_dispersion_model), (), (:inv_sqrt_k_prior,), (), Tuple{}, Tuple{Distributions.Truncated{Distributions.Normal{Float64}, Distributions.Continuous, Float64, Float64, Nothing}}, DynamicPPL.DefaultContext, false}

Shared negative-binomial dispersion k for the passive-surveillance streams (suspected deaths, reported cases and confirmed cases). Sampled on the 1/sqrt(k) scale with a weakly-informative half-normal prior following the Stan prior-choice recommendations. Returns (; k, inv_sqrt_k).

source
BVDOutbreakSize.tensorboard_callback Method
julia
tensorboard_callback(logdir; kwargs...)

TensorBoard streaming callback for nuts_sample, mirroring the optional Enzyme backend. tensorboard_callback(logdir; every = 20, histograms = true) opens a TensorBoardLogger.TBLogger(logdir) and, on every kept post-warmup draw, logs through the sampler-agnostic AbstractMCMC.ParamsWithStats interface to two grouped tag prefixes so the dashboard stays navigable:

  • params/<name> — every sampled parameter

  • diagnostics/<name> — log-density (logjoint), divergence flag (numerical_error), step size, tree depth, acceptance rate, ...

Each scalar streams every step as a .../value time series. With histograms = true (the default) a running histogram of the draws so far is also logged every every steps as .../distribution, populating the TensorBoard HISTOGRAMS and DISTRIBUTIONS dashboards. Set histograms = false for scalar traces only, or widen every to log histograms less often.

tensorboard_callback is a stub; loading TensorBoardLogger (using TensorBoardLogger) activates the method via BVDOutbreakSizeTensorBoardLoggerExt. Calling it without TensorBoardLogger loaded raises an informative ErrorException.

Pass the result to nuts_sample:

julia
using TensorBoardLogger
nuts_sample(model; callback = tensorboard_callback("logs/run"))

then view the run with tensorboard --logdir logs/run. Use chains = 1 for clean live traces; parallel chains share one logger and interleave.

See also: progress_callback, nuts_sample.

source
BVDOutbreakSize.test_positivity_model Method
julia
test_positivity_model(
;
    lambda_prior,
    fraction_tested_prior
) -> DynamicPPL.Model{typeof(test_positivity_model), (), (:lambda_prior, :fraction_tested_prior), (), Tuple{}, Tuple{Distributions.Truncated{Distributions.Normal{Float64}, Distributions.Continuous, Float64, Float64, Nothing}, Distributions.Beta{Float64}}, DynamicPPL.DefaultContext, false}

Test-positivity machinery shared by the suspected- and confirmed-case streams. Samples

  • λ_bg — the per-day non-BVD background suspected-case rate, on a half-normal scale. Drives the suspected/confirmed contrast: suspected cases mix the BVD onset-to-report signal with this additive background, while the laboratory pipeline only confirms the BVD share.

  • τ_test — the fraction of suspected cases that are sampled and routed to the laboratory pipeline.

The default λ_bg prior is a half-normal truncated(Normal(0, 1.0); lower = 0). Its total contribution to the expected suspected-case count over the grid is λ_bg · T, with T the seeding-to-cut-off span. The prior is deliberately informative because λ_bg is degenerate with outbreak size (the per-vintage reported mean mixes the p_drc-scaled BVD increment with λ_bg · Δt), so a diffuse prior lets the background absorb arbitrarily many suspected cases and resolve at the high end where the deaths and exports streams pin C_T. A background-noise process must not be able to explain more suspected cases than were ever reported. With SD 1.0 the median background is ≈ 0.67/day and the 95% prior bound ≈ 2.0/day, a modest minority of the ≈ 1077 suspected cases observed by the last stable suspected-case vintage while still admitting a genuine non-BVD signal; a wider SD (e.g. SD 5) left a second posterior mode in which the background explains the majority of suspected cases (positivity ≈ 0.2, background ≈ 2.3× the observed total). Pass lambda_prior to override. τ_test defaults to Beta(5, 2) (mean ≈ 0.71).

The derived per-suspected positivity is exposed inside reported_cases_model; the per-test positivity inside confirmed_cases_model. Returns (; λ_bg, τ_test).

source
BVDOutbreakSize.test_sensitivity_model Method
julia
test_sensitivity_model(
;
    sensitivity_prior
) -> DynamicPPL.Model{typeof(test_sensitivity_model), (), (:sensitivity_prior,), (), Tuple{}, Tuple{Distributions.Beta{Float64}}, DynamicPPL.DefaultContext, false}

Confirmation-process sensitivity prior. Beta(38, 2) centres near a mean of 0.95 with a tight spread. Confirmation runs on the altona RealStar Filovirus Screen RT-PCR, which detects Bundibugyo virus at 11–67 RNA copies per reaction; the Zaire-specific GeneXpert Ebola assay does not reliably detect Bundibugyo. A single assay draw is sensitive to about 0.85, but a suspect is confirmed or ruled out through repeat control tests rather than one PCR, so the effective process sensitivity is higher (two controls give about 0.98) and Beta(38, 2) credits that process (issue #374). Under the severe-first backlog the first vintage's analysed batch is near-pure BVD (q ≈ 1), so the v1 positivity ≈ s identifies the sensitivity from the early data. Returns (; s_test).

source
BVDOutbreakSize.test_specificity_model Method
julia
test_specificity_model(
;
    specificity_prior
) -> DynamicPPL.Model{typeof(test_specificity_model), (), (:specificity_prior,), (), Tuple{}, Tuple{Distributions.Beta{Float64}}, DynamicPPL.DefaultContext, false}

PCR specificity prior for the Ebola assay. Beta(60, 2) has mean ≈ 0.97 and 95% interval ≈ 0.91–0.998, a high-but-imperfect specificity reflecting that a small fraction of non-BVD specimens test positive (cross-reaction, contamination, low-level false positives). Used by the composition-linked confirmed-case positivity so the tested-positive probability is p = s · q + (1 − spec)(1 − q) with q the tested BVD share: the false-positive term (1 − spec)(1 − q) makes the confirmed counts respond to the non-BVD share 1 − q, so the laboratory data identify the background λ_bg rather than only the BVD signal. Returns (; spec).

source
BVDOutbreakSize.traveller_volume_model Method
julia
traveller_volume_model(
;
    mean,
    sd
) -> DynamicPPL.Model{typeof(traveller_volume_model), (), (:mean, :sd), (), Tuple{}, Tuple{Int64, Int64}, DynamicPPL.DefaultContext, false}

Prior on the mean daily traveller volume from the source area to Uganda. Default centred on ITURI_DAILY_TRAVEL with SD ITURI_DAILY_TRAVEL_SD, truncated at zero. Sets the per-capita travel rate for the exports stream.

source
BVDOutbreakSize.treatment_flow_model Method
julia
treatment_flow_model(
    isolation_history,
    bvd_reports_daily::AbstractVector,
    bg_daily::AbstractVector,
    p_drc::Real,
    CFR::Real;
    capacity_history,
    admissions_history,
    deaths_history,
    ruleout_history,
    absconded_history,
    confirmed_incare_history,
    suspect_incare_history,
    conf_hazard_daily,
    admission,
    severity,
    capacity,
    dispersion,
    k_external,
    cfr_modifier_prior,
    abscond_prior,
    incare_confirm_log_prior,
    admission_delay,
    death_los,
    recovery_los,
    ruleout_los,
    occupancy_break_days,
    occupancy_break_sd
) -> Any

DRC treatment-centre patient-flow likelihood. Occupancy is built as a running balance of latent admission, discharge and abscond events rather than a convolution. The "Patients en isolement" figure is the daily occupied-bed count, fitted as latent bed demand right-censored at the implied-capacity bound.

Admissions are the reported suspects (reported_cases_model) carried through a short suspected→admission delay and split into a BVD true-case inflow A_bvd = p_iso_bvd · p_drc · bvd_reports at the severity-skewed rate p_iso_bvd (isolation_severity_model) and a non-BVD inflow A_bg = p_iso · bg_daily at the base rate (isolation_admission_model).

The clinical layer sets the total occupancy and discharge flows and is label-independent, so a true case can die before confirmation. A BVD true case leaves by death (weight CFR_iso) on the admission→death stay or recovery (weight 1 − CFR_iso) on the admission→recovery stay; a non-BVD admission rules out on the rule-out stay; absconds drain the suspect pool at κ · O_susp(t−1). The total latent demand is the running balance

with A = A_bvd + A_bg. The in-care deaths flow is suspect and confirmed combined (the Tableau 6 décédés row), scored directly and not gated by confirmation. The in-care fatality CFR_iso = logistic(logit(CFR) + β_iso) adjusts the infection CFR to the admitted population by a sampled modifier β_iso; it is conditional on admission, not a causal treatment effect.

A label overlay carves the occupied stock into confirmed and suspect sub-stocks without removing anyone from the total. Confirmation relabels an occupied true case at the daily hazard ρ · τ_test · p_pos, the community hazard τ_test · p_pos borrowed from the lab pipeline (confirmed_cases_model) scaled by a sampled in-care modifier ρ = exp(γ_conf). In-care confirmation runs slower than the community rate (ρ < 1), the confirmed/suspect census identifying the modifier. The confirmed sub-stock is the confirmed subset of the occupied BVD stock and the suspect sub-stock the remainder O_susp(t) = D(t) − O_conf(t), so a case that dies before confirmation is a suspect death in the combined deaths flow.

Capacity enters only as a fixed, data-derived censoring bound; the latent demand is uncapped. The occupancy likelihood is a NegativeBinomial around the demand, right-censored at the recorded implied-capacity series (censoring_cap, censored_occupancy_model); admissions are censored at the recorded free-bed headroom C_fix(t) − occupancy(t-1). The capacity walk C(t) (bed_capacity_walk_model) carries the implied-capacity likelihood, and unmet demand D(t) − censored occupancy is a returned diagnostic.

The daily in-care outcome flows (deaths, rule-outs, admissions, absconds) are each an optional NegativeBinomial stream scored against the Tableau 6 patient-movement counts, a no-op when their history is empty. The two sub-stocks are scored against the Tableau 6 census breakdown (dont confirmés / dont suspects) where published, in place of the total on those days. An opt-in occupancy offset Δ(t) on the supplied occupancy_break_days (cumulative_occupancy_offset) absorbs a between-report measurement-basis discontinuity in the isolation series; empty (the default) is a no-op.

Exposes the cut-off occupancy, bed demand and shortfall, the utilisation, the BVD share of demand, CFR_iso and β_iso, the length-of-stay, the two sub-stock prevalences and their stay means, the in-care fraction and the daily series for forecasting and replication.

source
BVDOutbreakSize.treatment_only_model Method
julia
treatment_only_model(
    n::Integer;
    isolation_history,
    bed_capacity_history,
    treatment_admissions_history,
    treatment_deaths_history,
    treatment_ruleout_history,
    treatment_absconded_history,
    treatment_confirmed_incare_history,
    treatment_suspect_incare_history,
    confirmed_history,
    confirmed_cases,
    lab_history,
    lab_daily_history,
    tests_analysed,
    occupancy_break_days,
    confirmed_break_days,
    confirmed_break_gross_cases,
    confirmed_break_sd,
    breakpoint,
    infection,
    onset_incidence,
    cases,
    confirmed,
    treatment,
    cfr,
    dispersion,
    ascertainment
) -> Any

Isolation-occupancy-only composer (treatment-bed prevalence in isolation). Runs the infection process and onset staging, samples dispersion and pooled ascertainment, then runs the suspected-case stream in predictive mode (to draw the shared background rate, testing fraction and onset-to-report kernel) and conditions on the isolation/treatment-bed occupancy alone. See treatment_flow_model and reported_cases_model.

source
BVDOutbreakSize.two_clock_confirmed Method
julia
two_clock_confirmed(
    A_bvd::AbstractVector,
    conf_hazard::AbstractVector,
    S_clin::AbstractVector
) -> Any
julia
two_clock_confirmed(A_bvd, conf_hazard, S_clin)

Cohort-tracked confirmed-in-care sub-stock. A true-case admission cohort admitted on day u (A_bvd[u]) contributes to the confirmed stock on day t only once it has been confirmed and while it has not yet clinically departed:

a product of two clocks running from admission. The confirmation clock is the per-cohort cumulative confirmation probability under the time-varying daily hazard conf_hazard(t) = τ_test · p_pos(t) borrowed from the lab pipeline, with exposure from the day after admission:

so a just-admitted cohort (u = t) has CDF_conf = 0. The clinical clock is the BVD stay survival S_clin(d) = P(stay > d) (clinical_stay_survival), so Σ_{u ≤ t} A_bvd(u) · S_clin(t − u) reconstructs the abscond-free BVD stock. The confirmed-and-present cohort is a subset of the present cohort, so O_conf ≤ O_bvd holds by construction, without the proportional split's mean-field approximation: a true case that dies before its test returns is never counted as confirmed. Returns the length-n confirmed-in-care sub-stock; the caller forms the suspect sub-stock as the demand remainder D − O_conf.

source
BVDOutbreakSize.vintage_increments_model Method
julia
vintage_increments_model(
    modelled::AbstractVector,
    increments::Union{Missing, AbstractVector{<:Integer}},
    k::Real
) -> Any

Per-vintage increment likelihood for one stream, expressed as a proper vector likelihood. Given the modelled per-vintage increments modelled (see bin_increments), scores them against the observed increments with NegativeBinomials sharing the dispersion k (one per vintage).

The observed increments are passed as increments and scored with a loop of scalar ~ so the stream is real observed data that predict replicates. The increment variable is named increments, so under a prefixed submodel attachment the predict keys are <prefix>.increments[i]. When increments is missing the increments are sampled, making the submodel a predictive generator. When it is empty (zero vintages) the likelihood is a no-op. Returns the modelled and (when present) observed increments for reuse.

source
BVDOutbreakSize.vintage_obs Method
julia
vintage_obs(
    history,
    total::Union{Missing, Integer},
    n::Integer
) -> NamedTuple{(:days, :obs_increments), <:Tuple{Any, Any}}

Resolve a stream's per-vintage observation into the vintage day indices and the observed between-vintage increment vector to score, given the dated cumulative history (; days, counts), the cut-off total total and the grid length n (day n is the cut-off). When the history is non-empty it already ends at the cut-off (the last vintage count equals total), so the increments are differenced from the history alone and the separate cut-off total is not scored again. When the history is empty but a cut-off total is supplied (e.g. the tests-analysed stream, whose dated vintage history is absent), the cut-off becomes a single vintage point at day n so the total is still scored as one increment. A history with days but empty counts keeps the vintage day grid while leaving the increments missing, the posterior-predictive generator path that predict resamples. When both are empty or total is missing, the increments are missing over zero days. Returns (; days, obs_increments) with obs_increments either an Int vector or missing.

source