Skip to content

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.

Composable design of ComposableTuringIDModelsA latent process nested inside an infection model feeds an observation model through the single as_turing_model interface; any of the three roles can be swapped.Infection modeltakes a latent process as a slotLatent process Ztrandom walk · AR · differenced AR …drawn, then mapped to infections ItItObservation modelmaps infections to the observed datareporting delay · ascertainmentday-of-week · right-truncationcounts: Poisson / negative-binomialytdataytas_turing_modelthe one interface every part implements, so parts compose as submodelsSwap any part to change one assumption without touching the restLatentRandomWalkAR · MA · ARIMADiffLatentModelInfectionDirectInfectionsRenewal (drives Rt)ExpGrowthRateObservationPoissonErrorNegativeBinomialErrorLatentDelay wrapper
The three roles plug into one 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.

julia
using ComposableTuringIDModels, Distributions
latent = DiffLatentModel(; model = AR(), init = [Normal(), Normal()])
DiffLatentModel
└─ model: AR
   └─ ϵ_t: HierarchicalNormal

Fold it into a direct-infections process observed with Poisson noise.

julia
poisson_model = IDModel(
    DirectInfections(; Z = latent, initialisation = Normal()),
    PoissonError())
IDModel
├─ infection: DirectInfections
│  └─ Z: DiffLatentModel
│     └─ model: AR
│        └─ ϵ_t: HierarchicalNormal
└─ observation: PoissonError

swap then changes the count noise.

julia
using ComposableTuringIDModels: swap
negbin_model = swap(
    err -> err isa PoissonError ? NegativeBinomialError() : err, poisson_model)
IDModel
├─ infection: DirectInfections
│  └─ Z: DiffLatentModel
│     └─ model: AR
│        └─ ϵ_t: HierarchicalNormal
└─ observation: NegativeBinomialError

Each assembly is turned into one Turing model. missing data simulates from the prior and exposes the generated quantities.

julia
turing_model = as_turing_model(poisson_model, missing, 20)
(; generated_y_t, I_t, Z_t) = turing_model()
Z_t
20-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.21054592131338

Where to go next ​

  • Composable design explains the as_turing_model protocol 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.