Overview
ComposableTuringIDModels builds an epidemiological model by assembling small parts rather than writing one bespoke model. Each part becomes a Turing / DynamicPPL model through a single generic constructor, as_turing_model. Because every part speaks that one interface, parts nest inside one another and a whole model is composed from the pieces.
Three roles, one interface
A model is put together from parts filling three roles.
A latent process describes an unobserved series
over time, such as a log reproduction number or a growth rate. An infection process turns that latent series into unobserved infections
(directly, through exponential growth, or through the renewal equation). It takes the latent process as a slot and draws it while building the infections. An observation process turns infections into the observed data
, adding reporting delays, ascertainment, day-of-week effects, right-truncation, and count noise.
Each part is a plain struct with a single method of as_turing_model, which returns a DynamicPPL.Model. There is no deep type hierarchy. A part is identified by the method it implements, not by its place in a tree. A part that contains another part builds the inner model and samples it as a submodel.
The three roles feed one another and plug into that single interface.
as_turing_model interface, and any part can be swapped for a compatible one.Swap a part to change an assumption
Because the parts share one interface, you compare modelling assumptions by swapping one struct for another and leaving the rest untouched.
One latent process: an ARIMA-style differenced AR.
using ComposableTuringIDModels, Distributions
latent = DiffLatentModel(; model = AR(), init = [Normal(), Normal()])DiffLatentModel
└─ model: AR
└─ ϵ_t: HierarchicalNormalFold it into a direct-infections process observed with Poisson noise.
poisson_model = IDModel(
DirectInfections(; Z = latent, initialisation = Normal()),
PoissonError())IDModel
├─ infection: DirectInfections
│ └─ Z: DiffLatentModel
│ └─ model: AR
│ └─ ϵ_t: HierarchicalNormal
└─ observation: PoissonErrorswap then changes the count noise.
using ComposableTuringIDModels: swap
negbin_model = swap(
err -> err isa PoissonError ? NegativeBinomialError() : err, poisson_model)IDModel
├─ infection: DirectInfections
│ └─ Z: DiffLatentModel
│ └─ model: AR
│ └─ ϵ_t: HierarchicalNormal
└─ observation: NegativeBinomialErrorEach assembly is turned into one Turing model. missing data simulates from the prior and exposes the generated quantities.
turing_model = as_turing_model(poisson_model, missing, 20)
(; generated_y_t, I_t, Z_t) = turing_model()
Z_t20-element Vector{Float64}:
-0.8461781635056509
-3.0513793004101952
-6.90707025603445
-10.780303766368032
-14.627765209563727
-18.420412161474758
-22.207922563548333
-25.990710533973218
-29.782234659718505
-33.60681153351409
-37.41129286829049
-41.25412593956265
-45.115024326509406
-48.99099347400773
-52.87599859152727
-56.76384361738823
-60.63588761223084
-64.49861286198326
-68.36701730498007
-72.21054592131338Where to go next
Composable design explains the
as_turing_modelprotocol and how parts nest as submodels in more detail.The tutorials build complete models and fit them to real surveillance data, from a renewal model to a compartmental SIR.
The Public API lists every component you can compose.