Skip to content

Overview

ComposableTuringIDModels builds an epidemiological model by assembling small parts rather than writing one bespoke model. Each part plays one of three roles — a latent process, an infection process, or an observation process — and 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, e.g. 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, so the parts nest through the same interface they expose.

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 — parts compose as submodelsSwap any part — 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.

julia
using ComposableTuringIDModels, Distributions

# One latent process: an ARIMA-style differenced AR.
latent = DiffLatentModel(; model = AR(), init_priors = [Normal(), Normal()])

# Fold it into a direct-infections process, then swap only the observation
# model. Everything else stays the same.
poisson_model = IDModel(
    DirectInfections(; Z = latent, initialisation_prior = Normal()),
    PoissonError())

negbin_model = IDModel(
    DirectInfections(; Z = latent, initialisation_prior = Normal()),
    NegativeBinomialError())

# Each assembly is turned into one Turing model. `missing` data simulates from
# the prior; the composed model exposes its generated quantities.
turing_model = as_turing_model(poisson_model, missing, 20)
(; generated_y_t, I_t, Z_t) = turing_model()
length(generated_y_t), length(I_t), length(Z_t)
(20, 20, 20)

Where to go next

  • Composable design explains the as_turing_model protocol and how parts nest as submodels in more detail.

  • The case studies 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.