ComposableTuringIDModels.jl
A toolkit for composable probabilistic infectious disease modelling in Julia.
Why ComposableTuringIDModels?
Composable models: Assemble a model from interchangeable infection and observation parts — each infection model owning its own latent process — instead of writing one monolithic model.
Swap a part to change an assumption: Change the latent process, infection process, or observation model on its own to test how each assumption shapes your conclusions.
One interface: Every part becomes a Turing / DynamicPPL model through the single
as_turing_modelconstructor, so parts nest freely as submodels. The full Turing inference toolbox (NUTS, prior simulation) applies.Simulate and infer: Generate synthetic data from any model, then run Bayesian inference on the same model with real data.
A library of parts: Random walks, AR/MA/ARIMA latent processes, renewal and exponential-growth infection models, ODE (SIR/SEIR) processes, Poisson and negative-binomial observations, reporting delays, ascertainment, and aggregation — all interchangeable.
Getting started
You assemble a model from parts, and ComposableTuringIDModels turns the assembly into a single Turing model you can simulate from and fit. Each part is itself a model, joined through the generic as_turing_model constructor.
Compose a model: an ARIMA-style latent process (a differenced AR) folded into a direct-infections process, observed with Poisson error.
using ComposableTuringIDModels, Distributions, Turing
model = IDModel(
DirectInfections(;
Z = DiffLatentModel(; model = AR(), init = [Normal(), Normal()]),
initialisation = Normal()),
PoissonError())IDModel
├─ infection: DirectInfections
│ └─ Z: DiffLatentModel
│ └─ model: AR
│ └─ ϵ_t: HierarchicalNormal
└─ observation: PoissonErrorBuild a Turing model; missing observations simulate from the prior.
n = 30
prior_model = as_turing_model(model, missing, n)DynamicPPL.Model{typeof(ComposableTuringIDModels._as_turing_model_idmodel), (:model, :y_t, :n), (), (:y_t,), Tuple{IDModel{DirectInfections{DiffLatentModel{AR{Distributions.Truncated{Distributions.Normal{Float64}, Distributions.Continuous, Float64, Float64, Float64}, Distributions.Normal{Float64}, Int64, HierarchicalNormal{Float64, Distributions.Truncated{Distributions.Normal{Float64}, Distributions.Continuous, Float64, Float64, Float64}}, typeof(identity)}, Vector{Distributions.Normal{Float64}}}, typeof(exp), Distributions.Normal{Float64}}, PoissonError}, Missing, Int64}, Tuple{}, DynamicPPL.DefaultContext, false}(ComposableTuringIDModels._as_turing_model_idmodel, (model = IDModel, y_t = missing, n = 30), NamedTuple(), DynamicPPL.DefaultContext())Sample from the prior and inspect the generated quantities.
draw = rand(prior_model)
(; generated_y_t, I_t, Z_t) = prior_model()(generated_y_t = Union{Missing, BigInt}[2, 4, 22, 131, 524, 2550, 12708, 62826, 283261, 1299435 … 39680678179927, 191985621800092, 1016859392033456, 5514526077736982, 30373158883753228, 162403997027493312, 854037625960284800, 4500778044268547584, 22131982065189990400, 119071532775459323904], expected_y_t = [1.1219383450913283, 5.06917798769595, 25.14500013338704, 119.40172143837421, 553.5113658885243, 2563.0609839034487, 12574.335451538978, 62798.737093368625, 282895.8349290275, 1.2993071319007038e6 … 3.96806887582363e13, 1.9198560432814225e14, 1.0168593617313888e15, 5.514526065354386e15, 3.037315894765604e16, 1.6240399706225482e17, 8.540376257558783e17, 4.500778042443006e18, 2.2131982065830048e19, 1.1907153276775206e20], I_t = [1.1219383450913283, 5.06917798769595, 25.14500013338704, 119.40172143837421, 553.5113658885243, 2563.0609839034487, 12574.335451538978, 62798.737093368625, 282895.8349290275, 1.2993071319007038e6 … 3.96806887582363e13, 1.9198560432814225e14, 1.0168593617313888e15, 5.514526065354386e15, 3.037315894765604e16, 1.6240399706225482e17, 8.540376257558783e17, 4.500778042443006e18, 2.2131982065830048e19, 1.1907153276775206e20], Z_t = [0.7456337527065965, 2.2537545698687733, 3.8552349729435846, 5.413069516276333, 6.946858184530082, 8.479533418049687, 10.069989044790336, 11.678266140290818, 13.183409932428555, 14.707917602903803 … 31.942461654027973, 33.519017405702726, 35.18607111304637, 36.87673800637258, 38.582911581972674, 40.25943933280263, 41.91932754400581, 43.58135785205951, 45.17413128661572, 46.856832000117016])Condition on data and run inference.
using StatsBase
posterior_model = as_turing_model(model, generated_y_t, n)
chain = sample(posterior_model, NUTS(), 1_000)
summarystats(chain)╭─FlexiSummary (9 statistics) ─────────────────────────────────────────────────╮
│ iter collapsed │
│ chain collapsed │
│ ↓ stat = [mean, std, mcse, ess_bulk, ess_tail, rhat, q5, q50, q95] │
│ │
│ Parameters (34) ── AbstractPPL.VarName │
│ Float64 init[1], init[2], diff.init, diff.damp, diff.ρ, diff.std, │
│ diff.ϵ_t[1], diff.ϵ_t[2], diff.ϵ_t[3], diff.ϵ_t[4], diff.ϵ_t[5], │
│ diff.ϵ_t[6], diff.ϵ_t[7], diff.ϵ_t[8], diff.ϵ_t[9], diff.ϵ_t[10], │
│ diff.ϵ_t[11], diff.ϵ_t[12], diff.ϵ_t[13], diff.ϵ_t[14], │
│ diff.ϵ_t[15], diff.ϵ_t[16], diff.ϵ_t[17], diff.ϵ_t[18], │
│ diff.ϵ_t[19], diff.ϵ_t[20], diff.ϵ_t[21], diff.ϵ_t[22], │
│ diff.ϵ_t[23], diff.ϵ_t[24], diff.ϵ_t[25], diff.ϵ_t[26], │
│ diff.ϵ_t[27], init_incidence │
│ │
│ Extras (14) │
│ Float64 n_steps, is_accept, acceptance_rate, log_density, │
│ hamiltonian_energy, hamiltonian_energy_error, │
│ max_hamiltonian_energy_error, tree_depth, numerical_error, │
│ step_size, nom_step_size, logprior │
│ BigFloat loglikelihood, logjoint │
│ │
│ Summary │
│ param mean std mcse ess_bulk ess_tail rhat … │
│ init[1] -0.9988 0.0000 0.0000 1000.0080 NaN 1.0000 … │
│ init[2] 1.4132 0.0000 0.0000 1000.0080 NaN 1.0000 … │
│ diff.init 1.0273 0.0000 0.0000 1000.0080 NaN 1.0000 … │
│ diff.damp 0.2035 0.0000 0.0000 1000.0080 NaN 1.0000 … │
│ diff.ρ 0.2035 0.0000 0.0000 1000.0080 NaN 1.0000 … │
│ diff.std 0.0096 0.0000 0.0000 1000.0080 1000.0080 1.0000 … │
│ diff.ϵ_t[1] 1.1884 0.0000 0.0000 1000.0080 NaN 1.0000 … │
│ diff.ϵ_t[2] 1.8598 0.0000 0.0000 1000.0080 NaN 1.0000 … │
│ diff.ϵ_t[3] -0.4070 0.0000 0.0000 1000.0080 NaN 1.0000 … │
│ diff.ϵ_t[4] 1.4690 0.0000 0.0000 1000.0080 NaN 1.0000 … │
│ diff.ϵ_t[5] 0.4169 0.0000 0.0000 1000.0080 NaN 1.0000 … │
│ diff.ϵ_t[6] -0.0426 0.0000 0.0000 1000.0080 NaN 1.0000 … │
│ diff.ϵ_t[7] 0.1987 0.0000 0.0000 1000.0080 NaN 1.0000 … │
│ diff.ϵ_t[8] -0.1326 0.0000 0.0000 1000.0080 NaN 1.0000 … │
│ diff.ϵ_t[9] -1.1374 0.0000 0.0000 1000.0080 NaN 1.0000 … │
│ diff.ϵ_t[10] -0.9952 0.0000 0.0000 1000.0080 NaN 1.0000 … │
│ diff.ϵ_t[11] 0.1871 0.0000 0.0000 1000.0080 NaN 1.0000 … │
│ diff.ϵ_t[12] -0.2294 0.0000 0.0000 1000.0080 NaN 1.0000 … │
│ diff.ϵ_t[13] 0.1444 0.0000 0.0000 1000.0080 NaN 1.0000 … │
│ diff.ϵ_t[14] -1.5490 0.0000 0.0000 1000.0080 NaN 1.0000 … │
│ diff.ϵ_t[15] -0.6433 0.0000 0.0000 1000.0080 NaN 1.0000 … │
│ diff.ϵ_t[16] 0.5979 0.0000 0.0000 1000.0080 NaN 1.0000 … │
│ diff.ϵ_t[17] -0.3092 0.0000 0.0000 1000.0080 NaN 1.0000 … │
│ diff.ϵ_t[18] -1.4087 0.0000 0.0000 1000.0080 NaN 1.0000 … │
│ diff.ϵ_t[19] -0.7176 0.0000 0.0000 1000.0080 NaN 1.0000 … │
│ diff.ϵ_t[20] -0.1561 0.0000 0.0000 1000.0080 NaN 1.0000 … │
│ diff.ϵ_t[21] -1.9548 0.0000 0.0000 1000.0080 NaN 1.0000 … │
│ diff.ϵ_t[22] 1.8670 0.0000 0.0000 1000.0080 NaN 1.0000 … │
│ diff.ϵ_t[23] 0.0318 0.0000 0.0000 1000.0080 NaN 1.0000 … │
│ diff.ϵ_t[24] -1.7096 0.0000 0.0000 1000.0080 NaN 1.0000 … │
│ diff.ϵ_t[25] -1.8230 0.0000 0.0000 1000.0080 NaN 1.0000 … │
│ diff.ϵ_t[26] 1.5974 0.0000 0.0000 1000.0080 NaN 1.0000 … │
│ diff.ϵ_t[27] -0.3036 0.0000 0.0000 1000.0080 NaN 1.0000 … │
│ init_incide… -0.8098 0.0000 0.0000 1000.0080 1000.0080 1.0000 … │
╰──────────────────────────────────────────────────────────────────────────────╯Swap a part
Because every part is interchangeable, the same swap-in/swap-out pattern applies throughout. Replace PoissonError() with NegativeBinomialError(), wrap the observation in a LatentDelay, or change the latent process — without touching the rest of the model.
Same infection process, two observation assumptions.
latent = DiffLatentModel(; model = AR(), init = [Normal(), Normal()])
infections = DirectInfections(; Z = latent, initialisation = Normal())
poisson_model = IDModel(infections, PoissonError())
negbin_model = IDModel(infections, NegativeBinomialError())IDModel
├─ infection: DirectInfections
│ └─ Z: DiffLatentModel
│ └─ model: AR
│ └─ ϵ_t: HierarchicalNormal
└─ observation: NegativeBinomialErrorThat is the point: you compare modelling assumptions by swapping parts, not by rewriting models.
Related packages
ModifiedDistributions.jl modifies the behaviour of the underlying Distributions.jl distributions.
ReparameterisedDistributions.jl switches distributions between parameterisations, so a prior can be stated in whichever one is natural.
LoweredDistributions.jl lowers a distribution onto a backend-agnostic dynamical-systems representation, so a delay distribution and a compartmental model become two views of one thing.
Turing.jl is the probabilistic programming backend every model here compiles down to.
Where to learn more
New here? Start with the Overview and the Composable design page.
Want worked examples? See the tutorials, which fit complete models to real surveillance data.
Want the full interface? Browse the Public API.
Want to see the code or report a problem? Check the GitHub repository.
Adapted from
The modelling code in this package is ported and adapted from the open-source, Apache-2.0 licensed EpiAware package (CDCgov/Rt-without-renewal, ported from the fork seabbs/Rt-without-renewal). ComposableTuringIDModels is a modified, derived work: it has been renamed, re-architected around the generic as_turing_model constructor, and upgraded to build against the latest Turing.jl. See NOTICE for attribution and LICENSE for the Apache-2.0 terms. The commit history is the record of what changed.
Part of the EpiAware ecosystem
ComposableTuringIDModels is part of EpiAware, a set of composable tools for infectious disease modelling. See the other packages in the ecosystem.
Contributing
We welcome contributions and new contributors! Please open an issue or pull request on GitHub. This package follows ColPrac and is formatted with Runic.
How to cite
If you use ComposableTuringIDModels in your work, please cite it. Citation metadata lives in CITATION.cff, which GitHub renders as a "Cite this repository" button on the repository page.
Code of conduct
Please note that the ComposableTuringIDModels project is released with a Contributor Code of Conduct. By contributing, you agree to abide by its terms.