EpiAware EpiAware EpiAware
  • Approaches
    • Overview
    • Composed distributions
    • Composable Turing models
  • Tutorials
  • Docs
  • News
  • Gallery
  • Guides
    • Resources
    • Using Julia
    • FAQ
    • Contributing
    • Developer docs
  • Community
    • Get involved
    • Team
    • Funding
    • epinowcast & the forum

Composable Turing models

Build a joint model from interchangeable pieces for what drives transmission, how infections arise, and how they become the data you see.
Interchangeable parts, one interface
Uncertainty through the whole model
Built for the workflow, not the model
renewal_model.jl
using ComposableTuringIDModels, Distributions, Turing

# Renewal infections with an inline prior model
data = IDData(gen_distribution = Gamma(1.4, 1 / 0.38))

model = IDModel(
    Renewal(data;
        rt = AR(; ϵ_t = HierarchicalNormal(
            std_prior = HalfNormal(0.1))),
        initialisation_prior = Normal(log(1.0), 1.0)),

    # Multiple observation layers:
    #   delay → ascertainment → negative-binomial noise
    LatentDelay(
        Ascertainment(
            NegativeBinomialError(),
            ascertainment_prior = Beta(10, 2)),
        truncated(Normal(5.0, 2.0), 0.0, Inf)))

# Fit to case data with Turing's NUTS
chain = sample(as_turing_model(model, cases, length(cases)),
               NUTS(), MCMCThreads(), 1_000, 2)

The approach

A model is assembled from small parts rather than written as one bespoke model. Each part fills one of three roles. A prior model says what we believe about a parameter before seeing data, and it may vary over time, such as a reproduction number that drifts, or stay fixed, such as a single ascertainment rate. An infection model turns that parameter into unobserved infections, directly or through exponential growth or the renewal equation. An observation model turns those infections into the data you actually see.

One interface, no type hierarchy

Every part is a plain struct with a single method of as_turing_model, which returns a DynamicPPL.Model. There is no deep type hierarchy, and a part is identified by the method it implements rather than by its place in a tree. That is what makes the parts interchangeable: anything implementing the interface can stand in anywhere the interface is expected.

A part that contains another part builds the inner model and draws it as a submodel, so parts nest through the very interface they expose. The prior model is not a separate stage bolted on the front. It is folded into the infection model, which takes it as a slot and draws it while building the infections.

No model rewrites, only part swaps

Changing an assumption means swapping one part for another: replacing the renewal equation with exponential growth, switching from Poisson to negative-binomial observation, or adding a reporting delay. The model structure stays the same and the swapped part carries the new assumption. This makes it practical to test sensitivity to each modelling choice without maintaining a separate model for every combination of assumptions.

Layered observation models

Real data is never a direct readout of infections. Reporting delays, ascertainment (under-reporting), right-truncation, day-of-week effects, and count noise each change how infections become data. These are composable layers: each wraps the observation model below it and adds one transformation. Layers are added or removed independently. A delay is a LatentDelay wrapper, ascertainment is an Ascertainment wrapper, and noise is the core error model. Every layer is still a part that speaks as_turing_model.

One assembly for the full workflow

The same IDModel that defines the joint distribution also draws from the prior, conditions on observed data, runs inference, and produces posterior draws. There is no separate simulation model, inference model, and forecasting model, because the assembly is the model for every step. The generative story, the inference, and the forecast all share one structure, so a change to the model carries through the whole workflow.

Full Turing toolchain

Every assembly is a DynamicPPL model underneath, so the full Turing ecosystem applies: NUTS and Pathfinder for sampling, MCMCThreads for parallel chains, prior and posterior predictive checks, and the standard Turing diagnostics. There is no separate inference engine to learn.

Where next

  • Run it: the Tutorials gallery collects the runnable case studies for this approach, starting with the overview.
  • The ComposableTuringIDModels.jl documentation covers the part catalogue, design, and worked examples.
  • See the overview for the full design rationale and the nested architecture diagram.
  • Interested in the delay side of the model? See the composed distributions approach.

EpiAware — a composable Julia ecosystem for infectious disease modelling.

  • Packages

  • Get involved

  • Funding

  • Edit this page
  • Report an issue