London School of Hygiene & Tropical Medicine
14 August 2026
ComposableTuringIDModels.jlEpiNow2Turing.jlConvolvedDistributions.jlDistributions.jlsamabbott.co.uk/JuliaCon2026/composable
JuliaCon 2026, Muschel — N3, Friday 14 August 2026, 16:45.
Cramer et al. (2022), doi:10.1038/s41597-022-01517-w
Whitty (2015), doi:10.1186/s12916-015-0544-8
Prevalence (A), incidence (B), antibody prevalence (C) and \(R_t\) (D) fitted together as one model. Abbott and Funk (2022), doi:10.1101/2022.03.29.22273101. Survey figures from the paper’s introduction
Lison et al. (2024), doi:10.1371/journal.pcbi.1012021 · Endo et al. (2022), doi:10.1126/science.add4507
Warning
These design considerations were developed by the authors and have not yet received broader community input.
Table 1 of the paper pairs each requirement with the problem that motivates it, under six themes. All twelve are at epiaware.org/approaches
A workflow for infectious disease modelling, Abbott et al.
Components can be reused across contexts and combined in different configurations whilst maintaining statistical rigour.
Four illustrative applications, not implementations. The incubation period model appears in all four, the latent infection model in three, and the band beneath is what those shared components are themselves made of. Figure 1 of the paper, which is in CDC clearance. The design considerations behind it are at epiaware.org/approaches
SpeedyWeather.jlHydroModels.jlComrade.jlDomain-specific Julia ecosystems, from the roadmap talk’s inspiration deck · github.com/SpeedyWeather/SpeedyWeather.jl · github.com/HydroModels/HydroModels.jl · github.com/ptiede/Comrade.jl
ComposedDistributions.jl and ComposableTuringIDModels.jlDistributions.jlTuring.jl. Transmission, latent process and observation are separate objects that nest into one modelWording and design considerations from epiaware.org/approaches
ComposableTuringIDModels.jlTuring.jl
Photos from GitHub, read 2026-08-13 · Registered in General at v0.1.1 · github.com/EpiAware/ComposableTuringIDModels.jl · Abbott et al., Composable probabilistic models can lower barriers to rigorous infectious disease modelling, in CDC clearance
Four illustrative applications, not implementations. The incubation period model appears in all four and the latent infection model in three
struct IDModel{I, O} <: AbstractComposableModel
infection_model::I # process → I_t
observation_model::O # I_t → y_t
end
struct AR{D, I, P, E, F} <: AbstractLatentModel
damp::D
init::I
p::P
ϵ_t::E # an innovation slot
transform::F
end
struct NegativeBinomialError{S} <: AbstractObservationErrorModel
cluster_factor::S
endStruct fields abridged, parameter bounds dropped · src/latent_models/models/AR.jl and src/observation_models/ObservationErrorModels/NegativeBinomialError.jl, main at 011169f
Accessors.jl to update a componentdocs/src/design.md:18-20
ar2 = AR(;
damp = [truncated(Normal(0.2, 0.2), 0, 1),
truncated(Normal(0.1, 0.05), 0, 1)],
ϵ_t = HierarchicalNormal(std = HalfNormal(0.1)))
ma1 = MA(;
θ = [truncated(Normal(0.0, 0.2), -1, 1)],
ϵ_t = HierarchicalNormal(std = HalfNormal(0.1)))
# the AR's innovation slot now holds the MA process
arma21 = @set ar2.ϵ_t = ma1
arima211 = DiffLatentModel(arma21, Normal(0, 0.2); d = 1)Run against main at 011169f for this talk · @set is Accessors.jl, not ours · Keywords track main. The paper’s artefact pins an earlier release, where these were damp_priors, θ_priors and std_prior
::::
as_turing_model# AR draws its damp, init, and innovations as submodels
@model function as_turing_model(model::AR, n::Int)
ar_init ~ as_turing_submodel(model.init, p; prefix = true)
damp_AR ~ as_turing_submodel(model.damp, p; prefix = true)
ϵ_t ~ as_turing_submodel(model.ϵ_t, n - p)
ar = accumulate_scan(ARStep(reverse(damp_AR)),
ar_init, ϵ_t)
return ar
end
# DiffLatentModel draws its init, then descends into the AR
@model function as_turing_model(model::DiffLatentModel, n::Int)
latent_init ~ as_turing_submodel(model.init, d; prefix = true)
diff_latent ~ as_turing_submodel(model.model, n - d)
return _combine_diff(latent_init, diff_latent, d)
endsrc/latent_models/models/AR.jl:99-115 and src/latent_models/modifiers/DiffLatentModel.jl:61-68, reformatted to fit, main at 011169f, comments and @asserts removed · 61 as_turing_model definitions across 45 files in src/, counted 2026-08-12
as_turing_modelmdl is a DynamicPPL.Model; it simulates and fits from one definitionAR passes each slot to as_turing_submodel; DiffLatentModel passes its whole inner model, and the call descends from there
as_turing_submodel# a component writes its parameter once
damp ~ as_turing_submodel(model.damp, p; prefix = true)
# what is passed to the call picks the method
as_turing_submodel(m, args...; prefix = false) =
to_submodel(as_turing_model(m, args...), prefix)
as_turing_submodel(d::Distribution, ::ModelShape;
prefix = false) = dTwo of the three as_turing_submodel methods, src/base/priors.jl:65-82, reformatted to fit and with the ::Bool annotations dropped. The third takes a vector of priors · The call line is src/base/priors.jl:25 · The AR example is src/latent_models/models/AR.jl:40-43
Distribution or another Turing modelAR(damp = Normal(...)) is a constant coefficient, AR(damp = RandomWalk()) a time-varying oneSplit / StrataMap — split one expected series into several named observation streams; parallel, cascade, and strata-split pipelinesMixingStep — a mixing model drawn before the scan, so a fixed or inferred coupling matrix plugs into a RenewalConcatLatentModels — switch a latent process between segments of a seriesRenewalStep with folded-in modifiers# split into streams, one per outcome
Split((cases = PoissonError(),
deaths = NegativeBinomialError()))
# mix regions through an inferred coupling matrix
Renewal(; mixing = MixingStep())
# switch latent processes at a breakpoint
ConcatLatentModels([Intercept(Normal(2, 0.2)), AR()])
# wrap an observation stream in a delay
LatentDelay(PoissonError(), pmf)
# add day-of-week ascertainment
Ascertainment(PoissonError(), 7)src/observation_models/Split.jl:179, src/steps/MixingStep.jl:42, src/latent_models/manipulators/ConcatLatentModels.jl:29, src/steps/RenewalStep.jl:166, and src/observation_models/modifiers/LatentDelay.jl, main at 011169f, signatures abridged
EpiNow2)negbinbroadcast_weekly wraps ARIMA to produce piecewise constant weekly \(R_t\) valuesascertainment_dayofweek adds day-of-week reporting effects via softmax transformationLatentDelay wrappers compose incubation and reporting delays sequentially by nesting structsImportant
No new components needed and took ~ 3 hours
Figures 2 and 4 of the paper. Replicating a common configuration of EpiNow2, real-time estimation accounting for reporting delays, right truncation and day-of-week effects. Abbott et al. (2020), doi:10.12688/wellcomeopenres.16006.2. Daily COVID-19 cases from Italy, February to June 2020, shipped with EpiNow2. One configuration replicated, not a benchmark against the package
Turing.jlTuring.jl gave us submodels and a choice of samplersFrom the Turing.jl documentation
“not all AD libraries in there are thoroughly tested on Turing models. Thus, it is possible that some of them will either error … or maybe even silently give incorrect results”
Release dates from the GitHub API, 2026-08-10 · v0.35.5 is the release the paper’s artefact pins · turinglang.org/docs/usage/automatic-differentiation · The two practical limits are in the paper’s discussion · Logo from turinglang.org, read 2026-08-13
Enzyme.jl had 18 human authors in the last six months, 210 of its 276 commits from William Moses. Mooncake.jl had 12, with 155 of 221 from Hong GeTuringLang/ADTests published a per-backend support table for Turing models. It is archived, with no commits since May
ComposedDistributions.jlDistributions.jl distribution: chains and branches
Photos from GitHub, read 2026-08-13
cfr = 0.12
admission = @uncertain compose((
path = sequential(
:onset_admit => LogNormal(Normal(0.0, 0.2), 0.4),
:admit_outcome => resolve(
:death => (Gamma(1.5, 1.0), cfr),
:discharge => Gamma(2.0, 1.5))),
onset_report = truncated(Gamma(1.5, 1.0); upper = 21.0),
onset_referral = censored(Gamma(1.0, 2.0); upper = 14.0)))A hospital pathway from the ComposedDistributions.jl README, main checked 2026-08-13 · sequential chains delays and resolve picks an outcome with the given probability
ComposedDistributions.jl — the verb grammar aboveCensoredDistributions.jl — interval and primary-interval censoring, plus Distributions.jl’s truncationConvolvedDistributions.jl — convolution and quadratureModifiedDistributions.jl — transforming distributionsReparameterisedDistributions.jl — reparameterising distributionsDistributionsInference.jl — the interface between our composed distributions and inference approachesDistributions.jlDistributions.jl interfacerand and logpdf?Important
I have argued myself into both answers.
Important
Tell me what you would build this on, if not a probabilistic programming language.
samabbott.co.uk/JuliaCon2026/composable
The prototype is ComposableTuringIDModels.jl, github.com/EpiAware/ComposableTuringIDModels.jl
Sam Abbott, LSHTM · epiaware.org · github.com/seabbs