API
Public functions and constants exported by BVDOutbreakSize.
BVDOutbreakSize.ITURI_DAILY_TRAVEL Constant
ITURI_DAILY_TRAVELDefault prior mean for the daily outbound traveller volume from Ituri Province across seven points of entry.
sourceBVDOutbreakSize.ITURI_DAILY_TRAVEL_SD Constant
ITURI_DAILY_TRAVEL_SDDefault 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.
sourceBVDOutbreakSize.ITURI_POPULATION Constant
ITURI_POPULATIONSource population for the Ituri Province (McCabe et al., Table 1).
sourceBVDOutbreakSize.M_PRIOR_BASE Constant
M_PRIOR_BASECentre of the wide doubling-count prior in exponential_growth_model. 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. A centre of 3 places the prior cryptic phase at m·τ ≈ 60 d at the central 20-day doubling and a prior seed of 2^m = 8 infections at the renewal start. The genetic seeding bound pulls the lower tail of the outbreak age to sit at or before the most recent common ancestor.
BVDOutbreakSize.M_PRIOR_BASE_DATE Constant
M_PRIOR_BASE_DATEBase 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.
BVDOutbreakSize.M_PRIOR_DOUBLING_DAYS Constant
M_PRIOR_DOUBLING_DAYSCentral doubling time (days) for the size and growth priors, from Cuomo-Dannenburg & Ghafari's molecular-clock reanalysis (mean 15.2-24.5 d across six clock assumptions). The doubling-count prior centre advances by one doubling per M_PRIOR_DOUBLING_DAYS of elapsed time to the cut-off.
BVDOutbreakSize.RENEWAL_START_LEAD Constant
RENEWAL_START_LEADDays 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.
BVDOutbreakSize.REPORT_SCENARIOS Constant
REPORT_SCENARIOSPublished 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.
BVDOutbreakSize.REPORT_SCENARIOS_CI Constant
REPORT_SCENARIOS_CIPublished 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.
BVDOutbreakSize.RT_WALK_LEAD Constant
RT_WALK_LEADDays 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.
BVDOutbreakSize.background_cfr_model Method
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).
BVDOutbreakSize.background_pooling_model Method
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).
BVDOutbreakSize.background_re_model Method
background_re_model(
nv::Integer,
σ_bg::Real;
baseline_prior
) -> AnyPer-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.
BVDOutbreakSize.background_walk_model Method
background_walk_model(
n::Integer,
σ_rw::Real;
onset,
onset_ramp,
week,
baseline_prior
) -> AnyNon-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).
BVDOutbreakSize.bed_capacity_model Method
bed_capacity_model(
;
capacity_prior
) -> DynamicPPL.Model{typeof(bed_capacity_model), (), (:capacity_prior,), (), Tuple{}, Tuple{Distributions.Truncated{Distributions.Normal{Float64}, Distributions.Continuous, Float64, Float64, Nothing}}, DynamicPPL.DefaultContext, false}Isolation/treatment-bed capacity for the supply-limited occupancy stream (treatment_admission_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 truncated(Normal(450, 200); lower = 1) is weakly informative, 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).
BVDOutbreakSize.bed_capacity_walk_model Method
bed_capacity_walk_model(
n::Integer;
start,
week,
baseline_prior,
innovation_prior
) -> AnyTime-varying isolation/treatment-bed capacity over the daily grid, a multiplicative random walk: the supply-limited occupancy stream (treatment_admission_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 truncated(Normal(450, 200); lower = 1) prior as the scalar model, centred on the bed count implied by the reported occupancy rates (≈ 400–452 over 9–13 June), and 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.
BVDOutbreakSize.bin_increments Method
bin_increments(
daily::AbstractVector,
days::AbstractVector{<:Integer}
) -> AnyModelled 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.
BVDOutbreakSize.bvd_joint Function
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,
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,
tmrca_days,
tmrca_days_sd,
renewal_start_lead,
rt_walk_lead
) -> AnyJoint 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_admission_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).
BVDOutbreakSize.cases_only_model Method
cases_only_model(
n::Integer,
reported_cases::Union{Missing, Integer};
reported_history,
suspected_daily_history,
breakpoint,
infection,
onset_incidence,
cases,
dispersion,
ascertainment
) -> AnyCases-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.
BVDOutbreakSize.cdf_nmax Method
cdf_nmax(dist; q, cap, minlag) -> Anycdf_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.
BVDOutbreakSize.censored_delay_model Method
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).
BVDOutbreakSize.censored_occupancy_model Method
censored_occupancy_model(
means::AbstractVector,
ceilings::AbstractVector,
obs::Union{Missing, AbstractVector{<:Integer}},
k::Real
) -> AnyRight-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.
BVDOutbreakSize.censoring_cap Method
censoring_cap(iso_days, iso_obs, capacity_history) -> Anycensoring_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_admission_model.
BVDOutbreakSize.cfr_model Method
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.
BVDOutbreakSize.combined_callback Method
combined_callback(callbacks...) -> AnyCompose 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.
BVDOutbreakSize.comparison_table Method
comparison_table(
C_draws::AbstractVector;
scenarios
) -> DataFrames.DataFrameFor each published C_T scenario, the narrowest joint posterior credible interval (30, 60 or 90%) that contains it, or "outside 90%".
BVDOutbreakSize.confirmed_cases_model Method
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,
fit_unanchored
) -> AnyLaboratory 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 againstlab_history(specimens analysed) as observed~data with a NegativeBinomial sharing the surveillance dispersionk, identifyingτ_testand 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 a
Binomialof the observed specimens-analysed denominator in each laboratory window (confirmed_positivity_windows), with a partially-pooled per-window positivity (confirmed_positivity_model). Conditioning the positives on the observed analysed denominator (rather than a modelled count scaled byp_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.
BVDOutbreakSize.confirmed_cfr_table Method
confirmed_cfr_table(res; digits) -> DataFrames.DataFrameOne-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.
BVDOutbreakSize.confirmed_deaths_model Method
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,
receipt_pmf,
capacity_start,
case_analysed_daily,
case_suspected_daily,
scaling,
testing,
sensitivity,
specificity
) -> AnyLaboratory-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_dailyat the per-day suspected death-to-case ratio, times a testing-intensity scaling,v = scaling · case_analysed_daily · susp_death / susp_case, withsusp_deathandsusp_casethe 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 bycfr_bg(seedeaths_modelandbackground_cfr_model), keepsq_deathbelow one.Assay positivity.
p = s · q_death + (1 − spec)(1 − q_death)with PCR sensitivitys(test_sensitivity_model) and specificityspec(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.
BVDOutbreakSize.confirmed_deaths_only_model Function
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,
breakpoint,
infection,
onset_incidence,
deaths,
cases,
confirmed_deaths_stream,
dispersion,
ascertainment
) -> AnyConfirmed-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.
BVDOutbreakSize.confirmed_only_model Method
confirmed_only_model(
n::Integer,
confirmed_cases::Union{Missing, Integer};
confirmed_history,
lab_history,
lab_daily_history,
tests_analysed,
breakpoint,
infection,
onset_incidence,
cases,
confirmed,
dispersion,
ascertainment,
confirmed_positivity_link
) -> AnyConfirmed-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.
BVDOutbreakSize.confirmed_positives_model Method
confirmed_positives_model(
positives::Union{Missing, AbstractVector{<:Integer}},
analysed::AbstractVector{<:Integer},
p_pos::AbstractVector
) -> AnyPer-window confirmed-positives likelihood, expressed as a vector likelihood scored against the observed analysed denominators. Given the per-window analysed counts analysed and positivities p_pos, scores the observed positives with one Binomial(analysed[i], p_pos[i]) per window. 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.
BVDOutbreakSize.confirmed_positivity_model Method
confirmed_positivity_model(
nv::Integer;
baseline_prior,
pooling_prior
) -> AnyPer-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.
BVDOutbreakSize.confirmed_positivity_windows Function
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}}}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
Binomialdenominator 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 hasobs_analysed > 0and0 ≤ 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_startday (the last laboratory day) so the model bins the modelled volume over each late window's own day range, pinned atlate_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 inlate_analysedso the model can score it as a Binomial of that observed denominator instead, anchoring its positivity to data.
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.
BVDOutbreakSize.convolve_delay Method
convolve_delay(
x::AbstractVector,
delay::AbstractVector
) -> AnyConvolve 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.
BVDOutbreakSize.convolve_pmf Method
convolve_pmf(a::AbstractVector, b::AbstractVector) -> AnyDiscrete 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.
BVDOutbreakSize.convolve_survival Method
convolve_survival(
x::AbstractVector,
los::AbstractVector
) -> AnySurvival-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.
BVDOutbreakSize.dated_event_bins Method
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.
BVDOutbreakSize.dated_poisson_model Method
dated_poisson_model(
means::AbstractVector,
counts::Union{Missing, AbstractVector{<:Integer}}
) -> AnyPer-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.
BVDOutbreakSize.death_ascertainment_model Method
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).
BVDOutbreakSize.death_background_model Method
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).
BVDOutbreakSize.death_testing_fraction_model Method
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).
BVDOutbreakSize.death_testing_scaling_model Method
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).
BVDOutbreakSize.deaths_model Method
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
) -> AnyDRC 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.
BVDOutbreakSize.deaths_only_model Method
deaths_only_model(
n::Integer,
total_deaths::Union{Missing, Integer};
deaths_history,
suspected_daily_deaths_history,
breakpoint,
infection,
onset_incidence,
deaths,
dispersion
) -> AnyDeaths-only composer (back-calculation analogue). Runs the infection process and onset staging, samples dispersion, then conditions on the deaths likelihood only. See deaths_model.
BVDOutbreakSize.default_adtype Method
default_adtype() -> ADTypes.AutoMooncake{Mooncake.Config}Mooncake reverse-mode AD with default Mooncake.Config(). Used as the NUTS adtype keyword.
BVDOutbreakSize.delay_corrected_cfr Method
delay_corrected_cfr(
c_daily::AbstractVector,
Kc::AbstractVector,
Kd::AbstractVector,
deaths_total::Real
) -> AnyDelay-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.
BVDOutbreakSize.delay_corrected_confirmed_cfr Method
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.
BVDOutbreakSize.diagnostics_table Method
diagnostics_table(fits::Pair{String}...) -> AnyDataFrame of fit-quality diagnostics with one row per fit. Pass each fit as "label" => chain. Columns :fit, :max_rhat, :min_ess_bulk, :divergences.
BVDOutbreakSize.discretise_censored Method
discretise_censored(dist, nmax::Integer) -> AnyDaily 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.
BVDOutbreakSize.doubling_time Method
doubling_time(r) -> AnyDoubling 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.
BVDOutbreakSize.enzyme_adtype Function
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.
BVDOutbreakSize.euler_lotka_r Method
euler_lotka_r(R, g::AbstractVector; steps) -> AnyExponential 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.
BVDOutbreakSize.expand_vintage_rate Method
expand_vintage_rate(
rate::AbstractVector,
days::AbstractVector{<:Integer},
n::Integer
) -> AnyExpand 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.
BVDOutbreakSize.exponential_growth_model Method
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.15), centred on Cuomo-Dannenburg & Ghafari's molecular-clock doubling time for this outbreak (mean 15.2–24.5 d across six substitution-rate assumptions, centre 20 d), equivalent to a LogNormal(log(20), 0.15) prior on the doubling time. The log-SD 0.15 reads the 15.2–24.5 d spread as roughly a 95% interval (log-SD ≈ 0.12) and inflates it a little to allow for the wide per-assumption intervals, so the prior is slightly inflated in SD but unbiased relative to the source. 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(M_PRIOR_BASE, 3); lower = 0) is deliberately WIDE. Since m now counts only the CRYPTIC doublings (not the cut-off case total), its centre is much lower than the integral model's: with the ≈20-day doubling, M_PRIOR_BASE doublings span M_PRIOR_BASE · τ cryptic days, placing the origin in the genetically-plausible window (origin roughly Feb–Mar). The spread keeps the cryptic duration wide.
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). Returns (; τ, r, m, T, C_T).
BVDOutbreakSize.exports_deaths_model Method
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
) -> AnyDeaths-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)).
BVDOutbreakSize.exports_deaths_only_model Method
exports_deaths_only_model(
n::Integer,
exports_deaths::Union{Missing, Integer};
export_death_days,
breakpoint,
source_population,
infection,
onset_incidence,
deaths,
exports,
dispersion,
ascertainment
) -> AnyDeaths-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.
BVDOutbreakSize.exports_joint_only_model Method
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
) -> AnyExports-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.
BVDOutbreakSize.exports_model Method
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 dayd₁. 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_offsettruncation: the travel-gated export clock stops at the last observed importt_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 pastt_lastdoes not accrue. With an emptyexport_case_daysthe model falls back to the cut-off cumulative Poissonexported_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.
BVDOutbreakSize.exports_only_model Method
exports_only_model(
n::Integer,
exported_cases::Union{Missing, Integer};
export_case_days,
breakpoint,
source_population,
infection,
onset_incidence,
exports,
ascertainment
) -> AnyExports-only composer (geographic-spread analogue). Runs the infection process and onset staging, samples ascertainment, then conditions on the exports likelihood only. See exports_model.
BVDOutbreakSize.fit_callback Method
fit_callback(
name::AbstractString;
logdir,
spec
) -> Union{Nothing, Function}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-freeprogress_callback(a<name>.logfile underlogdir) and thetensorboard_callback(atensorboard/<name>run directory underlogdir)."progress"— the file progress stream only."tensorboard"(or"tb") — the TensorBoard stream only."none"— no logging; returnsnothing. 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.
BVDOutbreakSize.fit_diagnostics Method
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.
sourceBVDOutbreakSize.fit_parallel Method
fit_parallel(thunks::AbstractVector; chains) -> Anyfit_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.
BVDOutbreakSize.forecast_reported Method
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), scale the fitted expected counts by the resulting horizon growth factor, then replicate the cumulative counts. 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.:confirmed_cum,:confirmed_deaths_cum— laboratory-confirmed case and confirmed-death counterparts, present whenobs_confirmedandobs_confirmed_deathsare supplied.:cases_new, …:confirmed_deaths_new— new counts over the coming week (*_cumminus 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 carriesexpected_bed_demand_Tandbed_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), sobed_demand − isolation_levelis the projected bed shortfall. Replicated with the isolation stream's own dispersion.:recovered_cum,:recovered_new— cumulative recovered-among-confirmed by the horizon (and new this week whenobs_recoveredis supplied), present when the chain carriesexpected_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-offR_T).
Reads :r, :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 and :expected_confirmed_deaths_T from chn. 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.
BVDOutbreakSize.forecast_table Method
forecast_table(
fc::DataFrames.DataFrame;
digits
) -> DataFrames.DataFrameSummarise 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.
BVDOutbreakSize.forecast_vs_truth Method
forecast_vs_truth(
fc::DataFrames.DataFrame;
confirmed,
confirmed_deaths,
isolation,
digits
)Validate a forecast_reported projection against the counts that were later observed. confirmed and confirmed_deaths are the observed cumulative DRC laboratory-confirmed cases and confirmed deaths at the forecast target date. 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 — a last-week's-forecast versus what the beds actually held. Returns a DataFrame with one row per scored stream giving 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. The suspected reported streams are no longer reported, so they are not scored.
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.
sourceBVDOutbreakSize.forecast_vs_truth_trajectory Method
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.
BVDOutbreakSize.freeze_observations Method
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, :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}}, Int64, Any}}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.
BVDOutbreakSize.gamma_delay_model Method
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).
BVDOutbreakSize.gate_before Method
gate_before(v::AbstractVector, start::Integer) -> AnyZero 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.
BVDOutbreakSize.generation_interval_model Method
generation_interval_model(
nmax::Integer;
alpha_prior,
theta_prior
) -> AnyGeneration-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).
BVDOutbreakSize.genetic_seeding_model Method
genetic_seeding_model(
T::Real,
tmrca_days::Union{Missing, Real};
tmrca_days_sd
) -> AnyOne-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.
BVDOutbreakSize.independent_ascertainment_model Method
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.
BVDOutbreakSize.infection_model Method
infection_model(
n::Integer;
breakpoint,
rt_start,
rt_walk_start,
rt,
gi,
growth,
gi_nmax
) -> AnyGenerating 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).
BVDOutbreakSize.interpolate_knots Method
interpolate_knots(
knot_vals::AbstractVector,
days::AbstractVector{<:Integer},
n::Integer
) -> AnyLinearly 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.
BVDOutbreakSize.isolation_admission_model Method
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_admission_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).
BVDOutbreakSize.isolation_severity_model Method
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_admission_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).
BVDOutbreakSize.knot_days Method
knot_days(n::Integer; week, start) -> AnyDay 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.
BVDOutbreakSize.lab_delay_model Function
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) -> AnyReport-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).
BVDOutbreakSize.late_confirmed_model Method
late_confirmed_model(
increments::Union{Missing, AbstractVector},
modelled::AbstractVector,
analysed::AbstractVector{<:Integer},
p_pos::AbstractVector,
k::Real
) -> AnyLate 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 a Binomial(analysed[i], p_pos[i]) of the observed denominator (like an observed window), 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.
BVDOutbreakSize.load_observations Function
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, :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}}, 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, :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}}, 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"), 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.
BVDOutbreakSize.lognormal_meansd Method
lognormal_meansd(mean, sd) -> Distributions.LogNormalLogNormal 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.
BVDOutbreakSize.m_prior_centre Method
m_prior_centre(
as_of_date::Union{Dates.Date, AbstractString};
base_date,
m_base,
doubling_days
) -> Float64m_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 central 20-day doubling time (M_PRIOR_DOUBLING_DAYS, the molecular-clock estimate of cuomodannenburg2026), 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.
BVDOutbreakSize.markdown_table Method
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.
BVDOutbreakSize.nuts_sample Method
nuts_sample(
model;
samples,
chains,
target_accept,
seed,
progress,
adtype,
init,
check_model,
callback,
warmup,
kwargs...
) -> AnyNUTS 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 longer chains (1000 post-warmup draws each) rather than four shorter ones, mirroring the integral model (#211), which roughly halves the docs build at a similar total draw count.
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.
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 min(1000, samples ÷ 2) draws are adaptation steps rather than posterior samples; raise samples accordingly or drop them before summarising.
BVDOutbreakSize.onset_incidence_model Method
onset_incidence_model(
infections::AbstractVector;
incubation,
incubation_nmax
) -> AnyOnset-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).
BVDOutbreakSize.onset_to_death_model Method
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).
BVDOutbreakSize.onsets_over_time Method
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.
BVDOutbreakSize.plot_cfr_prior Method
plot_cfr_prior(
prior::Distributions.Distribution
) -> Makie.FigureDensity 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.
BVDOutbreakSize.plot_confirmed_cfr Method
plot_confirmed_cfr(res) -> Makie.FigurePosterior 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.
BVDOutbreakSize.plot_cumulative_cases Method
plot_cumulative_cases(
streams::Pair{String, <:AbstractVector}...;
scenarios,
xmax,
xlabel,
title
) -> AlgebraOfGraphics.FigureGridOverlaid 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.
BVDOutbreakSize.plot_cumulative_trajectories Method
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.
BVDOutbreakSize.plot_density_overlay Method
plot_density_overlay(
streams::Pair{String, <:AbstractVector}...;
xlabel,
title
) -> AlgebraOfGraphics.FigureGridOverlaid 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.
BVDOutbreakSize.plot_estimate_comparison Method
plot_estimate_comparison(
rows::AbstractVector;
xlabel,
xmax,
groups,
group_colours
) -> Makie.FigureHorizontal 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.
BVDOutbreakSize.plot_estimate_evolution Method
plot_estimate_evolution(
released::AbstractVector;
renewal,
trajectory,
xlabel,
ylabel,
title,
released_label,
renewal_label,
trajectory_label
) -> Makie.FigureEstimate-evolution plot: how the outbreak-size estimate moves as the data cut-off advances, drawn against the calendar date.
released is a vector of (cutoff_date, median, lo30, hi30, lo60, hi60, lo90, hi90) tuples, one per published project release, drawn in blue. renewal is the same tuple shape, the current renewal model re-fit frozen at each release date, drawn in red: the current method evaluated at a past cut-off. Each release and each frozen re-fit is its own independent fit, so both are drawn as discrete per-date estimates — a median marker with nested 30/60/90% vertical interval bars — rather than a connected ribbon. Marks sharing a date (an integral and a renewal release at one cut-off, or a release and its frozen re-fit) are dodged horizontally so each reads as a separate estimate.
trajectory is the current-data, current-model cumulative-infection trajectory over the day grid, a (dates, lo30, hi30, lo60, hi60, lo90, hi90) tuple where dates is the calendar date of each grid day and the remaining entries are the per-day credible bounds. It is a single fit shown over time, so it is drawn in a third colour as one continuous time-varying ribbon on the same calendar axis as the discrete marks.
Release dates are marked with dotted vertical rules.
xlabel/ylabel/title set the axis text; released_label, renewal_label and trajectory_label name the three series.
BVDOutbreakSize.plot_forecast Method
plot_forecast(fc::DataFrames.DataFrame) -> Makie.FigureOne-week-ahead forecast of the observed confirmed quantities from forecast_reported: new laboratory-confirmed cases and new confirmed deaths over the horizon. Each panel histograms the projected new count with its 90% predictive interval shaded. The panels are drawn only when the forecast carries the laboratory streams. The suspected reported streams are no longer reported, so they are not forecast here. The latent counterparts are shown by plot_forecast_latent.
BVDOutbreakSize.plot_forecast_beds Method
plot_forecast_beds(fc::DataFrames.DataFrame) -> Makie.FigureOne-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).
sourceBVDOutbreakSize.plot_forecast_beds_vs_truth Method
plot_forecast_beds_vs_truth(
fc::DataFrames.DataFrame;
isolation
)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.
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.
sourceBVDOutbreakSize.plot_forecast_latent Method
plot_forecast_latent(
fc::DataFrames.DataFrame
) -> Makie.FigureOne-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.
BVDOutbreakSize.plot_forecast_vs_truth Method
plot_forecast_vs_truth(
fc::DataFrames.DataFrame;
confirmed,
confirmed_deaths,
baseline_confirmed,
baseline_confirmed_deaths
)Confirmed-stream validation figure for a forecast_reported projection, laid out as a two-row grid. The top row shows the cumulative forecast distribution per confirmed stream (DRC confirmed cases and confirmed deaths); the bottom row shows the new counts 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. confirmed and confirmed_deaths are the observed cumulative counts; baseline_* are the counts at the forecast origin, so the observed new count is the cumulative truth minus the baseline. The suspected reported streams are no longer reported, so they are not scored here. The latent counterparts are scored distribution-versus-distribution by plot_forecast_vs_truth_latent.
BVDOutbreakSize.plot_forecast_vs_truth_latent Method
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).
BVDOutbreakSize.plot_no_onward_deaths Method
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.
BVDOutbreakSize.plot_pair Method
plot_pair(
chn,
params::AbstractVector{Symbol};
thin,
prior,
labels
) -> Makie.FigurePairPlots.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.
BVDOutbreakSize.plot_posterior_predictive Method
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.FigurePosterior 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.
BVDOutbreakSize.plot_posterior_predictive_grid Method
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.
BVDOutbreakSize.plot_prior_predictive Method
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.FigurePrior predictive variant of plot_posterior_predictive, with the panel labels switched to "Prior".
BVDOutbreakSize.plot_rt Method
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)).
BVDOutbreakSize.plot_rt_streams Method
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.
BVDOutbreakSize.plot_start_date_pair Method
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).
BVDOutbreakSize.plot_stream_trajectories Method
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).
BVDOutbreakSize.plot_vintage_conditional_ppc Method
plot_vintage_conditional_ppc(
panels::AbstractVector;
xlabel,
max_date
) -> Makie.FigurePer-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, 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.
BVDOutbreakSize.plot_vintage_incidence_ppc Method
plot_vintage_incidence_ppc(
panels::AbstractVector;
xlabel,
max_date
) -> Makie.FigurePer-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.
BVDOutbreakSize.pooled_ascertainment_model Method
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.
BVDOutbreakSize.pooled_dispersion_model Method
pooled_dispersion_model(
n_streams::Integer;
mean_prior,
sd_prior
) -> AnyPartially-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 in non-centred form: a population mean μ_log, a pooling SD τ, and per-stream standard-normal deviations z, giving inv_sqrt_k_s = exp(μ_log + τ z_s) and k_s = 1 / inv_sqrt_k_s^2. The non-centred form avoids the funnel between τ and z. 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). Exposes the per-stream dispersion vector k, the population-level dispersion k_pop = 1 / exp(μ_log)^2, the pooling SD τ and the raw deviations. Returns (; k, inv_sqrt_k, k_pop, μ_log, τ) with k a length-n_streams vector.
BVDOutbreakSize.posterior_summary Method
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.
BVDOutbreakSize.predict_committed Method
predict_committed(
chn;
obs_confirmed,
obs_confirmed_deaths,
obs_analysed,
horizon_days,
alg
) -> DataFrames.DataFramePer-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 sizeC(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 formCFR · C(T)(equivalently the realised deaths plus the committed-deaths tail ofpredict_no_onward_deaths).:confirmed_cases— committed laboratory-confirmed cases when the chain carries the lab parameters (:s_test,:spec_test,:τ_forward,:α_recv,:θ_recv) andobs_confirmed/obs_analysedare 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_deathandobs_confirmed_deathsis 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).
BVDOutbreakSize.predict_no_onward_deaths Method
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_deathsadditional future expected deaths beyondobs_deaths:total_projectedobs_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).
BVDOutbreakSize.progress_callback Method
progress_callback(; path, every)A lightweight, dependency-free streaming progress callback for nuts_sample. Returns a closure matching the AbstractMCMC callback signature
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.
BVDOutbreakSize.r_to_R0 Method
r_to_R0(r, g::AbstractVector) -> AnyReproduction 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.
BVDOutbreakSize.reconstruct_rt Method
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.
BVDOutbreakSize.recovered_model Method
recovered_model(
recovered_history,
recovered_total::Union{Missing, Integer},
confirmed_daily::AbstractVector,
CFR::Real;
recovery,
dispersion,
confirmation_to_recovery,
k_external
) -> AnyDRC 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.
BVDOutbreakSize.recovery_probability_model Method
recovery_probability_model(CFR::Real; offset_prior) -> AnyRecovery 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).
BVDOutbreakSize.renewal_infections Method
renewal_infections(
Rt::AbstractVector,
g::AbstractVector,
seed::AbstractVector
) -> AnyDaily 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.
BVDOutbreakSize.reported_cases_model Method
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
) -> AnyDRC 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.
BVDOutbreakSize.rt_walk_model Method
rt_walk_model(
n::Integer,
log_R0_base::Real;
week,
breakpoint,
rt_start,
ramp,
sigma_prior,
effect_prior
) -> AnyWeekly 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 a fortnight, the response damps R_t only partially by the cut-off.
BVDOutbreakSize.safe_nbinomial Method
safe_nbinomial(k, μ) -> Distributions.NegativeBinomialNaN / 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.
BVDOutbreakSize.safe_rate Method
safe_rate(x) -> AnyNaN / 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.
BVDOutbreakSize.seed_at_renewal_start Method
seed_at_renewal_start(C_T_prior) -> AnyRenewal-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.
BVDOutbreakSize.seed_infections Method
seed_infections(I0, r, len::Integer) -> AnySeed 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.
BVDOutbreakSize.seed_model Method
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.
BVDOutbreakSize.seeding_age Method
seeding_age(cumulative::AbstractVector, n::Integer) -> AnyOutbreak 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.
BVDOutbreakSize.severity_enrichment_model Method
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).
BVDOutbreakSize.sigmoid_ramp Method
sigmoid_ramp(
n::Integer,
day::Union{Missing, Real};
ramp
) -> AnySmooth 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.
BVDOutbreakSize.streams_table Method
streams_table(
streams::Pair{String, <:AbstractVector}...;
digits
) -> DataFrames.DataFrameSide-by-side credible intervals for C_T from several fits. Pass each fit as "label" => draws_vector.
BVDOutbreakSize.summary_table Method
summary_table(
chn,
params::AbstractVector{Symbol};
digits,
labels
) -> DataFrames.DataFrameDataFrame 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.
BVDOutbreakSize.surveillance_dispersion_model Method
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).
BVDOutbreakSize.tensorboard_callback Method
tensorboard_callback(
logdir;
kwargs...
) -> BVDOutbreakSizeTensorBoardLoggerExt.var"#4#5"{BVDOutbreakSizeTensorBoardLoggerExt.var"#6#7"{Int64, Bool, String, String, Dict{String, Vector{Float64}}, ReentrantLock, TensorBoardLogger.TBLogger{String, IOStream}}}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 parameterdiagnostics/<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:
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.
BVDOutbreakSize.test_positivity_model Method
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).
BVDOutbreakSize.test_sensitivity_model Method
test_sensitivity_model(
;
sensitivity_prior
) -> DynamicPPL.Model{typeof(test_sensitivity_model), (), (:sensitivity_prior,), (), Tuple{}, Tuple{Distributions.Beta{Float64}}, DynamicPPL.DefaultContext, false}PCR sensitivity prior. Beta(10, 1.76) is centred near a mean of about 0.85 with a spread of roughly 0.1; being a Beta it is not symmetric and carries somewhat more mass toward high sensitivity. Confirmation runs on the altona RealStar Filovirus Screen RT-PCR, which detects Bundibugyo virus at 11–67 RNA copies per reaction; the rapid Cepheid GeneXpert Ebola assay is Zaire-ebolavirus-specific and does not reliably detect Bundibugyo. The prior centres on a good analytical sensitivity while allowing modest downside for early low-viral-load specimens and field handling. Under the severe-first backlog model the first vintage's analysed batch is near-pure BVD (q ≈ 1 when selection is strong), so the v1 positivity ≈ s identifies the sensitivity from the early data. Scales the confirmed-case stream so the confirmed counts reflect imperfect detection of true BVD infections. Returns (; s_test).
BVDOutbreakSize.test_specificity_model Method
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).
BVDOutbreakSize.traveller_volume_model Method
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.
BVDOutbreakSize.treatment_admission_model Method
treatment_admission_model(
isolation_history,
bvd_reports_daily::AbstractVector,
bg_daily::AbstractVector,
p_drc::Real;
capacity_history,
admission,
severity,
capacity,
dispersion,
k_external,
bvd_los,
ruleout_los
) -> AnyDRC isolation / treatment-bed occupancy likelihood, a SUPPLY-LIMITED prevalence stream. The "Patients en isolement" figure is the daily count of occupied beds. Bed occupancy may be supply-driven — demand for beds can outstrip supply, with occupancy catching up as capacity is expanded — so the occupancy is modelled as a latent bed demand right-censored at the bed capacity, not the demand itself.
The latent demand is the suspect inflow carried through a length-of-stay survival S(τ) = P(LOS ≥ τ) (convolve_survival), the renewal analogue of the convolution secondary-observation model of EpiNow2 (Abbott et al., 2020). The reported suspects (reported_cases_model) are a BVD/background mixture whose two populations are admitted at different rates and leave on different clocks:
BVD demand
p_iso_bvd · p_drc · bvd_reportswith a sampled treatment length-of-stay (a confirmed case occupies a bed until recovery or death). BVD suspects are admitted atp_iso_bvd, skewed up from the base rate by a severity log-oddsδ_iso(isolation_severity_model): triage admits the sicker patients and BVD presents more severely, so BVD is enriched among the isolated. Admission does not condition on the unobserved BVD status of any individual; the skew is the net effect of severity-based triage.non-BVD demand
p_iso · bg_dailyat the base admission rate (isolation_admission_model), leaving once the suspect is ruled out and discharged, after a separately sampled rule-out stayruleout_los. This is a different parameter from the report-to-receipt laboratory delay (receipt_pmffromconfirmed_cases_model): a non-BVD bed is freed around the time the negative result returns, but discharge also carries clinical sign-off and bed logistics, so the rule-out stay is identified by the occupancy on its own clock.
Each report day's occupied-bed count follows a NegativeBinomial around the latent demand, right-censored at the bed capacity (censored_occupancy_model), with a dispersion k (the pooled isolation dispersion in the joint, sampled here when run standalone). The censoring bound is FIXED at the recorded implied-capacity series (censoring_cap), not a sampled ceiling: a sampled bound sitting on the data at high utilisation gives the censored likelihood a moving -Inf wall (a count above the bound has zero probability), which NUTS cannot cross and which drove ~60% divergent transitions at the ~94% utilisation of the current data. With the bound fixed, the likelihood is smooth — below capacity the count identifies demand directly, and at capacity it contributes the one-sided demand ≥ capacity tail (the marginalised censored encoding; Stan manual). The latent demand is left UNCENSORED, so demand above capacity is carried by the renewal / length-of-stay demand model and its priors, not by a fitted ceiling fraction; the bed shortfall above a saturated capacity is only partially identified from occupancy (occupancy says "demand was at least the beds filled", not how much more), so it is a prior/model-informed quantity. The capacity walk C(t) (bed_capacity_walk_model) still carries the implied-capacity likelihood and the forecast cap, and occupancy = min(demand, C) is the supply-capped stock the derived quantities report. The capacity is pinned by the implied bed count (occupancy / reported occupancy rate, capacity_history), each entry a noisy observation of C(t) on its day. Empty histories are no-ops; missing count vectors sample (the predictive path).
Exposes the cut-off occupancy, the cut-off bed demand (need under unconstrained supply), their difference (the bed shortfall), the utilisation occupancy / C, the true-BVD share of demand, the severity skew δ_iso and the BVD admission probability, and returns the daily demand and occupancy series for forecasting and posterior-predictive replication.
BVDOutbreakSize.treatment_only_model Method
treatment_only_model(
n::Integer;
isolation_history,
bed_capacity_history,
breakpoint,
infection,
onset_incidence,
cases,
treatment,
dispersion,
ascertainment
) -> AnyIsolation-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_admission_model and reported_cases_model.
BVDOutbreakSize.vintage_increments_model Method
vintage_increments_model(
modelled::AbstractVector,
increments::Union{Missing, AbstractVector{<:Integer}},
k::Real
) -> AnyPer-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.
BVDOutbreakSize.vintage_obs Method
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.