Skip to content

Public Documentation

Documentation for ComposableTuringIDModels's public interface.

ComposableTuringIDModels.ComposableTuringIDModels Module

Composable probabilistic infectious disease modelling in Julia.

ComposableTuringIDModels builds epidemiological models from small, reusable components — infection processes (each owning its own latent parameter process) and observation models — each turned into a Turing/DynamicPPL model by the single generic constructor as_turing_model. Components compose by sampling one another as submodels, so a full model is assembled rather than hand-written.

This package is ported and adapted from the open-source, Apache-2.0 licensed EpiAware package; see the NOTICE file for attribution.

Examples

julia
using ComposableTuringIDModels, Distributions
model = IDModel(
    DirectInfections(; Z = RandomWalk(), initialisation = Normal()),
    PoissonError())
rand(as_turing_model(model, missing, 20))
source

Contents

Index

Public API

ComposableTuringIDModels.AR Type

An autoregressive AR(p) latent process.

with damping coefficients from the prior in damp, initial conditions from the prior in init, and innovations from the error model ϵ_t. The order p is fixed by the damp prior (a length-k vector ⇒ order k, a single distribution or a process ⇒ order 1); the init prior is sized to match.

Each prior slot takes a raw prior: pass a bare Distribution (order 1), a vector of them (order = its length), or a process (a latent model). The ϵ_t innovations are a length-n PATH slot, so a bare Distribution there is auto-wrapped in an Intercept — a constant innovation path (one shared draw broadcast to every step); use IID for n independent innovations. At order 1 the damp slot decides whether the coefficient is constant or time-varying, through the same time-varying-parameter mechanism any component can use:

  • AR(damp = Normal(...)) — a Distribution gives a constant coefficient, drawn as a single scalar RV (efficient, no length-n allocation);

  • AR(damp = RandomWalk()) — a process gives a time-varying coefficient path , drawn at length n-1 and threaded per step.

The coefficient is mapped through transform (default tanh for a process, so an unbounded path stays in the stationary band; identity for a bounded Distribution) and tracked as the generated quantity ρ, recoverable from the chain (group(chain, :ρ)). Higher-order (p > 1) coefficients are constant; time-varying higher-order AR is not yet supported.

Examples

@example
using ComposableTuringIDModels, Distributions
ar = AR()
mdl = as_turing_model(ar, 10)
rand(mdl)
source
ComposableTuringIDModels.AbstractAccumulationStep Type

Abstract supertype for accumulation step structs used with accumulate_scan.

A concrete AbstractAccumulationStep is a callable (step)(state, ϵ) returning the next state. It is backend-agnostic: it contains no Turing/DynamicPPL machinery and is reused unchanged across model components (RandomWalk, AR, MA, LatentDelay).

source
ComposableTuringIDModels.AbstractComposableModel Type

The single light supertype for every model component in ComposableTuringIDModels.

Unlike the deep abstract hierarchy used by the original EpiAware package, this package keeps a shallow tree: one root supertype, and directly beneath it a small set of role supertypes — AbstractLatentModel, AbstractInfectionModel, AbstractObservationModel (and AbstractObservationErrorModel under the last) — that encode the role a component plays. There is no deeper AbstractTuring* tree and there are no per-concept generate_* functions; dispatch happens on the concrete struct inside the single generic as_turing_model.

Encoding the role in the type lets the composer and manipulators constrain their component slots, so passing a wrong-role component (e.g. an observation model where a latent model is expected) fails at construction rather than at sampling. See AbstractLatentModel and its siblings for the interface each role's as_turing_model must satisfy.

source
ComposableTuringIDModels.AbstractConstantRenewalStep Type

Abstract supertype for renewal accumulation steps (constant generation interval, with or without susceptible depletion).

source
ComposableTuringIDModels.AbstractInfectionModel Type

Supertype for infection process models.

An infection model maps a series length n to a path of unobserved infections I_t. It owns its own latent (parameter) process internally — generating, e.g., a (log) reproduction number or growth-rate path — and maps that to infections, so no external latent path is threaded in. Its role interface is

julia
as_turing_model(model::AbstractInfectionModel, n)  # ⇒ (; I_t, Z_t)

where the returned named tuple carries the infection path I_t and the model's internal latent draw Z_t (the (log) / growth-rate path, or nothing for models with no exposable latent such as ODEProcess). Exposing Z_t keeps the latent recoverable as a generated quantity downstream.

Members include DirectInfections, ExpGrowthRate, Renewal and ODEProcess. Only Renewal carries a generation interval; the others take a transformation directly.

n is the size of the value the model returns: a ModelShape. A bare n::Int is a length-n path, giving a plain vector I_t. An n::Dims{2} is (n_strata, n_time), which is size(I_t) — one renewal recursion per stratum, each with its own seed. CombineInfections keeps the plain Int (its stratum count is fixed by how many models it holds) and returns an n_strata x n_time matrix. Any of these compose with Split / StrataMap on the observation side for the full range of infection↔observation mapping cardinalities.

source
ComposableTuringIDModels.AbstractLatentModel Type

Alias for AbstractPriorModel.

A latent process and a parameter prior share one role: both map a length n to a length-n vector via as_turing_model(m, n). So AbstractLatentModel is a const alias for AbstractPriorModel (AbstractLatentModel === AbstractPriorModel). Either name refers to the same type. Use whichever reads better at the call site.

source
ComposableTuringIDModels.AbstractMixingModel Type

Abstract supertype for drawn coupling operators.

A fixed coupling operator is a plain matrix and needs no type. A mixing model is for the case where the operator is inferred, so it has to be rebuilt from sampled parameters on every draw. Its interface is one method,

julia
as_turing_model(m::MyMixing, n)  # ⇒ a DynamicPPL.Model returning the operator

returning whatever renewal_pressure accepts, which is usually a strata × strata matrix. Gravity is the worked example.

Handing one to Renewal's mixing keyword builds a MixingStep instead of a plain core, and the renewal resolves it before the scan through the same seam that resolves a prior-carrying modifier.

source
ComposableTuringIDModels.AbstractObservationErrorModel Type

Internal supertype shared by simple observation-error models (Poisson, negative binomial).

It exists only so that the generic observation-error as_turing_model loop — which is identical across error families — can be written once and dispatch the family-specific pieces (observation_error and generate_observation_error_priors) on the concrete type. It is the error sub-role of AbstractObservationModel; the package keeps no deeper hierarchy than this.

source
ComposableTuringIDModels.AbstractObservationModel Type

Supertype for observation models.

An observation model maps a path of expected observations Y_t to observed counts y_t. Its role interface is

julia
as_turing_model(model::AbstractObservationModel, y_t, Y_t)  # ⇒ observed counts y_t

y_t === missing triggers prior/predictive simulation; a concrete y_t conditions the model on data. Observation modifiers (e.g. LatentDelay, Ascertainment, Aggregate) are themselves AbstractObservationModels: wrapping an observation model yields another observation model. Their inner-model slots are typed AbstractObservationModel, so only observation components can be wrapped.

AbstractObservationErrorModel is the sub-role for the simple error families (Poisson, negative binomial).

source
ComposableTuringIDModels.AbstractPriorModel Type

Supertype for prior models — a parameter prior expressed as a length-n submodel rather than a bare Distribution.

A prior model maps a length n to a length-n vector of parameter values via the same as_turing_model protocol every other component speaks:

julia
as_turing_model(prior::AbstractPriorModel, n)  # ⇒ a length-`n` vector

A raw Distribution (or vector of them) is not a prior model but flows through the same as_turing_submodel seam: as_turing_model has Distribution and Vector{<:Distribution} methods, so a bare distribution composes as a length-n prior submodel exactly like a model does. This is the single role for every parameter process: a latent process (a RandomWalk for a time-varying parameter, an AR process, …) satisfies the same as_turing_model(m, n) ⇒ length-n contract, so it drops into any prior slot directly. A genuinely scalar parameter is drawn with a native tilde (σ ~ model.std), keeping the chain as small as a bare ~ dist.

source
ComposableTuringIDModels.AbstractRenewalModifier Type

Abstract supertype for renewal modifiers composed onto a RenewalStep.

The type covers two shapes. A scan modifier is called by the scan itself: it transforms the proposed new incidence and carries its own substate. It implements

  • modifier_init_state(mod, window) — the modifier's initial substate, given the step's initial incidence window. The window is passed because a substate that tracks the incidence has to match its shape: a scalar for one series, one value per stratum for a stratified renewal.

  • apply_modifier(mod, incidence, substate) — return (new_incidence, new_substate).

A prior-carrying modifier instead needs parameters sampled before the scan — a per-time importation rate, say — which the scan cannot draw, because a scan step is a plain deterministic function. It implements the other part of the interface:

  • as_turing_model(mod, n) — a DynamicPPL.Model returning the modifier used in the scan. The default method samples nothing and returns mod unchanged, so a purely deterministic modifier (e.g. SusceptibleDepletion(1000.0)) is a scan modifier and implements nothing extra. A prior-carrying modifier implements this method, draws its slots through as_turing_submodel, and returns a resolved scan modifier holding the drawn values — e.g. ImportedCases resolves to an ImportedRate and implements no scan interface of its own.

RenewalStep resolves its whole modifier tuple through this one seam (see its as_turing_model method), so a sampling modifier needs no special handling anywhere in the renewal model. A prior-carrying modifier reaching the scan means the step was built by hand and never resolved, so the scan interface errors with that message rather than a bare MethodError.

Each modifier's sampled variables are prefixed by its position in the modifier tuple, modifier_<i>, so two modifiers of the same kind cannot collide on a variable name — in Renewal(gen_int, SusceptibleDepletion(N), ImportedCases(Normal())) the importation rate is modifier_2.import_rates. Inserting or reordering a modifier therefore renames the variables of every modifier after it.

source
ComposableTuringIDModels.Aggregate Type

Aggregate the expected observations of an underlying model over reporting windows.

Each entry of aggregation gives the window length to sum over at the corresponding (broadcast) time point, and present (derived as aggregation .!= 0) marks the time points that are reported. The aggregation and presence vectors are broadcast to the observation length with RepeatEach, the expected observations are summed over each window, the inner model is applied to the present windows, and the predictions are scattered back into a full-length vector (zeros elsewhere).

Because the outermost modifier is applied first, the nesting of a LatentDelay decides the units the delay is measured in. Aggregate(LatentDelay(model, pmf), aggregation) sums into windows and then convolves, so the delay is in windows and the leading windows it consumes go unpredicted, while LatentDelay(Aggregate(model, aggregation), pmf) delays the daily series before summing, so the delay is in time points.

Either way the delay leaves the head of the series uncovered. A window with no expected values left to sum is dropped rather than scored against a zero it never measured, so the counts reported for it stay out of the likelihood. A window the delay only partially uncovers still has values to sum and is kept.

Arguments

  • ag: the Aggregate model.

  • y_t: the observed series (or missing when simulating predictively).

  • Y_t: the expected-observation series.

Examples

@example
using ComposableTuringIDModels
obs = Aggregate(PoissonError(), [0, 0, 0, 0, 0, 0, 7])
mdl = as_turing_model(obs, missing, fill(10.0, 14))
rand(mdl)

Fields

  • model: the underlying observation model applied to the aggregated windows.

  • aggregation: the per-period window lengths (0 marks an unreported point).

  • present: the boolean presence mask (aggregation .!= 0).

source
ComposableTuringIDModels.Ascertainment Type

Scale the expected observations of an underlying observation model by an ascertainment prior process.

The latent_model slot takes a latent model for a time-varying ascertainment effect, or a bare Distribution for a single constant ascertainment factor shared across the series (wrapped in an Intercept, so one value is drawn and broadcast). Whatever is passed generates a length-length(Y_t) series which is combined with the expected observations Y_t through transform before being passed to the inner observation model. The default transform applies a multiplicative effect on the exponential scale ((Y_t, x) -> xexpy.(Y_t, x)), so a value x multiplies the expected observation by exp(x). The prior is prefixed with latent_prefix (a latent model via PrefixLatentModel, a distribution via its sampled-variable name) unless the prefix is the empty string.

Arguments

  • obs_model: the Ascertainment model.

  • y_t: the observed series (or missing when simulating predictively).

  • Y_t: the expected-observation series.

Examples

@example
using ComposableTuringIDModels, Distributions
# A latent model gives a time-varying ascertainment effect. The default
# transform reads the effect on the log scale, so `log(0.1)` is a 10%
# ascertainment rate.
obs = Ascertainment(PoissonError(), FixedIntercept(log(0.1)))
rand(as_turing_model(obs, missing, fill(10.0, 5)))
# A bare Distribution / prior gives a single constant ascertainment factor.
obs_const = Ascertainment(PoissonError(), Normal(0.0, 0.1))
rand(as_turing_model(obs_const, missing, fill(10.0, 5)))

Fields

  • model: the underlying observation model the ascertained expected observations are passed to.

  • latent_model: the prior model generating the ascertainment effect (a latent model for a time-varying effect, a distribution/prior for a constant factor), prefixed unless latent_prefix is empty.

  • transform: the function (Y_t, x) combining expected observations with the ascertainment effect.

  • latent_prefix: the prefix applied to the ascertainment prior's variables.

source
ComposableTuringIDModels.BinomialError Type

A binomial observation-error model: the observed successes are binomially distributed about a per-time-point number of trials N and a success probability supplied by the expected series.

Unlike the count error families (PoissonError, NegativeBinomialError) — whose expected series Y_t is an expected count — the expected series passed to BinomialError is the success probability   (e.g. a prevalence, test-positivity, or ascertainment proportion).

The number-of-trials N comes from the data

A binomial likelihood needs a number of trials per time point, N_t. N is known data (it is not inferred), so — like the observed successes — it is supplied through the observation data y_t, not stored on the model. The BinomialError struct carries no data.

The observation data is a NamedTuple with a y field (the observed successes) and an N field (the number of trials):

julia
y_t = (y = observed_successes, N = trials)

where N is a scalar Integer (the same trials at every time point) or an AbstractVector{<:Integer} (per-time-point trials). To simulate, pass y = missing while still supplying N, e.g. y_t = (y = missing, N = fill(20, n)).

This follows the same NamedTuple-data pattern as a Split stream: the shared define_y_t hook unpacks the y field that every error model scores, and BinomialError additionally reads the N field it needs.

Examples

@example
using ComposableTuringIDModels
be = BinomialError()
# 20 trials per time point; the expected series is a success probability.
mdl = as_turing_model(be, (y = missing, N = fill(20, 10)), fill(0.3, 10))
rand(mdl)
source
ComposableTuringIDModels.BroadcastLatentModel Type

Broadcast a shorter latent process to length n under a broadcast rule.

The inner model is generated over the length the rule requires (broadcast_n), then expanded to length n (broadcast_rule).

Arguments

Examples

@example
using ComposableTuringIDModels
each = BroadcastLatentModel(RandomWalk(), 7, RepeatEach())
rand(as_turing_model(each, 10))

The model slot is a length-n PATH slot: a bare Distribution there is auto-wrapped in an Intercept, giving a constant inner path; a process, an IID, or a vector passes through. Use IID for n independent draws. It is composed through as_turing_submodel.

Fields

  • model: the underlying latent model.

  • period: the broadcast period.

  • broadcast_rule: the AbstractBroadcastRule applied.

source
ComposableTuringIDModels.CatalystODEParams Type

Declarative, model-agnostic ODE parameter component built from any Catalyst ReactionSystem, usable as the parameter component of an ODEProcess in place of the hand-coded SIRParams / SEIRParams.

You declare a reaction network and give priors for its initial conditions and rate parameters; Catalyst + ModelingToolkit generate the ODE system and a symbolic Jacobian (jac = true), so there is no hand-written vector field or Jacobian to keep in sync, and nothing here is specialised to a particular compartmental model. Construct it for an SIR network, an SEIR network, or any other network the same way — only the reactions change.

Sampling and problem rebuilding are symbolic: as_turing_model samples each supplied prior into a flat Turing variable named after its species / parameter symbol (e.g. β, S) and returns symbolic symbol => value maps, which remake places into the problem by name. There is no positional-index bookkeeping, so species / parameter ordering inside the compiled problem is never assumed. Index the resulting solution symbolically too, with the network's own handles: sol2infs = sol -> sol[rn.I, :].

Optional extension

The constructor and sampling logic load only when Catalyst and ModelingToolkit are present (using ComposableTuringIDModels, Catalyst, ModelingToolkit). The heavy symbolic stack stays out of the default install; the hand-coded models remain the zero-latency default. Constructing a CatalystODEParams before loading Catalyst raises an informative error.

Arguments

  • rn: the Catalyst ReactionSystem (e.g. from @reaction_network).

Keyword Arguments

  • tspan: the ODE solution time span.

  • u0_priors: the initial conditions, as symbolic-handle ⇒ spec pairs ([rn.S => Beta(...), rn.R => 0.0, ...]). Each spec is either a Distribution (sampled, as a flat variable named after the species) or a plain Real (a fixed initial value, not sampled). Every species of rn must appear.

  • p_priors: the rate parameters, as symbolic-handle ⇒ spec pairs ([rn.β => LogNormal(...), ...]), each a Distribution (sampled) or a fixed Real. Every parameter of rn must appear.

Fields

  • prob: the ODEProblem built from rn (auto symbolic Jacobian).

  • u0_specs: per-species specs (symbolic handle, flat name, prior-or-fixed).

  • p_specs: per-parameter specs (symbolic handle, flat name, prior-or-fixed).

Examples

julia
using ComposableTuringIDModels, Catalyst, ModelingToolkit, OrdinaryDiffEq, Distributions
sir = @reaction_network begin
    β, S + I --> 2I
    γ, I --> R
end
params = CatalystODEParams(sir;
    tspan = (0.0, 30.0),
    u0_priors = [sir.S => Beta(99, 1), sir.I => Beta(1, 99), sir.R => 0.0],
    p_priors = [sir.β => LogNormal(log(0.3), 0.1), sir.γ => LogNormal(log(0.1), 0.1)])
process = ODEProcess(params = params, sol2infs = sol -> sol[sir.I, :])
source
ComposableTuringIDModels.CombineInfections Type

Combine several infection processes into one n_strata x n_time I_t matrix.

Each model in models is drawn independently over the same series length n — a genuinely different infection process per stratum (a different region, a different pathogen variant), each with its own internal latent. Stratum k is row k of the returned I_t, the same inf_strata x time orientation StrataMap uses, so CombineInfections composes directly with Split on the observation side: IDModel(CombineInfections(models), Split(template, W)) maps several distinct infection processes onto observation streams through a weight matrix W, covering many-to-one (a single aggregation row) and many-to-many (a general W).

Each sub-model is deliberately prefixed by its names entry before being sampled as a submodel (like Split): two independent infection processes would otherwise collide on variable names (e.g. two RandomWalks both naming their innovation ϵ_t), so this is one of the components that prefixes on purpose rather than following the package's flat, prefix-off default.

n stays a plain Int here. The stratum count is fixed by how many models models holds, not by a shape argument, and each model runs its own independent scan, so there is no shared incidence window for a mixing operator to couple. See Renewal's mixing slot and Coupled patch models for the couplable case.

Fields

  • models: the vector of infection models, one per stratum.

  • names: the stratum names, used both as the submodel prefix and as the Z_t NamedTuple keys.

Examples

@example
using ComposableTuringIDModels, Distributions
north = DirectInfections(; Z = RandomWalk(), initialisation = Normal(log(50.0), 0.2))
south = DirectInfections(; Z = RandomWalk(), initialisation = Normal(log(20.0), 0.2))
model = CombineInfections([north, south], ["north", "south"])
sim = as_turing_model(model, 12)()
size(sim.I_t)   # 2 strata x 12 time points
source
ComposableTuringIDModels.CombineLatentModels Type

Combine several latent models of the same length by summing their outputs.

Each component is generated over the full length n and the results are added. When a non-empty prefix is supplied for a component it is wrapped in a PrefixLatentModel so its variables stay distinct.

Arguments

  • latent_models: the CombineLatentModels collection.

  • n: the shape to generate — a length or an (n_strata, n_time) shape, whatever the component models accept.

Examples

@example
using ComposableTuringIDModels, Distributions
combined = CombineLatentModels([Intercept(Normal(2, 0.2)), AR()])
rand(as_turing_model(combined, 10))

Fields

  • models: the vector of latent models (prefix-wrapped where a prefix is set).

  • prefixes: the vector of prefixes, one per model.

source
ComposableTuringIDModels.ConcatLatentModels Type

Concatenate several latent models along time into one length-n series.

The length n is partitioned across the component models by dimension_adaptor (default equal_dimensions); each component generates its own segment and the segments are concatenated.

Arguments

  • latent_models: the ConcatLatentModels collection.

  • n: the total length of the latent series to generate.

Examples

@example
using ComposableTuringIDModels, Distributions
combined = ConcatLatentModels([Intercept(Normal(2, 0.2)), AR()])
rand(as_turing_model(combined, 10))

Fields

  • models: the vector of latent models (prefix-wrapped where a prefix is set).

  • no_models: the number of models in the collection.

  • dimension_adaptor: maps (n, no_models) to a vector of segment lengths.

  • prefixes: the vector of prefixes, one per model.

source
ComposableTuringIDModels.ConstantRenewalStep Type

Renewal step with a constant generation interval (stored reversed) and a coupling operator.

rev_gen_int is a vector for one shared generation interval and a strata × lags matrix for one interval per stratum. mixing defaults to I, which leaves the strata uncoupled; see renewal_pressure for what else it accepts.

Fields

  • rev_gen_int: the reversed generation interval.

  • mixing: the coupling operator applied to the convolved window.

source
ComposableTuringIDModels.DiffLatentModel Type

Model a latent process as a d-fold differenced version of an inner process.

If is the inner (undifferenced) latent path supplied via model, then

and is recovered by applying cumsum d times. The d initial terms are inferred from the prior in init; d equals the length of that prior.

The init slot takes a raw prior: pass a vector of Distributions (its length sets d), or a richer prior model. It is sampled through as_turing_submodel.

The model slot is a length-n PATH slot: a bare Distribution there is auto-wrapped in an Intercept, giving a constant inner path; use IID for n independent draws.

Composing DiffLatentModel over an AR gives an ARIMA-style latent process.

Examples

@example
using ComposableTuringIDModels, Distributions
diff = DiffLatentModel(; model = RandomWalk(), init = [Normal(), Normal()])
mdl = as_turing_model(diff, 10)
rand(mdl)
source
ComposableTuringIDModels.DirectInfections Type

Model unobserved infections as a direct transformation of an internally generated latent path.

where the latent model Z supplies , is transformation, and the unconstrained initial infections are drawn from the prior in initialisation. The latent process is generated inside the model rather than threaded in from outside, so as_turing_model takes a ModelShape n and returns the named tuple (; I_t, Z_t).

This model carries no generation interval — it never uses one — so it takes a transformation directly (Renewal is the only infection model that carries a generation interval).

Handing Z a Stratify (or Replicate) and calling as_turing_model(model, (n_strata, n_time)) gives one direct-infections series per stratum, each with its own seed (from a vector-valued initialisation, or the same scalar seed broadcast to every stratum). This model has no coupling slot. A plain transformation of the latent path has no incidence window for a mixing operator to act on. See Renewal for coupled strata.

Fields

  • Z: the latent process model (an AbstractLatentModel) generating . A length-n PATH slot: a bare Distribution here is auto-wrapped in an Intercept, giving a constant path (one shared draw broadcast to length n); use IID for n independent draws, or Stratify/Replicate for a strata axis.

  • transformation: the link mapping the unconstrained sum to non-negative infections (default exp).

  • initialisation: the prior for the unconstrained initial infections (a Distribution or prior model, sampled through as_turing_submodel).

Examples

@example
using ComposableTuringIDModels, Distributions
inf = DirectInfections(; Z = RandomWalk(), initialisation = Normal())
mdl = as_turing_model(inf, 10)
rand(mdl)

Stratified, sharing a random walk with partially pooled per-stratum deviations:

@example
strat = DirectInfections(; Z = Stratify(RandomWalk(), Hierarchy()),
    initialisation = Normal())
size(as_turing_model(strat, (3, 10))().I_t)
source
ComposableTuringIDModels.DirectSample Type

Direct sampling from a model's prior (no MCMC).

apply_method(model, ::DirectSample) samples the prior: with an integer n_samples it draws that many times with Turing.Prior() (returning a chain), and with nothing it draws once with rand (returning a NamedTuple).

Fields

  • n_samples: number of prior draws, or nothing for a single rand draw.
source
ComposableTuringIDModels.ExactGP Type

An exact Gaussian-process latent process.

The exact counterpart of HilbertSpaceGP. Where the Hilbert-space model approximates the GP by a short weighted sum of fixed basis functions, this model forms the full   covariance matrix from the covariance kernel and draws the path from it directly, so it is the exact GP the Hilbert-space model approximates:

The nugget is relative: it scales with , the diagonal of . A fixed absolute nugget is swamped once the sampler visits a large , and the Cholesky factorisation then throws on a matrix that is only numerically indefinite, ending the chain; a nugget with an absolute floor instead dominates the covariance at small . Scaling it leaves the relative variance inflation at throughout. A floor of   is added so the factorisation stays defined at   exactly, where vanishes.

The path is drawn non-centred: standard-normal weights are pushed through the Cholesky factor of the covariance. As with HilbertSpaceGP this keeps only , and the length-n weights sampled, a parameterisation NUTS handles well. Unlike the Hilbert-space model, the covariance and its Cholesky factorisation depend on the sampled and , so they are rebuilt on every log-density evaluation at cost. That is the price of exactness, and the reason the Hilbert-space approximation exists; this model is the accuracy reference to compare it against, best suited to short series.

Kernels are KernelFunctions.jl types (SqExponentialKernel, Matern32Kernel, Matern52Kernel, ...) — the same kernels HilbertSpaceGP uses, and the ones AbstractGPs.jl builds exact GPs from. The kernel sees the standardised index standardised_index, exactly as in HilbertSpaceGP, so the length scale is scale-free and means the same thing for both models. That grid depends only on n, so — again as in HilbertSpaceGPas_turing_model builds it once and captures it rather than rebuilding it inside the @model body; only the covariance and its factorisation are per-evaluation work.

Fields

  • length_scale: prior for the length scale ; it must put no mass below zero, since the covariance is not positive definite at  . Checked at construction.

  • marginal_std: prior for the marginal standard deviation ; it must put no mass below zero. Checked at construction.

  • kernel: the covariance kernel, a KernelFunctions.jl Kernel (default SqExponentialKernel()).

  • jitter: relative diagonal nugget for a stable Cholesky factor (default 1e-6); the amount added is .

Sampled variables

and are sampled under those names, and the n weights as z, so a chain reads as chain[:σ] and a value is pinned with fix(model, (ℓ = 0.5, σ = 0.5)).

Those names reach the top level of a composed model unprefixed, where σ collides with the σ of an error model such as NormalError and check_model fails. Wrap the process in PrefixLatentModel and its hyperparameters become gp.ℓ and gp.σ. Composable design covers why names are generic and prefixes local.

Examples

@example
using ComposableTuringIDModels, Distributions
gp = ExactGP()
mdl = as_turing_model(gp, 30)
rand(mdl)

A rougher prior with a Matérn-3/2 kernel:

@example
gp_matern = ExactGP(kernel = Matern32Kernel())
length(as_turing_model(gp_matern, 30)())

Composed with an error model that owns a σ of its own, prefixed so the two scales stay apart:

@example
using DynamicPPL: VarInfo
composed = IDModel(
    Renewal(;
        generation_time = [0.3, 0.4, 0.3],
        rt = PrefixLatentModel(; model = gp, prefix = "gp"),
        initialisation = Normal()
    ),
    NormalError()
)
keys(VarInfo(as_turing_model(composed, fill(10.0, 20), 20)))
source
ComposableTuringIDModels.ExpGrowthRate Type

Model unobserved infections via an internally generated time-varying exponential growth rate.

where the latent model rt supplies the (log) growth rates , is transformation, and the unconstrained initial infections come from the prior in initialisation. The growth-rate process is generated inside the model, so as_turing_model takes a ModelShape n and returns the named tuple (; I_t, Z_t) with Z_t the growth-rate path.

This model carries no generation interval — it never uses one — so it takes a transformation directly (Renewal is the only infection model that carries a generation interval).

Handing rt a Stratify (or Replicate) and calling as_turing_model(model, (n_strata, n_time)) gives one growth-rate series per stratum, each cumulated (and seeded) independently. This model has no coupling slot. The cumulative sum has no incidence window for a mixing operator to act on. See Renewal for coupled strata.

Fields

  • rt: the latent process model (an AbstractLatentModel) generating the growth-rate path. A length-n PATH slot: a bare Distribution here is auto-wrapped in an Intercept, giving a constant path (one shared draw broadcast to length n); use IID for n independent draws, or Stratify/Replicate for a strata axis.

  • transformation: the link mapping the unconstrained cumulative sum to non-negative infections (default: numerically equivalent to exp, implemented via LogExpFunctions.xexpy for numerical stability).

  • initialisation: prior for the unconstrained initial infections (a Distribution or prior model, sampled through as_turing_submodel).

Examples

@example
using ComposableTuringIDModels, Distributions
egr = ExpGrowthRate(; rt = RandomWalk(), initialisation = Normal())
rand(as_turing_model(egr, 10))

Stratified, each stratum an independent growth-rate path:

@example
strat = ExpGrowthRate(; rt = Replicate(RandomWalk()), initialisation = Normal())
size(as_turing_model(strat, (3, 10))().I_t)
source
ComposableTuringIDModels.FixedIntercept Type

A fixed (non-sampled) intercept broadcast to a length-n latent process.

source
ComposableTuringIDModels.Gravity Type

A gravity coupling operator with inferred exponents.

The exponents of gravity are prior slots, so the operator is rebuilt from sampled values on every draw. The coupling between strata is therefore estimated rather than assumed. Handing one to Renewal's mixing keyword is the only difference from a fixed matrix. The renewal draws it through the step seam before the scan.

Each exponent slot takes a bare Distribution (one scalar draw), a FixedIntercept to hold it fixed, or any other prior model, so a partly-fixed operator needs no separate type.

Both the fixed and the inferred path call the same gravity function, so they cannot drift.

Fields

  • pop: the population of each stratum.

  • dist: the strata × strata distance matrix.

  • α: prior for the exponent on the destination population.

  • β: prior for the exponent on the origin population.

  • γ: prior for the exponent on distance.

  • within: the own-stratum weight, relative to a typical pairwise term.

Examples

@example
using ComposableTuringIDModels, Distributions
pop = [1.0e6, 2.0e5]
dist = [0.0 50.0; 50.0 0.0]
g = Gravity(pop, dist; α = Normal(0, 0.5), β = Normal(0, 0.5),
    γ = truncated(Normal(2, 0.5), 0, Inf))
size(as_turing_model(g, (2, 20))())
source
ComposableTuringIDModels.HalfNormal Type

A half-normal prior distribution parameterised by its mean μ.

so that  .

Examples

julia
using ComposableTuringIDModels, Distributions
hn = HalfNormal(1.0)
nothing
# output
source
ComposableTuringIDModels.HierarchicalNormal Type

A non-centred hierarchical normal latent process.

Fields

  • mean: the mean of the normal process.

  • std: the prior for the standard deviation — a Distribution (a constant , one scalar RV) or a process (a length-n, e.g. time-varying, scale). Drawn through the single as_turing_submodel seam and broadcast over the innovations, so a process makes the scale time-varying (stochastic volatility) with no other change.

  • add_mean: flag controlling whether mean is added (false when mean == 0).

Examples

@example
using ComposableTuringIDModels, Distributions
hn = HierarchicalNormal()
mdl = as_turing_model(hn, 10)
rand(mdl)
source
ComposableTuringIDModels.Hierarchy Type

Partially pool a per-group level across groups, where the cross-group relationship is itself a prior model.

Hierarchy is a non-centred partial-pooling latent process over a grouping dimension. Built with as_turing_model(h, n_groups) it draws a single shared level from mean and n_groups group deviations from across, and returns the numeric length-n_groups vector

with the group deviations. It returns numeric values (like every other latent model), so it threads straight into any latent slot — e.g. an infection model's Z — with no group-axis contract change.

The number of groups is not a field of the struct: it is supplied at build time (read from the grouping dimension of the data), exactly the way a series length n is passed to as_turing_model(latent, n).

Both slots take a raw prior (a bare Distribution, or a latent/prior model), sampled through as_turing_submodel, so the pooling behaviour is parameterised rather than hard-coded through across:

  • an i.i.d.-Normal (IID(Normal()), the default) gives classic exchangeable partial pooling — each group's deviation is an independent draw shrunk toward the shared level;

  • a RandomWalk relates neighbouring groups (adjacent age-bands / ordered strata), so the group effects are correlated along the grouping dimension;

  • any other AbstractLatentModel works — the hierarchy is a prior process over the grouping dimension.

This is the numeric, contract-compliant partial-pooling construct: it returns length-n_groups values rather than per-group model variants, and takes its cross-group relationship through the prior interface.

Fields

  • mean: prior for the shared level (a Distribution or prior model).

  • across: the cross-group relationship generating the group deviations (default IID(Normal())). A length-n_groups PATH slot: a bare Distribution here is auto-wrapped in an Intercept — a constant deviation shared across groups — so use IID for exchangeable (independent) group deviations.

Examples

@example
using ComposableTuringIDModels, Distributions
# Partially pool a per-group level across 3 groups with classic (exchangeable)
# pooling; n_groups is supplied at build time.
h = Hierarchy(; across = IID(Normal(0.0, 1.0)))
length(as_turing_model(h, 3)())
source
ComposableTuringIDModels.HilbertSpaceGP Type

A Hilbert-space approximate Gaussian process (HSGP) latent process.

A Gaussian process places a prior over functions and is a natural latent process for a smoothly varying quantity such as . An exact GP is impractical inside a sampler: it needs an   covariance factorisation that costs per leapfrog step. This model uses the Hilbert-space basis-function approximation of [ DocumenterCitations.CitationSiteNode("riutortmayol2023practical-cite-3")

], which writes the GP as a short weighted sum of fixed basis functions

where the eigenfunctions and eigenvalues of the Laplacian on the interval   are

and is the spectral density of the chosen covariance kernel. Kernels are KernelFunctions.jl types, so the model reuses the ecosystem-standard kernels rather than defining its own: SqExponentialKernel (the default) gives very smooth paths, while Matern32Kernel / Matern52Kernel give progressively rougher ones. Only the spectral_density changes between kernels; the basis is shared. The Gaussian-process case study checks the basis against the Gram matrix AbstractGPs.jl builds from the same kernel, and compares this model against ExactGP.

Only , and the weights are sampled; the basis and eigenvalues depend only on n, m and the boundary factor c, not on any sampled parameter. as_turing_model therefore builds them outside the @model body and captures them, so nothing in the basis is differentiated and each log-density evaluation only reweights and combines a fixed matrix. (Composed inside another model — a Renewal whose rt slot is this GP — the enclosing @model reconstructs its submodels on every evaluation, so the basis is rebuilt inside the traced call. It still depends on no sampled parameter, but it is executed and taped like the rest of the body, so it is not free; the Gaussian-process case study measures what that costs.) The latent path is a cheap matrix–vector product of a fixed basis against a small set of standard-normal weights — a non-centred parameterisation that is fast and samples well under NUTS, including with Mooncake reverse-mode AD.

The accuracy/speed trade-off is controlled by two numbers [ DocumenterCitations.CitationSiteNode("riutortmayol2023practical-cite-4")

]: the number of basis functions m (more basis functions resolve shorter length scales, at linear cost) and the boundary factor c (the domain is extended to    beyond the half-range of the standardised inputs, so that boundary effects do not distort the fit). Because the inputs are standardised to unit standard deviation (see standardised_index), is scale-free — measured in standard deviations of the inputs, not raw time steps — so a fixed m stays adequate as the series length changes.

The two act at opposite ends of the length-scale range, and each sets the floor on accuracy where the other cannot help. A short needs a larger m: the basis has to resolve wiggles finer than . A long — comparable to the standardised half-range   — needs a larger c, because the approximation is periodic on   and a slowly varying path feels that boundary; adding basis functions does nothing for it. Below c = 1.2 that boundary error cannot be cleared by any m, so the constructor rejects it.

The defaults are tuned for the squared-exponential kernel

With m = 20 and c = 1.5 the squared-exponential covariance is reconstructed to a few parts in ten thousand for between roughly 0.3 and 0.5, degrading to 1.6% at   and 26% at   (where m is what fixes it: m = 60 brings   back to three parts in ten thousand) and to 1.5% at   (where c sets the floor instead). The Matérn kernels are markedly worse at the same m — their spectral density has an algebraic rather than Gaussian tail, so the truncated basis discards more of it. At   the Matern32Kernel error is about 2.5%, roughly 110 times the squared-exponential figure, and at   it is still 0.6%; Matern52Kernel sits between the two.

A truncated basis loses variance rather than adding it, so where m is too small the process is systematically under-dispersed: at m = 20 the implied marginal standard deviation against a nominal   is 0.99/0.96/0.97 (squared-exponential / Matérn-3/2 / Matérn-5/2) at   and 0.89/0.84/0.86 at  , which biases the inferred upward without being visible in a plot of the fit. The default prior puts roughly a fifth of its mass below 0.15, so raise m for a fit that settles on a short length scale, and raise it further for a Matérn kernel. The prior's floor of 0.05 is a numerical guard on the spectral density, not a claim that a 20-function basis resolves that scale.

Fields

  • length_scale: prior for the length scale ; it must put no mass below zero, since   has no spectral density. Checked at construction.

  • marginal_std: prior for the marginal standard deviation ; it must put no mass below zero. Checked at construction.

  • m: number of basis functions.

  • c: boundary factor; the GP is approximated on   with  .

  • kernel: the covariance kernel, a KernelFunctions.jl Kernel (default SqExponentialKernel()).

Sampled variables

and are sampled under those names, and the m basis weights as β, so a chain reads as chain[:σ] and a value is pinned with fix(model, (ℓ = 0.5, σ = 0.5)).

Those names reach the top level of a composed model unprefixed, where σ collides with the σ of an error model such as NormalError and check_model fails. Wrap the process in PrefixLatentModel and its hyperparameters become gp.ℓ and gp.σ. Composable design covers why names are generic and prefixes local.

Examples

@example
using ComposableTuringIDModels, Distributions
gp = HilbertSpaceGP()
mdl = as_turing_model(gp, 30)
rand(mdl)

A rougher prior with a Matérn-3/2 kernel:

@example
gp_matern = HilbertSpaceGP(kernel = Matern32Kernel())
length(as_turing_model(gp_matern, 30)())

Composed with an error model that owns a σ of its own, prefixed so the two scales stay apart:

@example
using DynamicPPL: VarInfo
composed = IDModel(
    Renewal(;
        generation_time = [0.3, 0.4, 0.3],
        rt = PrefixLatentModel(; model = gp, prefix = "gp"),
        initialisation = Normal()
    ),
    NormalError()
)
keys(VarInfo(as_turing_model(composed, fill(10.0, 20), 20)))
source
ComposableTuringIDModels.IDModel Type

A composed epidemiological model linking an infection process and an observation model.

The infection process owns its own latent (parameter) process internally, so a composed model is just two parts: infections, then observations.

Sampling as_turing_model(model, y_t, n) runs the two stages as submodels:

The returned generated quantities are (; generated_y_t, expected_y_t, I_t, Z_t). generated_y_t is the observation model's sampled y_t (the observed-or-simulated series, or a NamedTuple of streams for a Split); expected_y_t is its pre-error expected series (the uniform observation return contract). Z_t is the infection model's internal latent draw (e.g. the (log) path), kept accessible as a generated quantity, or nothing for infection models with no exposable latent (e.g. ODEProcess). Pass y_t = missing to simulate from the prior, or a data vector to condition.

Fields

  • infection_model: the infection process model generating (and its internal latent ).

  • observation_model: the observation model mapping to .

Examples

@example
using ComposableTuringIDModels, Distributions
model = IDModel(
    DirectInfections(; Z = RandomWalk(), initialisation = Normal()),
    PoissonError())
mdl = as_turing_model(model, missing, 20)
rand(mdl)
source
ComposableTuringIDModels.IDObservables Type

Container for the outputs of an inference run: the model, the data, the posterior samples, and any generated quantities.

Fields

  • model: the model that was sampled.

  • data: the data the model was conditioned on.

  • samples: the posterior samples (or optimiser result).

  • generated: generated quantities, or missing if not computed.

source
ComposableTuringIDModels.IDProblem Type

A full epidemiological inference problem: an infection process, an observation model, and a time span. The latent (parameter) process is owned by the infection model, so it is not a separate slot here.

as_turing_model(problem, data) assembles the corresponding IDModel over tspan and conditions it on data.y_t. The infection process's shape is read from the observation model and data.y_t at build time (see infection_strata), not stored on the problem: a plain vector (or missing) gives a single-series infection process, exactly as today, while a data matrix or a NamedTuple of streams gives a stratified infection process with one row per stratum.

Arguments

  • idproblem: the IDProblem.

  • data: a value with a y_t field holding the observations (or missing).

Examples

@example
using ComposableTuringIDModels, Distributions
problem = IDProblem(
    infection = DirectInfections(; Z = RandomWalk(), initialisation = Normal()),
    observation_model = PoissonError(),
    tspan = (1, 20))
rand(as_turing_model(problem, (; y_t = missing)))

Fields

  • infection: the infection process model.

  • observation_model: the observation model.

  • tspan: the (first, last) time span of the series.

source
ComposableTuringIDModels.IID Type

Model a latent process as independent, identically distributed draws from ϵ_t.

Examples

@example
using ComposableTuringIDModels, Distributions
model = IID(Normal(0, 1))
mdl = as_turing_model(model, 10)
rand(mdl)
source
ComposableTuringIDModels.ImportedCases Type

Imported-cases modifier for RenewalStep.

Adds an externally seeded importation rate to the renewal incidence,

so infections can arrive from outside the modelled population — the mechanism behind a renewal process that would otherwise die out from a zero initial incidence, and behind reintroduction after local elimination.

For infection arriving from another modelled stratum, rather than from outside the system altogether, use the renewal mixing slot instead (see renewal_pressure); handing this modifier a Stratify rate gives a strata × time importation rate, one exogenous stream per stratum, read per step with at exactly as a shared rate is.

importation_rate is a per-step parameter slot holding the unconstrained rate , read at step with at: a bare Distribution is one unknown constant shared across time, a Vector{<:Distribution} or a latent process (e.g. a RandomWalk) is a length-n path. The modifier maps whatever it gets onto the positive rate with its own transformation (default exp), so importation is positive by construction and any latent process can drive it — a time-varying stays positive even as the underlying path crosses zero.

The rate is drawn before the scan through the modifier seam (see AbstractRenewalModifier), giving an ImportedRate that adds at step . Where it sits in the modifier tuple therefore decides how it composes: placed after a SusceptibleDepletion the imports are added to the depleted incidence, so they are not scaled by the susceptible fraction and do not themselves deplete the pool; placed before it they are treated as part of the incidence the pool depletes.

The drawn rate is named import_rates, prefixed by the modifier's position in the renewal step's modifier tuple, so its posterior is read from a chain as

julia
model = Renewal(gen_int, SusceptibleDepletion(N), ImportedCases(Normal());
    rt = RandomWalk(), initialisation = Normal())
# `modifier_2` because importation is the second modifier; `exp` puts the
# draws back on the scale of imports per unit time.
exp.(vec(chain[@varname(modifier_2.import_rates)]))

Inserting or reordering modifiers renames it.

Nothing clamps the incidence afterwards: the renewal recursion stays positive because is, which is transformation's job. Passing transformation = identity hands that job to you, and a rate that goes negative then subtracts from the incidence.

Arguments

  • importation_rate: the unconstrained importation-rate prior — a Distribution, a Vector{<:Distribution}, or any prior/latent process.

Keyword Arguments

  • transformation: the map from the unconstrained rate onto the positive importation rate (default exp).

Examples

Drawing the modifier through its pre-scan seam shows the rate the scan will add. A bare Distribution gives one constant, here with median    imports per unit time:

@example
using ComposableTuringIDModels, Distributions, Random
Random.seed!(189)
as_turing_model(ImportedCases(Normal(-1.0, 0.5)), 5)().rate

A latent process gives a path, positive at every time despite the walk itself being unconstrained:

@example
Random.seed!(189)
as_turing_model(ImportedCases(RandomWalk()), 5)().rate

Either goes onto a renewal process as a positional modifier, where the rate joins the model's parameters under the modifier's position:

@example
r = Renewal([0.2, 0.3, 0.5], ImportedCases(Normal(-1.0, 0.5));
    rt = FixedIntercept(0.0), initialisation = Normal())
keys(rand(as_turing_model(r, 5)))

Fields

  • importation_rate: the unconstrained importation-rate prior.

  • transformation: the map onto the positive importation rate.

source
ComposableTuringIDModels.Intercept Type

Broadcast a single sampled intercept value to a length-n latent process.

The field intercept sets the prior the intercept is drawn from — a Distribution, drawn with a native tilde (a single scalar draw broadcast to length n).

Examples

@example
using ComposableTuringIDModels, Distributions
int = Intercept(Normal(0, 1))
mdl = as_turing_model(int, 10)
rand(mdl)
source
ComposableTuringIDModels.LatentDelay Type

Apply a reporting delay to an underlying observation model.

The expected observations are convolved with the (reversed) delay PMF before being passed to the wrapped model. LatentDelay shortens the expected observation vector by the length of the delay PMF to avoid fitting to partially observed data.

The delay composes through the same constant-vs-process seam as every other parameter, so it can be fixed (a known PMF, or a continuous distribution discretised once at construction), uncertain (an UncertainDelay whose distribution's parameters are prior slots, sampled and rediscretised per draw so the delay is inferred), or time-varying (a delay that changes with time). A time-varying delay is either a deterministic per-time sequence of PMFs or an UncertainDelay with a process-valued parameter; it is applied with a time-indexed convolution (one reversed kernel per step). The uncertain and process-valued cases thread their parameters through the same as_turing_submodel seam every other component uses, so a prior on the delay composes exactly like a prior anywhere else in the model.

Constructors

  • LatentDelay(model, pmf) — from a fixed delay PMF (non-negative, sums to 1).

  • LatentDelay(model, distribution; D, Δd) — discretise a fixed continuous delay distribution once via double-interval censoring (CensoredDistributions.jl). D is the truncation horizon; if omitted and distribution has finite support (e.g. it was built with truncated), the support's upper bound is used as D directly, so a caller who has already truncated the distribution does not need to repeat the horizon as a separate keyword — truncated(dist, lower, upper) is enough.

  • LatentDelay(model, pmfs::AbstractVector{<:AbstractVector}) — a deterministic time-varying delay from a per-time sequence of PMFs (one per time point, all the same length, each non-negative and summing to 1).

  • LatentDelay(model, delay::UncertainDelay) — an inferred delay whose distribution parameters carry priors; time-invariant when the parameters are Distributions and time-varying when any is a process (see UncertainDelay).

Fields

  • model: the wrapped observation model the delayed expected observations are passed to.

  • delay: the delay specification — the reversed fixed delay PMF (a vector), a per-time sequence of PMFs, or an UncertainDelay component that samples the delay parameters and builds the PMF(s) per draw.

Examples

A fixed delay:

@example
using ComposableTuringIDModels, Distributions
obs = LatentDelay(NegativeBinomialError(), truncated(Normal(5.0, 2.0), 0.0, Inf))
mdl = as_turing_model(obs, missing, fill(10, 30))
mdl()

A fixed delay with an explicit horizon expressed on the distribution itself (truncating above at 15, rather than passing D = 15.0 separately):

@example
bounded = LatentDelay(
    NegativeBinomialError(), truncated(Normal(5.0, 2.0), 0.0, 15.0)
)
length(bounded.delay)

A deterministic time-varying delay — a PMF per time point (here sharpening over time), all the same length:

@example
n = 30
pmfs = [(w = [0.6 - 0.01t, 0.3, 0.1 + 0.01t]; w ./ sum(w)) for t in 1:n]
tv = LatentDelay(PoissonError(), pmfs)
as_turing_model(tv, missing, fill(100.0, n))().y_t
source
ComposableTuringIDModels.MA Type

A moving-average MA(q) latent process.

with coefficients from the prior in θ and innovations from the error model ϵ_t. The order q is the length of the coefficient prior.

ϵ_t is a length-n path slot: a process gives time-varying innovations, while a bare Distribution is auto-wrapped in an Intercept, giving a constant innovation path (one shared draw broadcast to every step). Use IID for n independent innovations.

At order 1 the θ slot decides whether the coefficient is constant or time-varying, through the same single-seam mechanism as AR's damping: MA(θ = Normal(...)) is a constant coefficient (one scalar RV) while MA(θ = RandomWalk()) threads a per-step coefficient path. Higher-order (q > 1) coefficients are constant.

Examples

@example
using ComposableTuringIDModels, Distributions
ma = MA()
mdl = as_turing_model(ma, 10)
rand(mdl)
source
KernelFunctions.Matern32Kernel Type
julia
Matern32Kernel(; metric=Euclidean())

Matérn kernel of order with respect to the metric.

Definition

For inputs and metric   , the Matérn kernel of order is given by

By default, is the Euclidean metric   .

See also: MaternKernel

source
KernelFunctions.Matern52Kernel Type
julia
Matern52Kernel(; metric=Euclidean())

Matérn kernel of order with respect to the metric.

Definition

For inputs and metric   , the Matérn kernel of order is given by

By default, is the Euclidean metric   .

See also: MaternKernel

source
ComposableTuringIDModels.MissingObservations Type

A partially-missing observation vector split into a concrete value vector and a presence mask, so it carries no Missing in its type.

value[i] is the observed entry at i when present[i] is true, and an unused placeholder otherwise. Defined here (rather than in compose.jl, where concrete_observations builds one) so that it loads before the observation-error models that score one directly, further down the include order.

Examples

@example
using ComposableTuringIDModels: MissingObservations
carrier = MissingObservations([1.0, 0.0, 3.0], [true, false, true])
carrier.value[carrier.present]
source
ComposableTuringIDModels.MixingStep Type

A renewal core whose coupling operator is drawn before the scan.

This is to a coupling operator what ImportedCases is to an importation rate. A scan step is a deterministic function, so an operator built from sampled parameters cannot be assembled inside the recursion. MixingStep holds the generation interval and an AbstractMixingModel. Its as_turing_model seam draws the model's parameters and hands back a ConstantRenewalStep carrying the realised operator.

It is generic over every mixing model, so a new movement model needs only its own as_turing_model returning a matrix.

Fields

  • rev_gen_int: the reversed generation interval.

  • mixing: the mixing model drawn before the scan.

source
ComposableTuringIDModels.ModelShape Type

The shape a shape-aware component's n argument can take.

n::Int asks for a length-n path; n::Dims{2} asks for an (n_strata, n_time) matrix, which is size(I_t) for a stratified infection process. A bare Distribution is the one exception to the contract: it ignores whichever shape it is asked for and draws a single scalar.

Examples

@example
using ComposableTuringIDModels
ModelShape
source
ComposableTuringIDModels.NUTSampler Type

NUTS sampling method for a DynamicPPL.Model.

Fields

  • target_acceptance: target acceptance rate.

  • adtype: automatic-differentiation backend.

  • mcmc_parallel: MCMC parallelisation strategy.

  • nchains: number of chains.

  • max_depth: NUTS tree-depth limit.

  • Δ_max: divergence threshold.

  • init_ϵ: initial step size (0.0 lets NUTS find one).

  • ndraws: total draws.

  • metricT: HMC metric type.

  • nadapts: adaptation steps (-1 uses the Turing default).

source
ComposableTuringIDModels.NegativeBinomialError Type

A negative-binomial observation-error model with an inferred cluster factor.

The field cluster_factor sets the prior for the cluster factor — a Distribution (a constant, one scalar RV) or a process (a length-n, e.g. time-varying, overdispersion). It is drawn through the single as_turing_submodel seam and read per time point via at, so a process makes the overdispersion time-varying with no other change.

Examples

@example
using ComposableTuringIDModels, Distributions
nb = NegativeBinomialError()
mdl = as_turing_model(nb, missing, fill(10, 10))
rand(mdl)
source
ComposableTuringIDModels.NegativeBinomialMeanClust Function

Construct a SafeNegativeBinomial from a mean μ and cluster factor α using the variance relationship    .

Arguments

  • μ: the mean of the distribution.

  • α: the cluster factor relating mean and variance.

Examples

@example
using ComposableTuringIDModels
NegativeBinomialMeanClust(10.0, 0.1)
source
ComposableTuringIDModels.NormalError Type

A normal (Gaussian) observation-error model with an inferred standard deviation.

Unlike PoissonError and NegativeBinomialError, which model count observations, NormalError models continuous observations: each observed value is normally distributed about its expected value,

with the standard deviation drawn from the prior in std. It is the minimal non-count observation error, useful for already-aggregated or transformed quantities (e.g. log-incidence, prevalence proportions, wastewater concentrations) where a Gaussian likelihood is appropriate.

The field std sets the prior for — a Distribution (a constant, one scalar RV) or a process (a length-n, e.g. time-varying, standard deviation). It is drawn through the single as_turing_submodel seam and read per time point via at, so a process makes the observation noise time-varying with no other change.

Examples

@example
using ComposableTuringIDModels, Distributions
ne = NormalError()
mdl = as_turing_model(ne, missing, fill(10.0, 10))
rand(mdl)
source
ComposableTuringIDModels.Null Type

A null latent model that generates nothing (no latent variables).

Examples

julia
using ComposableTuringIDModels
null = Null()
mdl = as_turing_model(null, 10)
isnothing(mdl())

# output

true
source
ComposableTuringIDModels.ODEProcess Type

An infection process defined by solving an ODE.

ODEProcess combines a parameter struct (params, e.g. SIRParams or SEIRParams, whose as_turing_model samples (u0, p)) with a solver, extra solver_options, and a sol2infs link mapping the ODE solution to a latent-infection series. The compartmental dynamics are fully determined by the sampled ODE parameters, so the model carries no separate latent process: its as_turing_model samples the parameters, solves the ODE, and returns (; I_t, Z_t) with Z_t = nothing (no exposable latent path).

Arguments

  • infection: the ODEProcess.

  • n: the requested series length; passed through to the parameter model (the ODE dimension is fixed, so n is otherwise unused — nothing is also accepted).

Examples

@example
using ComposableTuringIDModels, OrdinaryDiffEq, Distributions, LogExpFunctions
sirparams = SIRParams(
    tspan = (0.0, 100.0),
    infectiousness = LogNormal(log(0.3), 0.05),
    recovery_rate = LogNormal(log(0.1), 0.05),
    initial_prop_infected = Beta(1, 99))
N = 1000.0
sir_process = ODEProcess(
    params = sirparams,
    sol2infs = sol -> softplus.(N .* sol[2, :]),
    solver_options = Dict(:saveat => 1.0))
as_turing_model(sir_process, nothing)()

Fields

  • params: the ODE parameter model (an AbstractLatentModel, e.g. SIRParams / SEIRParams, whose as_turing_model samples (u0, p)).

  • solver: the ODE solver (default AutoVern7(Rodas5P())).

  • sol2infs: link mapping the ODE solution to an infection series.

  • solver_options: extra options passed to solve (a Dict or NamedTuple).

source
ComposableTuringIDModels.PoissonError Type

A Poisson observation-error model.

Examples

julia
using ComposableTuringIDModels
poi = PoissonError()
mdl = as_turing_model(poi, missing, fill(10, 10))
rand(mdl)
nothing
# output
source
ComposableTuringIDModels.PrefixLatentModel Type

Wrap an inner latent model so its sampled variables are prefixed with prefix.

The inner model is prefixed with DynamicPPL.prefix before being sampled as a submodel, so its variables appear as prefix.varname.

Arguments

  • model: the inner latent model.

  • n: the shape to generate — a length or an (n_strata, n_time) shape, whatever the inner model accepts.

Examples

@example
using ComposableTuringIDModels
pm = PrefixLatentModel(; model = HierarchicalNormal(), prefix = "Test")
rand(as_turing_model(pm, 10))

The model slot takes a raw component: a latent model, or a Distribution (or a vector of them).

Fields

  • model: the latent model to prefix.

  • prefix: the string prefix applied to the inner model's variables.

source
ComposableTuringIDModels.PrefixObservationModel Type

Wrap an inner observation model so its sampled variables are prefixed with prefix.

The inner model is prefixed with DynamicPPL.prefix before being sampled as a submodel, so its variables appear as prefix.varname.

Arguments

  • observation_model: the PrefixObservationModel.

  • y_t: the observed series (or missing when simulating predictively).

  • Y_t: the expected-observation series.

Examples

@example
using ComposableTuringIDModels
pm = PrefixObservationModel(; model = PoissonError(), prefix = "Test")
mdl = as_turing_model(pm, missing, fill(10.0, 5))
rand(mdl)

Fields

  • model: the inner observation model to prefix.

  • prefix: the string prefix applied to the inner model's variables.

source
ComposableTuringIDModels.R_to_r Function

Approximate the exponential growth rate r implied by a reproduction number R₀ and discrete generation interval w.

Solves   by a small-r initial guess refined with newton_steps Newton iterations.

Arguments

  • R₀: the reproduction number.

  • w: the discrete generation interval weights (or a Renewal model, whose generation interval is used).

Keyword Arguments

  • newton_steps: number of Newton refinement steps (default 2).

  • Δd: generation-interval discretisation width (default 1.0).

Examples

@example
using ComposableTuringIDModels
R_to_r(1.5, [0.2, 0.3, 0.5])
source
ComposableTuringIDModels.RandomWalk Type

Model the latent process as a random walk.

where is drawn from the prior in init and the increments come from the error model ϵ_t (a HierarchicalNormal by default, giving an inferred step standard deviation).

The init slot takes a raw prior: pass a bare Distribution, or a richer prior model. It is sampled through as_turing_submodel.

ϵ_t is a length-n PATH slot: a process gives time-varying increments, while a bare Distribution is auto-wrapped in an Intercept, giving a constant increment path (one shared draw broadcast to every step). Use IID for n independent increments.

Examples

@example
using ComposableTuringIDModels, Distributions
rw = RandomWalk()
mdl = as_turing_model(rw, 10)
rand(mdl)
source
ComposableTuringIDModels.RecordExpectedLatent Type

Record the inner latent vector as a tracked generated quantity (exp_latent).

Arguments

  • model: the inner latent model whose output is recorded.

  • n: the shape to generate — a length or an (n_strata, n_time) shape, whatever the inner model accepts.

Examples

@example
using ComposableTuringIDModels
rm = RecordExpectedLatent(FixedIntercept(0.1))
rand(as_turing_model(rm, 1))

The model slot is a length-n PATH slot: a bare Distribution there is auto-wrapped in an Intercept, giving a constant inner path; a process, an IID, or a vector passes through. Use IID for n independent draws. It is composed through as_turing_submodel.

Fields

  • model: the latent model whose expected latent vector is recorded.
source
ComposableTuringIDModels.RecordExpectedObs Type

Record the expected observations as a tracked generated quantity (exp_y_t).

The expected observations Y_t are tracked via the := syntax before the inner model is applied unchanged, so the expected observations are available in the returned chain alongside the inner model's variables.

Arguments

  • model: the RecordExpectedObs model.

  • y_t: the observed series (or missing when simulating predictively).

  • Y_t: the expected-observation series.

Examples

@example
using ComposableTuringIDModels
obs = RecordExpectedObs(PoissonError())
mdl = as_turing_model(obs, missing, fill(10.0, 5))
rand(mdl)

Fields

  • model: the inner observation model whose expected observations are recorded.
source
ComposableTuringIDModels.Renewal Type

Model unobserved infections via a time-varying renewal process driven by an internally generated (log) reproduction number.

where the latent model rt supplies the (log) reproduction number , is transformation, is the discrete generation interval, and the pre-window infections decay at the growth rate implied by . The process is generated inside the model, so as_turing_model takes a ModelShape n and returns the named tuple (; I_t, Z_t) with Z_t the (log) path.

Renewal is the one infection model that needs a generation interval, so it takes one directly through the generation_time keyword, which dispatches on the value: a discrete probability vector is used as-is, a continuous Distribution is discretised internally (see the constructor), and a pmf-producing prior model (e.g. an UncertainDelay) lets the generation interval itself be inferred — its distribution's parameters carry priors and the interval is rediscretised per draw through the as_turing_submodel seam.

Renewal is a step-composing helper: positional AbstractRenewalModifier arguments are composed onto the renewal RenewalStep. Passing a SusceptibleDepletion(N) gives a renewal process with a fixed population and susceptible depletion

Modifiers apply in the order given, and a modifier carrying priors (e.g. an ImportedCases importation rate) draws them once before the scan, so composing one takes no extra wiring: Renewal(gen_int, SusceptibleDepletion(N), ImportedCases(Normal(-1, 0.5))).

Strata and coupling

Handing rt a Stratify (or Replicate) and calling as_turing_model(renewal, (n_strata, n_time)) runs one renewal recursion per stratum. R_t is a n_strata × n_time matrix, each stratum has its own seed (from a vector-valued initialisation, or the same scalar seed broadcast to every stratum), and each stratum's incidence window advances on its own. The mixing keyword couples the strata by transforming the incidence window each stratum's force of infection is convolved against — see renewal_pressure for the extension point. It defaults to I (LinearAlgebra.I), leaving strata uncoupled: a Dims{2} shape with no mixing is n_strata independent single-series renewal processes sharing one model.

The generation interval, when inferred, is drawn at the shape the renewal needs: one pmf for n::Int, one pmf per stratum for n::Dims{2}. Because each parameter of an UncertainDelay is itself a prior slot, giving one a Hierarchy partially pools that parameter across strata. A partially pooled generation interval therefore needs no new component, e.g. UncertainDelay(LogNormal, [Hierarchy(), σ_prior]; D = 14.0) drawn at n_strata.

Fields

  • gen_int: the discrete generation interval vector (non-negative, sums to 1), or, for an inferred generation interval, the pmf-producing prior model.

  • transformation: the transformation between the unconstrained and constrained domains (default exp).

  • rt: the latent process model (an AbstractLatentModel) generating the (log) reproduction number. A length-n PATH slot: a bare Distribution here is auto-wrapped in an Intercept, giving a constant path (one shared draw broadcast to length n); use IID for n independent draws, or Stratify/Replicate for a strata axis.

  • initialisation: prior for the unconstrained initial infections (a Distribution or prior model, sampled through as_turing_submodel).

  • recurrent_step: the renewal accumulation step (an AbstractConstantRenewalStep), or nothing when the generation interval is inferred and the step is built per draw.

  • mixing: the coupling operator applied to the incidence window before R_t (default I, uncoupled); see renewal_pressure.

Constructor

  • Renewal(; generation_time, rt, initialisation, transformation = exp, mixing = I, D_gen = nothing, Δd = 1.0) — one keyword constructor that dispatches on generation_time:
    • a discrete probability vector (non-negative, sums to 1) is used directly as the generation interval;

    • a continuous Distribution is discretised via double-interval censoring (CensoredDistributions.jl), using D_gen/Δd, with the delay-0 bin dropped and the remainder renormalised; and

    • a pmf-producing prior model (an AbstractPriorModel such as an UncertainDelay) is held as-is and sampled per draw, giving an inferred generation interval (an uncertain discretised distribution used as the generation interval). Its fixed horizon keeps the interval length constant across draws; the lag-0 bin is dropped and the remainder renormalised per draw, exactly as for the fixed distribution.

Examples

A fixed generation interval:

@example
using ComposableTuringIDModels, Distributions
renewal = Renewal(; generation_time = [0.2, 0.3, 0.5], rt = RandomWalk(),
    initialisation = Normal())
rand(as_turing_model(renewal, 20))

An inferred generation interval — an uncertain discretised distribution used as the generation interval, whose LogNormal parameters carry priors:

@example
gen = UncertainDelay(
    LogNormal, [Normal(1.9, 0.2), truncated(Normal(0.5, 0.2), 0, Inf)]; D = 14.0)
renewal = Renewal(; generation_time = gen, rt = RandomWalk(),
    initialisation = Normal())
rand(as_turing_model(renewal, 20))

# With a fixed population and susceptible depletion.
depleting = Renewal([0.2, 0.3, 0.5], SusceptibleDepletion(1000.0);
    rt = RandomWalk(), initialisation = Normal())
rand(as_turing_model(depleting, 20))

A stratified renewal: one recursion per stratum, sharing a random walk in R_t-space with partially pooled per-stratum deviations, left uncoupled (mixing defaults to I):

@example
strat = Renewal(; generation_time = [0.2, 0.3, 0.5],
    rt = Stratify(RandomWalk(), Hierarchy()), initialisation = Normal())
size(as_turing_model(strat, (3, 20))().I_t)
source
ComposableTuringIDModels.RenewalStep Type

The renewal accumulation step: a force-of-infection core (a constant generation interval by default) with a tuple of modifiers (AbstractRenewalModifiers) composing on top, sharing one incidence window.

With no modifiers it is a plain renewal recurrence. With modifiers its state is (; val, window, substates) — the newest incidence, the shared window, and one substate per modifier; each step computes the core force of infection, threads it through the modifiers (each transforming the incidence and updating its own substate), then advances the shared window once with the final incidence.

RenewalStep(core, (SusceptibleDepletion(N),)) is a renewal process with a fixed population N and susceptible depletion; a Renewal built with a SusceptibleDepletion(N) modifier uses exactly this step.

source
ComposableTuringIDModels.RepeatBlock Type

Broadcast rule that repeats the latent process in blocks of length period (e.g. a piecewise-constant weekly process).

Examples

@example
using ComposableTuringIDModels
broadcast_rule(RepeatBlock(), [1, 2, 3, 4, 5], 10, 2)
source
ComposableTuringIDModels.RepeatEach Type

Broadcast rule that repeats the latent process at each position within a period (e.g. a fixed day-of-week effect).

Examples

@example
using ComposableTuringIDModels
broadcast_rule(RepeatEach(), [1, 2], 10, 2)
source
ComposableTuringIDModels.Replicate Type

Draw n_strata independent copies of a path model, one per stratum.

Built with as_turing_model(m, (n_strata, n_time)), Replicate draws model n_strata times over the time axis, each draw prefixed by its stratum index so the variables of stratum g never collide with stratum h, and stacks the results into a n_strata × n_time matrix. There is no cross-stratum relationship: every stratum's path is independent of every other's.

As with Stratify, the stratum count is not a field of the struct: it arrives through the shape argument, so one Replicate serves any panel width.

Two uses:

  • on its own, in a strata slot, e.g. rt = Replicate(RandomWalk()) gives n_strata fully independent R_t paths with no shared structure at all;

  • in Stratify's across slot, e.g. Stratify(RandomWalk(), Replicate(RandomWalk())) gives per-stratum deviations that vary in time rather than a single constant per-stratum offset — see across_shape, which Replicate overrides to draw the full (n_strata, n_time) shape.

The model slot takes a raw prior (a bare Distribution, or a latent/prior model), sampled through as_turing_submodel; a bare Distribution is auto-wrapped in an Intercept (a constant path), the same PATH convention used elsewhere (e.g. RandomWalk's ϵ_t).

Fields

  • model: the path model drawn independently for each stratum.

Examples

@example
using ComposableTuringIDModels, Distributions
# 3 fully independent random-walk paths over a 20-step time axis; the strata
# count arrives through the shape, not a field.
rep = Replicate(RandomWalk())
size(as_turing_model(rep, (3, 20))())
source
ComposableTuringIDModels.ReportTriangle Type

A 2D reporting-triangle observation model (epinowcast-style nowcasting), the joint counterpart to the RightTruncate marginal.

ReportTriangle consumes the expected eventual totals Y_t = μ_t (per reference day, the same quantity the infection pipeline produces) together with a reporting-delay PMF p drawn from a submodel, expands them to per-cell expected means μ_{t,d} = μ_t · p[d + 1], and scores only the observed cells of a ReportingTriangle (t + d ≤ now) under a per-cell count error model. The not-yet-reported cells are never sampled. Because the model's Y_t stays the eventual total, the nowcast of the eventual total is just Y_t read out as a generated quantity (and the completed triangle is μ_{t,d} over all d).

The delay PMF is supplied as a composable submodel delay_model — mirroring how RightTruncate takes a ReportingCDF — sampled with to_submodel(..., false) inside the model. The default ReportingPMF wraps a fixed PMF (the fixed-delay, independent-cell variant: each observed cell is an independent Poisson / negative-binomial draw about its mean, via the per-cell AbstractObservationErrorModel supplied as error_model). Because the delay is a submodel, an estimated / time-varying delay — and the multinomial-split parameterisation — are the seams this grows from (a later phase of the nowcasting design).

The default PMF is built with the same released-CD double_interval_censored + pdf discretisation path that LatentDelay uses, so the triangle's per-cell means and the right-truncation nowcast share one delay kernel.

Constructors

  • ReportTriangle(error_model, delay_model) — from a delay submodel producing the PMF (e.g. a ReportingPMF); Dmax is read from it.

  • ReportTriangle(error_model, pmf::AbstractVector) — wrap a precomputed delay PMF (non-negative, sums to 1) in the default ReportingPMF.

  • ReportTriangle(error_model, distribution; D, Δd) — discretise a continuous reporting-delay distribution via double-interval censoring (CensoredDistributions.jl) into a ReportingPMF, exactly as LatentDelay.

The y_t data contract

The observation data is a ReportingTriangle, built through the shared define_y_t hook from either a matrix or a long-form table:

julia
y_t = define_y_t(obs, N, Y_t)                       # from a count matrix
y_t = define_y_t(obs, reports, Y_t; now = now)      # from (reference, delay, count) rows

Pass y_t = missing to simulate: ReportTriangle builds a fully observed triangle (now = n + Dmax) of missing cells and fills them predictively.

Fields

  • error_model: the per-cell count-error model (e.g. PoissonError, NegativeBinomialError).

  • delay_model: the delay submodel producing the reporting-delay PMF p (delays 0 … Dmax); cell (t, d) has expected mean Y_t[t] · p[d + 1]. The default ReportingPMF holds a fixed PMF.

Examples

@example
using ComposableTuringIDModels, Distributions
obs = ReportTriangle(PoissonError(), truncated(Normal(2.0, 1.0), 0.0, Inf))
# Simulate a triangle for 15 reference days of expected total 50.
sim = as_turing_model(obs, missing, fill(50.0, 15))()
sim.observed
source
ComposableTuringIDModels.ReportingCDF Type

A composable reporting-completeness component: a Distribution (or a precomputed vector) turned into the cumulative reporting proportion by age, for use as the correction submodel of RightTruncate.

Given a series length n, sampling as_turing_model(c::ReportingCDF, n) returns a length-n vector F where F[a + 1] is the fraction of a reference day's eventual total reported within a days (a = 0, 1, …, n - 1), in [0, 1]. Reference days older than the reporting delay's support are fully reported (F = 1), so the vector is padded with ones to length n. The CDF built from a delay distribution is non-decreasing, but ReportingCDF does not require monotonicity — a precomputed curve may be non-monotonic, so an over-/under-reporting correction that recovers can be expressed.

It is the fixed-delay default used by RightTruncate: the completeness is precomputed once and held constant. Because the correction is supplied to RightTruncate as a submodel, a user can instead pass any latent component that produces a length-n completeness curve — a flexible non-parametric CDF, or even a non-monotonic correction — without changing RightTruncate.

Constructors

  • ReportingCDF(distribution; D, Δd) — discretise a continuous reporting-delay distribution via double-interval censoring (CensoredDistributions.jl) and take the cumulative sum of the resulting PMF, exactly the released-CD path LatentDelay uses.

  • ReportingCDF(cdf) — from a precomputed completeness vector by age (in [0, 1]; need not be monotonic).

Examples

@example
using ComposableTuringIDModels, Distributions
c = ReportingCDF(truncated(Normal(5.0, 2.0), 0.0, Inf))
as_turing_model(c, 10)()

Fields

  • cdf: the reporting-completeness curve by age (cdf[a + 1]), in [0, 1] (need not be monotonic). Padded with ones up to the requested length when shorter.
source
ComposableTuringIDModels.ReportingPMF Type

A composable reporting-delay PMF component: a Distribution (or a precomputed vector) turned into the reporting-delay PMF p (delays 0 … Dmax), for use as the delay submodel of ReportTriangle.

Sampling as_turing_model(c::ReportingPMF, n) returns the length-(Dmax + 1) PMF (the argument n is the role-interface series length and is ignored — the PMF is indexed by delay, not reference day). The PMF is non-negative and sums to one; Dmax = length(pmf) - 1.

It is the fixed-delay default used by ReportTriangle: the PMF is precomputed once and held constant. Because the delay is supplied to ReportTriangle as a submodel — mirroring how RightTruncate takes a ReportingCDF — a user can instead pass any latent component producing the delay PMF, the seam an estimated / time-varying delay grows from.

Constructors

  • ReportingPMF(distribution; D, Δd) — discretise a continuous reporting-delay distribution via double-interval censoring (CensoredDistributions.jl), exactly the released-CD path LatentDelay uses.

  • ReportingPMF(pmf) — from a precomputed delay PMF (non-negative, sums to 1).

Examples

@example
using ComposableTuringIDModels, Distributions
c = ReportingPMF(truncated(Normal(2.0, 1.0), 0.0, Inf))
as_turing_model(c, 10)()

Fields

  • pmf: the reporting-delay PMF p (delays 0 … Dmax, non-negative, sums to 1); Dmax = length(pmf) - 1.
source
ComposableTuringIDModels.ReportingTriangle Type

A reporting triangle: the reference-date × reporting-delay count matrix with the not-yet-reported cells masked off.

counts[t, d + 1] is the number of events with reference day t first reported at delay d = 0, 1, …, Dmax. A cell is observed at the present time now iff t + d ≤ now; the remaining lower-right cells have not yet been reported and are masked out by observed. This is the native object of epinowcast-style nowcasting: it keeps the full joint reference-day × delay structure rather than collapsing it to a per-reference-day observed-so-far total (the marginal that RightTruncate conditions on — see the consistency note below).

Build one with define_y_t from either a dense matrix or a long-form table of (reference, delay, count) rows; pass it as the y_t data to a ReportTriangle observation model.

Consistency with right-truncation (CDF-scaling)

Summing the observed cells of reference day t (delays d = 0 … now − t) gives Σ_d counts[t, d+1], the observed-so-far total — which in expectation is μ_t · F[(now − t) + 1], exactly the CDF-scaled expected observed-so-far that RightTruncate conditions on. So the observed row-sums of the triangle are the marginal of the joint model; the triangle additionally models how that total is split across delays.

Fields

  • counts: the N[t, d+1] count matrix (reference day × delay). Unobserved cells are ignored by ReportTriangle; they may hold missing, 0, or any placeholder.

  • observed: the boolean mask of reported cells (t + d ≤ now).

  • Dmax: the maximum reporting delay (the number of delay columns is Dmax + 1, delays 0 … Dmax).

Examples

@example
using ComposableTuringIDModels
# A 4 × 3 matrix (reference days 1..4, delays 0..2); now = 4.
N = [10 5 2; 12 6 3; 14 7 4; 16 8 5]
rt = define_y_t(ReportTriangle(PoissonError(), [0.5, 0.3, 0.2]), N, fill(20.0, 4))
rt.observed
source
ComposableTuringIDModels.RightTruncate Type

Correct an underlying observation model for right-truncation (not-yet-reported counts), the EpiNow2-style CDF-scaling nowcast.

The infection → expected-observation pipeline produces  , the expected eventual total for reference day t. At the present time now (taken to be the last reference day, now = n) a reference day of age    has only had a fraction   of its eventual total reported, where is the reporting-completeness CDF. The expected observed-so-far is therefore   , and conditioning the inner observation error on that — rather than on the full — corrects the right-truncation. Recent, still-maturing reference days are automatically down-weighted, while the model's Y_t remains the eventual total (so the nowcast of the eventual total is just Y_t read out as a generated quantity).

The modifier mirrors Ascertainment: it wraps an inner observation model, draws a length-n correction series from a submodel, transforms the expected-observation vector, and delegates via to_submodel(..., false). The correction cdf_model is any component producing a length-n completeness curve F (by age). The default is a fixed ReportingCDF built from a reporting delay distribution (the released-CD case), but because the correction is a submodel a user can supply a flexible non-parametric CDF — or even a non-monotonic correction — without changing RightTruncate.

The completeness is indexed by age: the most recent reference day (t = n, age 0) is scaled by F[1] and the oldest (t = 1, age n - 1) by F[n], so the age-indexed series is reversed onto the reference-day axis. A fully-reported correction (all ones) leaves the inner model unchanged.

This is the fixed-delay variant. An estimated / time-varying delay (a latent correction with sampled parameters) is a planned follow-up, and is exactly the submodel slot generalising to it.

Constructors

  • RightTruncate(model, cdf_model) — from an inner observation model and a correction component (a latent-role submodel producing the length-n completeness curve, e.g. a ReportingCDF).

  • RightTruncate(model, distribution; D, Δd) — wrap a continuous reporting-delay distribution in the default ReportingCDF (the released-CD path).

  • RightTruncate(model, cdf::AbstractVector) — wrap a precomputed completeness vector in a fixed ReportingCDF.

Arguments

  • obs_model: the RightTruncate model.

  • y_t: the observed-so-far series (or missing when simulating predictively).

  • Y_t: the expected eventual-total series.

Examples

@example
using ComposableTuringIDModels, Distributions
obs = RightTruncate(NegativeBinomialError(), truncated(Normal(5.0, 2.0), 0.0, Inf))
mdl = as_turing_model(obs, missing, fill(100.0, 30))
rand(mdl)

Fields

  • model: the inner observation-error model the corrected expected observations are passed to.

  • cdf_model: the correction submodel producing the length-n reporting completeness curve (by age).

source
ComposableTuringIDModels.SEIRParams Type

SEIR compartmental model parameters and priors, usable as the latent component of an ODEProcess.

The sampled initial infected proportion is split between the exposed and infectious compartments using the constant-incidence equilibrium proportions   and  .

Declarative alternative

This model's vector field and Jacobian are hand-coded. To build a new or custom compartmental network without hand-deriving a Jacobian, use CatalystODEParams: it reads any Catalyst reaction network and generates the ODE system and Jacobian symbolically (an opt-in extension).

Arguments

  • params: the SEIRParams struct.

  • n: unused Int size argument; accepted for the common as_turing_model signature.

Keyword Arguments

  • tspan: the ODE solution time span.

  • infectiousness: prior for .

  • incubation_rate: prior for .

  • recovery_rate: prior for .

  • initial_prop_infected: prior for the initial infected proportion.

Examples

@example
using ComposableTuringIDModels, OrdinaryDiffEq, Distributions
seirparams = SEIRParams(
    tspan = (0.0, 30.0),
    infectiousness = LogNormal(log(0.3), 0.05),
    incubation_rate = LogNormal(log(0.1), 0.05),
    recovery_rate = LogNormal(log(0.1), 0.05),
    initial_prop_infected = Beta(1, 99))
rand(as_turing_model(seirparams, 0))

Fields

  • prob: the ODEProblem instance for the SEIR model.

  • infectiousness: prior for .

  • incubation_rate: prior for .

  • recovery_rate: prior for .

  • initial_prop_infected: prior for the initial infected proportion.

source
ComposableTuringIDModels.SIRParams Type

SIR compartmental model parameters and priors, usable as the latent component of an ODEProcess.

Declarative alternative

This model's vector field and Jacobian are hand-coded. To build a new or custom compartmental network without hand-deriving a Jacobian, use CatalystODEParams: it reads any Catalyst reaction network and generates the ODE system and Jacobian symbolically (an opt-in extension).

Arguments

  • params: the SIRParams struct.

  • n: unused Int size argument (the ODE dimension is fixed); accepted for the common as_turing_model signature.

Keyword Arguments

  • tspan: the ODE solution time span.

  • infectiousness: prior for .

  • recovery_rate: prior for .

  • initial_prop_infected: prior for the initial infected proportion.

Examples

@example
using ComposableTuringIDModels, OrdinaryDiffEq, Distributions
sirparams = SIRParams(
    tspan = (0.0, 30.0),
    infectiousness = LogNormal(log(0.3), 0.05),
    recovery_rate = LogNormal(log(0.1), 0.05),
    initial_prop_infected = Beta(1, 99))
rand(as_turing_model(sirparams, 0))

Fields

  • prob: the ODEProblem instance for the SIR model.

  • infectiousness: prior for .

  • recovery_rate: prior for .

  • initial_prop_infected: prior for the initial infected proportion.

source
ComposableTuringIDModels.SafeNegativeBinomial Type

A negative binomial distribution parameterised by (r, p) that avoids InexactError at very large means.

The package uses a mean/cluster-factor parameterisation when constructing this distribution from an expected count (see NegativeBinomialMeanClust).

Construction does not validate r/p, since these may transiently be out-of-domain values on the automatic-differentiation path (e.g. built from a sampled cluster factor while logpdf is evaluated). rand validates instead, raising a DomainError naming the invalid parameter rather than the opaque sqrt/Gamma error a bad value would otherwise surface as.

Examples

julia
using ComposableTuringIDModels, Distributions
bigμ = exp(48.0)
σ² = bigμ + 0.05 * bigμ^2
p = bigμ / σ²
r = bigμ * p / (1 - p)
d = SafeNegativeBinomial(r, p)
logpdf(d, 100)
nothing
# output
source
ComposableTuringIDModels.SafePoisson Type

A Poisson distribution parameterised by its mean λ that avoids InexactError for very large means.

Construction does not validate λ, since it may transiently be an out-of-domain value on the automatic-differentiation path. rand validates instead, raising a DomainError naming λ.

Examples

julia
using ComposableTuringIDModels, Distributions
d = SafePoisson(exp(48.0))
logpdf(d, 100)
nothing
# output
source
ComposableTuringIDModels.Split Type

Split one expected series into several named observation streams — the single observation-composition construct for parallel, cascade, and data-driven strata composition.

Each stream is a full AbstractObservationModel (a bare error family or a delay / ascertainment / truncation pipeline). Split feeds every stream the expected series arriving at the point where it sits in the pipeline and automatically prefixes each stream's sampled variables with its name (via DynamicPPL.prefix), so no manual prefix layer is needed. The uniform (; y_t, expected) return contract exposes each stream's pre-error expected series so streams can thread on one another.

Composition by placement

Split is itself an observation model consuming an expected series, so where it sits chooses the composition:

  • Parallel — placed high, on infections: every stream observes the same (cases and deaths each a delayed, ascertained fraction of the same infections). IDModel(inf, Split((cases = …, deaths = …))).

  • Cascade — placed low, inside a stream's pipeline: the shared upstream layers run first and Split branches on their expected output, so a later stream is observed downstream of an earlier one. For deaths as a delayed fraction of the expected reported cases, share the case delay then split: LatentDelay(Split((cases = leaf, deaths = pipeline)), case_delay).

  • Data-driven strata — built from a single template model (or a set of named streams) plus a weight map: the infection→observation cardinality is entirely down to map, covering the same range of cardinalities as above. See StrataMap for the underlying projection.

The threaded quantity is always a stream's expected (pre-error) series, never its realised noisy draw; observing a downstream stream off another's sampled counts is out of scope.

Data contract

y_t is a NamedTuple of observed series keyed by stream name (or missing to simulate). The return value is (; y_t, expected), each a NamedTuple of per-stream series. When Split is nested inside another modifier the incoming missing reaches it as a shared placeholder; the explicit stream names let it still fan out.

In the data-driven strata mode (m.names === nothing), y_t may instead be an AbstractMatrix (one stream per row, "group1", "group2", … as the generated names) — the data contract a multi-stratum IDModel using CombineInfections on the infection side uses.

The incoming expected series may be a single vector (broadcast to every stream), a per-stream NamedTuple, an inf_strata × time matrix (one stream per row), or a StrataMap.

Constructors

  • Split(streams::NamedTuple) — explicit named streams, one-to-one with the incoming expected series.

  • Split(streams::NamedTuple, map::AbstractMatrix) — explicit named streams fed by an obs_strata × inf_strata weight map, so named streams can still express a many-to-one, many-to-many, or finer/coarser mapping.

  • Split(template::AbstractObservationModel) — a data-driven strata split replicating the template once per y_t entry.

  • Split(template::AbstractObservationModel, map::AbstractMatrix) — a strata split that projects the incoming expected series (an inf_strata × time matrix, or a single vector treated as one infection stratum) onto the observation streams through the obs_strata × inf_strata weight map. This is the composed-model form of a StrataMap: the strata come from the infection process at run time, so IDModel(infection, Split(template, map)) maps infections onto streams end-to-end.

Examples

@example
using ComposableTuringIDModels, Distributions
# Parallel: cases and deaths, each a delayed fraction of the SAME infections.
parallel = Split((
    cases = LatentDelay(NegativeBinomialError(), [0.4, 0.3, 0.2, 0.1]),
    deaths = LatentDelay(NegativeBinomialError(), [0.1, 0.2, 0.3, 0.4])))
rand(as_turing_model(parallel, (cases = missing, deaths = missing), fill(100.0, 12)))

# Cascade: share the case delay, then split so deaths sit downstream of cases.
cascade = LatentDelay(
    Split((
        cases = PoissonError(),
        deaths = LatentDelay(
            Ascertainment(PoissonError(), FixedIntercept(log(0.1))),
            [0.2, 0.3, 0.5]))),
    [0.5, 0.3, 0.2])
rand(as_turing_model(cascade, (cases = missing, deaths = missing), fill(100.0, 12)))

# Many-to-one: three age strata of infections observed as one hospitalisation
# stream, via a single aggregation row.
hospitalisations = Split((hosp = NegativeBinomialError(),), [1.0 1.0 1.0])
I_t = [10.0 10.0 10.0; 20.0 20.0 20.0; 5.0 5.0 5.0]   # 3 strata, 3 time steps
rand(as_turing_model(hospitalisations, (hosp = missing,), I_t))

Fields

  • streams: a NamedTuple of per-stream models, or a single strata template.

  • names: the stream names, or nothing in strata mode (names come from data).

  • map: an obs_strata × inf_strata weight matrix projecting infection strata onto streams, or nothing when the incoming expected series is used directly.

source
KernelFunctions.SqExponentialKernel Type
julia
SqExponentialKernel(; metric=Euclidean())

Squared exponential kernel with respect to the metric.

Definition

For inputs and metric   , the squared exponential kernel is defined as

By default, is the Euclidean metric   .

See also: GammaExponentialKernel

source
ComposableTuringIDModels.StrataMap Type

A strata mapping supplied as the expected series to a Split: project a multi-stratum expected series onto observation streams through a (possibly weighted) linear map.

strata is an inf_strata × time matrix of per-infection-stratum expected series; map is an obs_strata × inf_strata weight matrix. Stream k receives   , so map is the one mechanism for every infection→observation cardinality:

  • one-to-onemap = I(n), or no map at all.

  • many-to-one — one aggregation row, e.g. [1.0 1.0 1.0] summing three infection strata into a single observation stream.

  • many-to-many — a general weight matrix.

  • finer or coarser than the infection process — a non-square map, e.g. splitting one infection stratum across two reporting streams with weights that sum to 1.

The stream count is size(map, 1), read from the data at model-build time.

Fields

  • strata: the inf_strata × time expected series (per infection stratum).

  • map: the obs_strata × inf_strata weight matrix.

Examples

@example
using ComposableTuringIDModels
M = [10.0 10.0 10.0 10.0 10.0; 4.0 4.0 4.0 4.0 4.0]
W = [1.0 0.0; 0.0 1.0; 1.0 1.0]      # stratum 1, stratum 2, and their sum
sm = StrataMap(M, W)
size(sm.map, 1)                      # 3 observation streams
source
ComposableTuringIDModels.Stratify Type

Add a strata axis to a shared latent path.

Stratify(shared, across) draws shared once over the time axis and across over the strata axis, then broadcasts the two together with combine into a n_strata × n_time matrix,

with the shared draw and the across draw. Built with as_turing_model(m, (n_strata, n_time)), so the stratum count is not a field of the struct: it arrives through the shape argument, exactly the way a series length n is passed to a plain path model. One Stratify therefore serves any panel width.

across is drawn at across_shape(m.across, n), which is n_strata by default: across is a length-n_strata vector, and combine broadcasts it against a 1 × n_time row, (g,) .+ (1, t) ⇒ (g, t) — a constant offset per stratum. An across model that itself spans both axes (e.g. Replicate) overrides across_shape to draw the full (n_strata, n_time) matrix instead, and the same broadcast, (g, t) .+ (1, t) ⇒ (g, t), now gives a time-varying deviation per stratum. One combine line therefore serves both a constant per-stratum offset and a time-varying one, with no branch on which across was supplied.

The across slot is where the pooling choice lives:

  • Hierarchy partially pools the per-stratum deviations towards a shared level;

  • IID draws each deviation independently, giving no pooling;

  • FixedIntercept(0.0) fixes every deviation to zero, giving full pooling — every stratum then shares shared exactly;

  • a RandomWalk correlates neighbouring strata, useful when the strata are ordered (e.g. adjacent age bands).

Both slots take a raw prior (a bare Distribution, or a latent/prior model), sampled through as_turing_submodel; a bare Distribution is auto-wrapped in an Intercept (a constant path), the same PATH convention used elsewhere (e.g. RandomWalk's ϵ_t).

Fields

  • shared: the path drawn once over the time axis.

  • across: the cross-stratum relationship generating the per-stratum offsets (or deviations, for an across that spans both axes).

  • combine: how across and shared are broadcast together (default +, a multiplicative effect on a log scale).

Examples

@example
using ComposableTuringIDModels, Distributions
# A shared random walk in R_t-space with partially pooled per-stratum
# deviations; the strata count (3) arrives through the shape, not a field.
strat = Stratify(RandomWalk(), Hierarchy())
size(as_turing_model(strat, (3, 20))())
source
ComposableTuringIDModels.SusceptibleDepletion Type

Susceptible-depletion modifier for RenewalStep.

Scales the proposed incidence by the available susceptible fraction and depletes the susceptible pool,

with population size = pop_size. Its substate is the current susceptible count . Adding it to a renewal step gives a renewal process with a fixed population and susceptible depletion, e.g. Renewal(gen_int, SusceptibleDepletion(N)).

pop_size is a scalar for one series, and a per-stratum vector for a stratified renewal. Each stratum then depletes its own pool. A scalar given to a stratified renewal is shared by every stratum, so each depletes a separate pool of the same size.

It samples nothing, so it is a plain scan modifier: the pre-scan seam (see AbstractRenewalModifier) returns it unchanged.

Fields

  • pop_size: the population size, one value or one per stratum.
source
ComposableTuringIDModels.TransformLatentModel Type

Apply a transformation function to the output of an inner latent model.

Arguments

  • model: the inner latent model whose output is transformed.

  • n: the shape to generate — a length or an (n_strata, n_time) shape, whatever the inner model accepts.

Examples

@example
using ComposableTuringIDModels, Distributions
trans = TransformLatentModel(Intercept(Normal(2, 0.2)), x -> exp.(x))
rand(as_turing_model(trans, 5))

The model slot is a length-n PATH slot: a bare Distribution there is auto-wrapped in an Intercept, giving a constant inner path; a process, an IID, or a vector passes through. Use IID for n independent draws. It is composed through as_turing_submodel.

Fields

  • model: the latent model to transform.

  • transform: the transformation function applied to the latent vector.

source
ComposableTuringIDModels.TransformObservationModel Type

Apply a transformation function to the expected observations before passing them to an inner observation model.

The expected observations Y_t are mapped through transform and the result is passed to the inner model. The default transform applies a softplus (x -> log1pexp.(x)), keeping the transformed expected observations positive.

Arguments

  • obs: the TransformObservationModel.

  • y_t: the observed series (or missing when simulating predictively).

  • Y_t: the expected-observation series.

Examples

@example
using ComposableTuringIDModels
obs = TransformObservationModel(PoissonError(), x -> x .* 2)
mdl = as_turing_model(obs, missing, fill(10.0, 5))
rand(mdl)

Fields

  • model: the inner observation model the transformed expected observations are passed to.

  • transform: the transformation applied to the expected observations.

source
ComposableTuringIDModels.UncertainDelay Type

A delay whose distribution's parameters are prior slots, so the reporting delay is inferred rather than fixed.

UncertainDelay(family, params; D, Δd) describes a continuous delay distribution family(θ...) whose positional parameters θ are drawn from the priors in params (one prior per parameter). Each prior may be a bare Distribution (the parameter is constant — uncertain but time-invariant) or a process (an AbstractPriorModel, e.g. a RandomWalk, so the parameter is time-varying), through the same constant-vs-process seam every other component uses. Each draw builds the right-truncated, double-interval-censored delay PMF (the same _discretised_pmf path the fixed delay uses):

  • if every parameter is a Distribution, one time-invariant PMF is built and as_turing_model(u::UncertainDelay) returns it (drawing the parameters as one slot through the as_turing_submodel seam); and

  • if any parameter is a process, as_turing_model(u::UncertainDelay, n) builds one PMF per time point — each parameter is read at time t via at (a constant stays constant, a process path is indexed), so the delay, and its discretised PMF, varies with time. A time-varying delay needs a series length, so the no-n method raises an error.

The truncation horizon D is required and fixed: it holds the PMF length constant across draws and across time points (only the parameters vary), which the convolution relies on. Δd is the discretisation bin width.

Constructor

  • UncertainDelay(family, params; D, Δd = 1.0)family is a distribution constructor (e.g. LogNormal, Gamma) called as family(θ...), and params is a vector of priors (a Distribution or a process), one per positional parameter of family.

Fields

  • params: the priors for the delay distribution's positional parameters.

  • family: the distribution constructor built from the sampled parameters.

  • D: the fixed right-truncation horizon (keeps the PMF length constant).

  • Δd: the discretisation bin width.

Examples

An uncertain (time-invariant) delay whose LogNormal parameters carry priors:

@example
using ComposableTuringIDModels, Distributions
delay = UncertainDelay(
    LogNormal, [Normal(1.5, 0.4), truncated(Normal(0.4, 0.2), 0, Inf)]; D = 20.0)
obs = LatentDelay(NegativeBinomialError(), delay)
mdl = as_turing_model(obs, missing, fill(100.0, 40))
rand(mdl)

A time-varying delay: the LogNormal meanlog is a RandomWalk (it drifts with time) while the sdlog carries a constant prior:

@example
tv = UncertainDelay(
    LogNormal, [RandomWalk(), truncated(Normal(0.4, 0.2), 0, Inf)]; D = 20.0)
tv_obs = LatentDelay(NegativeBinomialError(), tv)
as_turing_model(tv_obs, missing, fill(100.0, 40))().y_t
source
ComposableTuringIDModels.accumulate_scan Function

Apply an AbstractAccumulationStep across an input sequence in a single pass.

This is an optimised accumulate-based replacement for an explicit for loop. acc_step is a callable step (state, ϵ) -> new_state, initial_state seeds the scan, and ϵ_t is the driving sequence. The returned value is assembled by get_state from the accumulated states.

Arguments

  • acc_step: an AbstractAccumulationStep, a callable (state, ϵ) -> new_state applied at each element of the sequence.

  • initial_state: the seed state passed to accumulate as init.

  • ϵ_t: the driving sequence accumulated over.

Examples

@example
using ComposableTuringIDModels
accumulate_scan(ComposableTuringIDModels.RWStep(), 0.0, [1.0, 2.0, 3.0])
source
ComposableTuringIDModels.across_shape Function

The shape Stratify draws its across slot at.

The default method returns n[1], the stratum count alone, so across draws one value per stratum: a per-stratum offset, constant over time. An across model that spans both axes (e.g. Replicate) overrides this method to return the full shape n, so it draws a (n_strata, n_time) matrix instead — a per-stratum deviation that varies over time. This is the extension point: a new across model that needs a shape other than a bare stratum count adds a method here.

Arguments

  • across: the model in the across slot.

  • n: the (n_strata, n_time) shape the Stratify is being built at.

Examples

@example
using ComposableTuringIDModels
ComposableTuringIDModels.across_shape(Hierarchy(), (3, 20))
source
ComposableTuringIDModels.apply_method Function

Condition a model by fixing some parameters and conditioning on others, then run an inference method.

Arguments

  • idproblem: the IDProblem (or a DynamicPPL.Model).

  • method: the inference method (a sampler, e.g. NUTSampler).

  • data: the data to condition on (with a y_t field).

Keyword Arguments

  • fix_parameters: a NamedTuple of parameters to fix.

  • condition_parameters: a NamedTuple of parameters to condition on.

  • kwargs...: forwarded to the inference method.

Examples

@example
using ComposableTuringIDModels, Distributions
problem = IDProblem(
    infection = DirectInfections(; Z = RandomWalk(), initialisation = Normal()),
    observation_model = PoissonError(),
    tspan = (1, 20))
y = rand(as_turing_model(problem, (; y_t = missing)))
nothing
source
ComposableTuringIDModels.arima Function

Build an ARIMA(p, d, q) latent process: an arma wrapped in a d-fold DiffLatentModel.

Arguments

  • ar_init: prior(s) for the AR initial conditions.

  • diff_init: prior(s) for the differencing initial conditions (sets d).

  • damp: prior(s) for the AR damping coefficients.

  • θ: prior(s) for the MA coefficients.

  • ϵ_t: the innovation model (default HierarchicalNormal).

Examples

@example
using ComposableTuringIDModels, Distributions
model = arima()
rand(as_turing_model(model, 10))
source
ComposableTuringIDModels.arma Function

Build an ARMA(p, q) latent process: an AR whose innovation model is an MA.

Arguments

  • init: prior(s) for the AR initial conditions.

  • damp: prior(s) for the AR damping coefficients.

  • θ: prior(s) for the MA coefficients.

  • ϵ_t: the innovation model (default HierarchicalNormal).

Examples

@example
using ComposableTuringIDModels, Distributions
model = arma(; θ = [truncated(Normal(0.0, 0.02), -1, 1)],
    damp = [truncated(Normal(0.0, 0.02), 0, 1)])
rand(as_turing_model(model, 10))
source
ComposableTuringIDModels.as_turing_model Function

Construct a DynamicPPL.Model from an ComposableTuringIDModels model component.

as_turing_model is the single generic entry point of the package. Every concrete model struct implements exactly one

julia
@model function as_turing_model(m::MyModel, args...; kwargs...)
    ...
end

method, and components are composed by sampling submodels of one another through the as_turing_submodel seam:

julia
z ~ as_turing_submodel(inner_model, n)

as_turing_submodel disables automatic variable prefixing by default so that parameter names stay flat unless prefixing is explicitly requested (prefix = true).

The fallback method below errors with a clear message when a struct does not yet implement as_turing_model, which keeps the public surface honest.

Arguments

  • model: an ComposableTuringIDModels model component (a subtype of AbstractComposableModel).

  • args...: positional arguments forwarded to the component's method, such as the series length n (latent models) or the expected/observed series (infection and observation models).

  • kwargs...: keyword arguments forwarded to the component's method.

Examples

julia
using ComposableTuringIDModels, Distributions
turing_model = as_turing_model(RandomWalk(), 10)
rand(turing_model)
source

A path model has no strata axis of its own; ask for one explicitly rather than guessing whether a shared path was meant to be pooled across strata.

Every AbstractPriorModel speaks the length-n path contract (as_turing_model(m, n::Int)). This guard rejects a Dims{2} shape for any model that has not opted into a strata axis, so a stratum-shaped call to a plain path model fails at the point it is made rather than returning something silently wrong. Wrap the model in a Stratify (or a Replicate) to give it one.

Examples

@example
using ComposableTuringIDModels, Distributions
try
    as_turing_model(RandomWalk(), (3, 10))
catch e
    e
end
source

Sample a raw prior Distribution as a single scalar RV.

Giving as_turing_model a Distribution method lets a bare distribution flow through as_turing_submodel exactly like a full model, so a component's parameter slot samples θ ~ as_turing_submodel(model.slot, n) uniformly whether the slot holds a bare distribution or a process. A bare distribution draws ONE scalar value (a constant, no length-n allocation) whatever shape it is asked for — n is ignored, whether it is a length or an (n_strata, n_time) shape. A component then reads a possibly-time-varying parameter per step with at, so the scalar stays constant while a process-valued slot varies — this is the single seam behind AR's optionally-time-varying damping and the other per-step parameters.

For n independent draws (a white-noise process) use the explicit IID component; for a single shared value broadcast to length n use Intercept; for per-element priors use a Vector{<:Distribution}.

Arguments

  • prior: the prior distribution.

  • n: accepted for a uniform seam signature; ignored (the draw is scalar whatever shape is asked for).

Examples

@example
using ComposableTuringIDModels, Distributions
as_turing_model(Normal(), 3)()   # a single scalar draw
source

Sample a vector of prior Distributions as a length-n prior submodel, one independent draw per element.

The length is fixed by the vector, so n must match it. This is the explicit way a prior slot asks for n independent draws with per-element priors (e.g. an AR's per-lag damping coefficients).

Arguments

  • prior: the vector of prior distributions.

  • n: the required length (must equal length(prior)).

Examples

@example
using ComposableTuringIDModels, Distributions
as_turing_model([Normal(0, 1), Normal(5, 0.1)], 2)()
source

Resolve an accumulation step ahead of the scan, sampling any parameters its parts carry.

The default method samples nothing and returns the step unchanged. A RenewalStep resolves both its core and its AbstractRenewalModifiers through their own as_turing_model methods and rebuilds itself from the resolved parts. Renewal draws its step through this one seam, so neither a modifier with priors nor a drawn coupling operator (a MixingStep) needs special handling in the infection model.

Each modifier is prefixed by its position in the tuple, so the th modifier's variables are namespaced modifier_<i> (see AbstractRenewalModifier).

Arguments

  • step: the accumulation step to resolve.

  • n: the series length the scan will run over.

source

Draw the coupling operator ahead of the scan.

Samples the AbstractMixingModel's parameters through as_turing_submodel and returns the ConstantRenewalStep the scan uses, carrying the realised operator. The operator's variables are namespaced under core.mixing, so a fixed and an inferred coupling differ in the chain and nowhere else.

Arguments

  • step: the MixingStep to resolve.

  • n: the shape the scan will run over.

source

Sample the importation rate ahead of the scan.

Draws the unconstrained rate slot through as_turing_submodel — a bare Distribution giving one constant rate, a process giving a length-n path — maps it onto the positive scale with the modifier's transformation, and returns the ImportedRate the scan uses. The map is broadcast, so a constant stays a scalar (no length-n allocation) and the scan reads either shape with at.

Arguments

  • mod: the ImportedCases modifier.

  • n: the length of the renewal series.

source

Generate observations from an observation-error model.

Supports missing observations (y_t === missing, simulating predictively) and expected-observation vectors Y_t shorter than y_t (the expected values are aligned to the last length(Y_t) entries). Expected values are nudged by a tiny constant to avoid degenerate error distributions.

The error family supplies generate_observation_error_priors (sampled as a submodel) and observation_error (the per-time-point distribution).

Returns the uniform (; y_t, expected) tuple: y_t is the observed (or simulated) counts and expected is the pre-error series. Exposing expected lets a Split thread one stream's expectation into another.

source

Sample a constant-parameter UncertainDelay over an axis, returning the same pmf at every point.

Every parameter is a Distribution (uncertain but constant), so one pmf is drawn — through the no-n method above — and copied across the axis. This is what lets a stratified renewal's generation-interval slot take a fully constant UncertainDelay: drawn at n_strata, every stratum gets the same (uncertain) interval, with no new component. Give one of the parameters a Hierarchy instead for a partially pooled per-stratum interval — that makes the delay time-varying (see the method below) rather than needing this one.

source

Sample a time-varying UncertainDelay as a length-n sequence of delay pmfs.

Each parameter is drawn through the as_turing_submodel seam: a Distribution parameter draws a scalar (constant across time), while a process parameter (an AbstractPriorModel) draws a length-n path. The pmf at time t is built from each parameter read at t via at, so the delay distribution — and its discretised pmf — varies with time. The fixed horizon D keeps every pmf the same length.

source

Convenience 2-argument form: read the infection process's shape from the data.

The observation model and the data together fix the shape of the infection process, so nothing about it needs to be supplied explicitly or stored on the model. as_turing_model(model, Y) is as_turing_model(model, Y, shape) with shape resolved via infection_strata: the number of infection strata the observation model consumes given the data's row count, paired with the data's time length.

Three age strata observed as one hospitalisation stream is Split(NegativeBinomialError(), [1.0 1.0 1.0]); a 1 x T data matrix then builds a 3-stratum infection process.

Examples

@example
using ComposableTuringIDModels, Distributions
model = IDModel(
    DirectInfections(;
        Z = Stratify(RandomWalk(), Hierarchy(; across = IID(Normal(0, 0.5)))),
        initialisation = Normal(log(50), 0.2)),
    Split(PoissonError(), [1.0 1.0 1.0]))
# One observation stream, 12 time steps: the three infection strata come
# from the weight matrix, not from the data.
Ymiss = Matrix{Union{Missing, Float64}}(missing, 1, 12)
sim = as_turing_model(model, Ymiss)()
size(sim.I_t)
source
ComposableTuringIDModels.as_turing_submodel Function

Compose a component as a Turing submodel: to_submodel(as_turing_model(m, args...), prefix).

This is the single public composition seam of the package. Every composition point — a manipulator wrapping an inner model, an infection model owning its latent process, a component sampling a vector/process-valued prior slot — threads its sub-component through here, and third-party component authors use it as the way to compose an as_turing_model inside their own @model body:

julia
latent ~ as_turing_submodel(inner_model, n)

Because as_turing_model also has Distribution and Vector{<:Distribution} methods, the same call composes a raw prior:

julia
damp ~ as_turing_submodel(model.damp, p; prefix = true)   # a Distribution or a process

prefix defaults to false — the package standard, keeping the submodel's variable names flat. Two kinds of call site pass prefix = true:

  • a prior slot (a component's damp / init / θ etc.), so the slot's left-hand name namespaces the whole prior submodel and a process-valued prior can never collide with the host's own variables;

  • the deliberately-prefixing components (PrefixLatentModel, Split), which stream their children under an explicit name.

Arguments

  • m: the component (or raw prior) to compose.

  • args...: positional arguments forwarded to as_turing_model (e.g. the series length n, or the observed/expected series for an observation model).

Keyword Arguments

  • prefix: whether to prefix the submodel's variables with the tilde left-hand name (default false).

Examples

Inside a component's @model body it is used on the right of a ~:

julia
latent ~ as_turing_submodel(inner_model, n)
damp ~ as_turing_submodel(model.damp, p; prefix = true)

It returns a Turing submodel; the underlying prior submodel returns a length-n value:

@example
using ComposableTuringIDModels, Distributions
length(as_turing_model(Normal(), 4)())
source
ComposableTuringIDModels.ascertainment_dayofweek Function

Build an Ascertainment model for a day-of-week reporting effect.

The latent model is wrapped with broadcast_dayofweek so a 7-day effect is broadcast across the expected-observation series, and combined multiplicatively with the expected observations by default.

Arguments

  • model: the underlying observation model.

Keyword Arguments

  • latent_model: the latent model broadcast over the week (default HierarchicalNormal()).

  • transform: the function (x, y) combining expected observations with the broadcast effect (default (x, y) -> x .* y).

  • latent_prefix: the prefix applied to the latent model's variables (default "DayofWeek").

Examples

@example
using ComposableTuringIDModels
obs = ascertainment_dayofweek(PoissonError())
mdl = as_turing_model(obs, missing, fill(10.0, 14))
rand(mdl)
source
ComposableTuringIDModels.assert_prior_length Function

Assert that a vector-of-Distributions prior has exactly k elements.

Pairs with prior_order: once a slot has fixed the order k (e.g. from damp), a second per-lag/per-element slot given as a vector (e.g. init) must match it. A single Distribution or a richer prior model broadcasts to k and imposes no constraint, so it always passes.

Arguments

  • p: the prior for the slot being checked. A vector of Distributions is length-checked; anything else broadcasts and always passes.

  • k: the required number of elements, fixed earlier by another slot.

  • what: a short description of the slot, used in the assertion message.

Examples

@example
using ComposableTuringIDModels, Distributions
ComposableTuringIDModels.assert_prior_length([Normal(), Normal()], 2, :damp)
source
ComposableTuringIDModels.at Function

Read a possibly-time-varying parameter at step t.

A scalar (a constant parameter, drawn from a Distribution prior through the single as_turing_submodel seam) is returned unchanged at every step; a vector (a per-step path, drawn from a process prior) is indexed at t; a matrix (a strata × time parameter) is indexed at column t. A component's recursion writes at(ρ, t) * … so the same code serves a constant and a time-varying (or strata-varying) parameter — the scalar branch is zero-cost (no per-step allocation).

This is the read side of the widening seam a custom component uses to make any per-step parameter optionally time-varying: draw the slot through as_turing_submodel, then read it per step with at. See Time-varying damping in an AR process for the worked example.

Examples

@example
using ComposableTuringIDModels
(ComposableTuringIDModels.at(0.5, 3), ComposableTuringIDModels.at([0.1, 0.2, 0.3], 2))
source
ComposableTuringIDModels.broadcast_dayofweek Function

Build a BroadcastLatentModel for a day-of-week effect: a transformed inner model repeated across a 7-day period.

Arguments

  • model: the inner latent model.

  • link: link applied before broadcasting (default x -> 7 * softmax(x), constraining the week effects to sum to 7).

Examples

@example
using ComposableTuringIDModels
broadcast_dayofweek(RandomWalk())
source
ComposableTuringIDModels.broadcast_n Function

Length of the inner series an AbstractBroadcastRule needs to produce a length-n broadcasted series. Each rule implements its own method.

Arguments

  • rule: the AbstractBroadcastRule.

  • n: the length of the broadcasted output series.

  • period: the broadcast period.

Examples

@example
using ComposableTuringIDModels
broadcast_n(RepeatEach(), 10, 7), broadcast_n(RepeatBlock(), 10, 7)
source
ComposableTuringIDModels.broadcast_rule Function

Expand an inner latent series to length n under an AbstractBroadcastRule. Each rule implements its own method.

Arguments

  • rule: the AbstractBroadcastRule.

  • latent: the inner latent series to expand.

  • n: the length of the broadcasted output series.

  • period: the broadcast period.

Examples

@example
using ComposableTuringIDModels
broadcast_rule(RepeatEach(), [1, 2], 5, 2)
source
ComposableTuringIDModels.broadcast_weekly Function

Build a BroadcastLatentModel for a piecewise-constant weekly process.

Arguments

  • model: the inner latent model.

Examples

@example
using ComposableTuringIDModels
broadcast_weekly(RandomWalk())
source
ComposableTuringIDModels.condition_model Function

Condition a DynamicPPL.Model by fixing some parameters and conditioning on others.

julia
condition_model(model, fix_parameters, condition_parameters)

equals condition(fix(model, fix_parameters), condition_parameters). Either named tuple may be empty.

Arguments

  • model: the DynamicPPL.Model to fix and condition.

  • fix_parameters: a named tuple of parameters to fix to constant values.

  • condition_parameters: a named tuple of parameters to condition on data.

Examples

@example
using ComposableTuringIDModels, Distributions
m = as_turing_model(RandomWalk(), 10)
condition_model(m, (rw_init = 0.0,), NamedTuple())
source
ComposableTuringIDModels.define_y_t Function

Unpack the observed count series an observation-error model scores from the data y_t, dispatching on the model type.

The default method covers every count family (Poisson, negative binomial) and the Gaussian family: it accepts a plain observation vector, a missing (replaced by a length-Y_t vector of missing for predictive simulation), a MissingObservations carrier (rebuilt into a ragged Vector{Union{Missing,T}}), or a NamedTuple carrying the counts in a y field alongside any extra per-time-point data (a model that needs more than the counts — e.g. BinomialError, which also needs the number of trials — reads those extra fields itself). This keeps the simple case ergonomic (a plain vector just works) while letting a model opt into a richer NamedTuple data contract.

Arguments

  • obs_model: the observation-error model.

  • y_t: the observed data — a vector, missing, a MissingObservations carrier, or a NamedTuple.

  • Y_t: the expected-observation series (used to size a missing series).

Examples

@example
using ComposableTuringIDModels
# A plain vector passes through; a NamedTuple's `y` field is unpacked.
define_y_t(PoissonError(), [1, 2, 3], fill(10.0, 3)),
define_y_t(PoissonError(), (y = [1, 2, 3],), fill(10.0, 3))
source

Build the ReportingTriangle data a ReportTriangle scores.

Dispatched on ReportTriangle as the triangle method of the shared define_y_t data-unpacking hook (the vector / NamedTuple methods serve the per-time-point error families). It accepts:

  • a ReportingTriangle — returned unchanged (already built);

  • a dense matrix N[t, d+1] — the present time defaults to now = size(N, 1) (the last reference day), masking cell (t, d) observed iff t + d ≤ now;

  • a long-form table of (reference, delay, count) rows (any Tables.jl table, e.g. a DataFrame) — accumulated into the matrix, with now supplied as a keyword;

  • missing — a fully observed triangle of missing cells for predictive simulation (now = n + Dmax, so every delay of every reference day is reported).

Arguments

  • obs_model: the ReportTriangle model (fixes Dmax).

  • y_t: the raw data — a ReportingTriangle, a matrix, a long-form table, or missing.

  • Y_t: the expected eventual-total series (its length n sets the number of reference days when sizing a missing triangle).

Keyword Arguments

  • now: the present time used for the t + d ≤ now mask. Defaults to the number of reference days for a matrix, and is required for a long-form table.

  • reference, delay, count: the column names in a long-form table (defaults :reference, :delay, :count).

Examples

@example
using ComposableTuringIDModels
obs = ReportTriangle(PoissonError(), [0.5, 0.3, 0.2])      # Dmax = 2
N = [10 5 2; 12 6 3; 14 7 4]                            # 3 reference days
define_y_t(obs, N, fill(20.0, 3)).observed
source
ComposableTuringIDModels.equal_dimensions Function

Partition n elements into m segments of as-equal-as-possible length.

Each segment gets floor(n / m), and the n mod m leftover elements are handed out one apiece to the leading segments. The segment lengths therefore always sum to exactly n (differing by at most one), which is required by ConcatLatentModels's @assert sum(dims) == n check. This is the default dimension_adaptor for ConcatLatentModels.

Arguments

  • n: the total number of elements.

  • m: the number of segments.

Examples

@example
using ComposableTuringIDModels
ComposableTuringIDModels.equal_dimensions(10, 3)
source
ComposableTuringIDModels.expected_Rt Function

Expected reproduction number from a discrete generation interval and an infection series.

Arguments

  • gen_int: the discrete generation interval weights (or a Renewal model, whose generation interval is used).

  • infections: the infection series (longer than the generation interval).

Examples

@example
using ComposableTuringIDModels
expected_Rt([0.2, 0.3, 0.5], [100.0, 200, 300, 400, 500])
source
ComposableTuringIDModels.forecast Function

Forecast observations over a future horizon from a fitted model.

Given a model fit to an observed series y of length (yielding chain), forecast predicts the observations for the next horizon time points     out of sample. Each posterior draw is carried forward: the fitted parameters and the in-sample latent path are held fixed while the latent process is extended over the horizon by drawing its future innovations from the prior, so the returned forecast propagates both parameter and latent uncertainty.

y extends along the TIME axis: a plain vector of length (the single-series case), a strata x T matrix (extended by horizon further columns), or a NamedTuple of per-stream series (each stream extended in turn). The strata count, when there is one, is read from the observation model and y exactly as IDProblem reads it at build time.

This works because the package's latent processes are non-centred: a RandomWalk, AR or MA accumulates an i.i.d. sequence of standard innovations, so the future innovations are independent prior draws that continue — rather than overwrite — the fitted trajectory. forecast extends each draw's innovation stream to the horizon length with fresh prior draws and then calls predict on the model rebuilt at length  .

The result is a chain of the same shape as the input containing the predicted observations y_t[T+1] … y_t[T+h] (the in-sample points stay conditioned on the data and so are not resampled). Pass it to generated_observables or returned to recover the extended latent trajectories per draw.

The extension is exact for the package's non-centred processes because their future innovations are independent of the fitted history. A latent whose stored stream is itself jointly correlated across time (e.g. an exact-GP MvNormal) would instead need its tail drawn conditional on the history; forecast detects that generically and errors rather than returning a mis-calibrated forecast.

Arguments

  • model: the fitted IDModel (or an IDProblem).

  • y: the observed series the model was fit to (length ).

  • chain: the posterior samples from fitting model to y.

  • horizon: the number of future time points to forecast.

Keyword arguments

  • rng: random number generator for the future innovations and the predictive draws.

Examples

@example
using ComposableTuringIDModels, Distributions, Turing, Random
Random.seed!(1)
model = IDModel(
    DirectInfections(; Z = RandomWalk(), initialisation = Normal(1.0, 0.5)),
    PoissonError())
y = fill(5, 15)
chain = sample(as_turing_model(model, y, length(y)), Prior(), 40;
    progress = false)
fc = forecast(model, y, chain, 7)
size(fc)
source

Forecast from a fitted IDProblem; see forecast for the model method. The observation model and infection process are taken from the problem, and the horizon extends the problem's tspan.

source
ComposableTuringIDModels.generate_observation_error_priors Function

Generate the priors required by an observation-error model. Returns a named tuple consumed by observation_error. The default is an empty tuple.

Arguments

  • obs_model: the observation-error model whose priors are generated.

  • y_t: the observed series (or missing when simulating predictively).

  • Y_t: the expected-observation series.

Examples

@example
using ComposableTuringIDModels
m = generate_observation_error_priors(NegativeBinomialError(), missing, fill(10.0, 5))
rand(m)
source
ComposableTuringIDModels.generated_observables Function

Wrap a model, data, and inference solution into an IDObservables.

When solution is an MCMC Chains, the model is re-run over the draws with DynamicPPL.returned to recover the model's returned generated quantities (e.g. (; generated_y_t, expected_y_t, I_t, Z_t)) per sample, stored in the generated field. For any other solution (an optimiser result, a prior draw, …) there are no per-draw generated quantities, so generated is missing.

Arguments

  • model: the model that was sampled (a conditioned DynamicPPL.Model).

  • data: the data the model was conditioned on.

  • solution: the inference solution (samples or optimiser result).

Examples

@example
using ComposableTuringIDModels, Distributions
m = as_turing_model(
    IDModel(
        DirectInfections(; Z = RandomWalk(), initialisation = Normal()),
        PoissonError()), missing, 10)
generated_observables(m, (; y_t = missing), rand(m))
source
ComposableTuringIDModels.get_param_array Function

Reshape an MCMCChains.Chains object into a (draws × chains) array of per-sample NamedTuples.

Arguments

  • chn: the Chains object.

Examples

@example
using ComposableTuringIDModels
nothing
source
ComposableTuringIDModels.get_state Function

Assemble the final sequence from the raw output of accumulate_scan.

The default method prepends initial_state to the last element of each accumulated state. Step structs whose state is a named tuple (e.g. the MA and LatentDelay steps) override this method to extract the relevant field.

Arguments

  • acc_step: the AbstractAccumulationStep used in the scan; the method dispatches on its concrete type.

  • initial_state: the seed state used by accumulate_scan.

  • state: the raw accumulated output produced by accumulate.

Examples

@example
using ComposableTuringIDModels
accumulate_scan(ComposableTuringIDModels.RWStep(), 0.0, [1.0, 2.0, 3.0])
source
ComposableTuringIDModels.gravity Function

Build a gravity coupling operator from populations and distances.

Off-diagonal entry is the share of stratum g's force of infection that comes from stratum h; the diagonal is the share from itself. within sets the self-weight relative to a typical pairwise term, so the default of 1.0 makes a stratum's own history count about as much as a neighbour's.

Populations are taken in units of their mean and each row is normalised to sum to one. Both are required rather than optional: renewal_pressure computes , so any overall scale on K is indistinguishable from scaling . Normalising leaves K carrying only where a stratum's force comes from and carrying its size. pop can therefore be raw counts.

is identifiable through the split between the diagonal and the off-diagonals, so it needs within to be non-zero. At within = 0 a common row factor divides out and cancels.

Arguments

  • pop: the population of each stratum, in any units.

  • dist: the strata × strata distance matrix. Only its off-diagonal entries are read.

Keyword Arguments

  • α: the exponent on the destination population (default 1.0).

  • β: the exponent on the origin population (default 1.0).

  • γ: the exponent on distance (default 2.0).

  • within: the own-stratum weight, relative to a typical pairwise term (default 1.0).

Examples

@example
using ComposableTuringIDModels
pop = [1.0e6, 2.0e5]
dist = [0.0 50.0; 50.0 0.0]
gravity(pop, dist; α = 0.0, β = 1.0, γ = 2.0)
source
ComposableTuringIDModels.implements_infection_interface Function

Check that model satisfies the AbstractInfectionModel interface: it is an infection model and as_turing_model(model, n) returns a DynamicPPL.Model.

The infection model owns its latent process internally, so the construction check passes only a series length n (no external latent path).

Arguments

  • model: the component to check.

Keyword Arguments

  • n: the shape argument used for the construction check (default 10). An Int draws a length-n path. A Dims{2} (n_strata, n_time) draws a strata x time matrix. Pass the matching value for a component that needs a strata axis.

Examples

julia
using ComposableTuringIDModels, Distributions
implements_infection_interface(
    DirectInfections(; Z = RandomWalk(), initialisation = Normal()))
source
ComposableTuringIDModels.implements_observation_interface Function

Check that model satisfies the AbstractObservationModel interface: it is an observation model and as_turing_model(model, y_t, Y_t) returns a DynamicPPL.Model.

Arguments

  • model: the component to check.

Keyword Arguments

  • y_t: the observed series for the construction check (default missing, i.e. predictive simulation).

  • Y_t: the expected-observation series (default fill(10.0, 10)).

Examples

julia
using ComposableTuringIDModels
implements_observation_interface(PoissonError())
source
ComposableTuringIDModels.implements_prior_interface Function

Check that model satisfies the AbstractPriorModel interface: it is a prior model and as_turing_model(model, n) returns a DynamicPPL.Model.

Every AbstractLatentModel is also an AbstractPriorModel, so this holds for latent models (a latent process used directly as a prior) as well as for any bespoke prior submodel. A bare Distribution is not a prior model (it composes through as_turing_submodel instead), so this returns false for one.

Arguments

  • model: the component to check.

Keyword Arguments

  • n: the prior length used for the construction check (default 10).

Examples

julia
using ComposableTuringIDModels
implements_prior_interface(RandomWalk())
source
ComposableTuringIDModels.infection_strata Function

The number of infection strata an observation model consumes.

The seam an observation model uses to say how many infection strata it consumes, given the number of observation streams in the data. The default passes the observation stream count straight through (a one-to-one mapping). Split with a weight map overrides it with the map's column count, so a many-to-one or many-to-many mapping can build the right-shaped infection process from the data alone.

Arguments

  • obs: the observation model.

  • n_obs_strata: the number of observation streams in the data.

Examples

@example
using ComposableTuringIDModels
ComposableTuringIDModels.infection_strata(
    Split(PoissonError(), [1.0 1.0 1.0]), 1)
source
ComposableTuringIDModels.observation_error Function

The per-time-point observation-error distribution given an expected value and the sampled priors. Each error family implements its own method.

Arguments

Examples

@example
using ComposableTuringIDModels
observation_error(PoissonError(), 10.0)
source
ComposableTuringIDModels.pairwise_gen_int Function

Fold per-pair generation intervals into a coupling operator.

Returns the strata × strata × lags array renewal_pressure takes, with entry [g, h, i] equal to K[g, h] * G[g, h, i]: the weight stratum h puts on stratum g at lag i. A between-stratum transmission can therefore carry a longer effective interval than a within-stratum one, which a single shared interval cannot express.

Lags are indexed forwards, so [:, :, 1] is lag 1.

Arguments

  • K: the strata × strata coupling weights.

  • G: the per-pair generation intervals, either a strata × strata × lags array whose [g, h, :] is the interval from h to g, or a single vector used for every pair.

Examples

@example
using ComposableTuringIDModels
K = [0.9 0.1; 0.05 0.95]
size(pairwise_gen_int(K, [0.2, 0.3, 0.5]))
source
ComposableTuringIDModels.path_prior Function

Widen a raw prior into a well-defined length-n PATH prior.

A bare Distribution given to a length-n PATH slot (an innovation, or a latent process such as Hierarchy's across) is wrapped in an Intercept so it is a constant path (one shared draw broadcast to length n), not a scalar. A process, an explicit IID/Intercept, or a vector passes through unchanged.

A component author writing a new PATH-slot constructor calls this on the raw argument, exactly as AR, MA and Hierarchy do for their innovation/across slots. Per-step PARAMETER slots (damp, θ, std, …) keep the bare Distribution — a scalar constant — and must not use this; use IID for n independent draws instead.

Examples

@example
using ComposableTuringIDModels, Distributions
ComposableTuringIDModels.path_prior(Normal()) isa Intercept
source
ComposableTuringIDModels.prior_order Function

The order (p/q/d) implied by a prior slot.

A vector of Distributions fixes the order to the vector length (one independent per-lag/per-element prior); a single Distribution or a richer prior model (an AbstractPriorModel) defaults to order 1. This is how AR and MA infer their order from damp/θ without a separate order argument; a component author defining a similar per-lag slot uses it the same way.

Examples

@example
using ComposableTuringIDModels, Distributions
(ComposableTuringIDModels.prior_order(Normal()),
    ComposableTuringIDModels.prior_order([Normal(), Normal()]))
source
ComposableTuringIDModels.r_to_R Function

Reproduction number implied by an exponential growth rate r and discrete generation interval w: .

Arguments

  • r: the exponential growth rate.

  • w: the discrete generation interval weights.

Examples

@example
using ComposableTuringIDModels
r_to_R(0.1, [0.2, 0.3, 0.5])
source
ComposableTuringIDModels.renewal_foi Function

Force of infection for a constant-generation-interval renewal step: the reproduction number times the renewal_pressure on each stratum,

This is the raw new-incidence term before any modifier (e.g. susceptible depletion) is applied. renewal_foi is shared by the internal ConstantRenewalStep core and the composable RenewalStep so the two cannot drift. It reads the coupling operator off the step and defers to renewal_pressure, so a new coupling needs no change here.

Arguments

  • step: the renewal step supplying the generation interval and the mixing.

  • window: the recent incidence, oldest to newest.

  • Rt: the reproduction number, a scalar or one value per stratum.

Examples

@example
using ComposableTuringIDModels
step = ComposableTuringIDModels.ConstantRenewalStep(reverse([0.2, 0.3, 0.5]))
ComposableTuringIDModels.renewal_foi(step, [10.0, 20.0, 30.0], 1.5)
source
ComposableTuringIDModels.renewal_init_state Function

The state a renewal scan carries: the newest incidence val alongside the window it came from, and substates when the step has modifiers.

Pairing the value with the window lets get_state read a field rather than index the accumulated array, which keeps the scan differentiable under every AD backend.

A new renewal step implements this, usually by wrapping renewal_init_window.

Arguments

  • step: the renewal step being seeded.

  • I₀: the initial incidence, one value or one per stratum.

  • r: the growth rate implied by .

  • len_gen_int: the number of lags the generation interval covers.

Examples

@example
using ComposableTuringIDModels
step = ComposableTuringIDModels.ConstantRenewalStep(reverse([0.2, 0.3, 0.5]))
ComposableTuringIDModels.renewal_init_state(step, 10.0, 0.1, 3)
source
ComposableTuringIDModels.renewal_init_window Function

The incidence window a renewal scan starts from: len_gen_int values decaying at the rate r implied by , seeded at I₀. A vector for one series, a strata × lags matrix for a stratified renewal, since I₀ and r broadcast.

A new renewal step implements this if it seeds its window differently, and renewal_init_state to wrap it in the state the scan carries.

Arguments

  • step: the renewal step being seeded.

  • I₀: the initial incidence, one value or one per stratum.

  • r: the growth rate implied by .

  • len_gen_int: the number of lags the generation interval covers.

Examples

@example
using ComposableTuringIDModels
step = ComposableTuringIDModels.ConstantRenewalStep(reverse([0.2, 0.3, 0.5]))
ComposableTuringIDModels.renewal_init_window(step, 10.0, 0.1, 3)
source
ComposableTuringIDModels.renewal_pressure Function

Generation-time-weighted incidence pressure on each stratum, before .

This is the one dispatch point for coupling between strata. It sits between the generation-time convolution and . That is why coupling is a slot on the renewal core rather than a renewal modifier: a modifier transforms the finished force of infection, and    .

The mixing argument chooses the method.

  • I (LinearAlgebra.I), the default, leaves the strata uncoupled. For a single series the window is a vector and this is a dot product, so a one-stratum model runs exactly the arithmetic it would with no strata axis at all.

  • A strata × strata matrix mixes the convolved histories, so off-diagonal mass is infection arriving from another stratum. Row g says where stratum g's force of infection comes from.

  • A strata × lags generation interval gives each stratum its own interval, with or without a mixing matrix on top.

  • A strata × strata × lags array gives a per-pair generation interval, so a between-stratum transmission can carry a longer effective interval. Build one with pairwise_gen_int. Its lags are indexed forwards, so K[:, :, 1] is the lag-1 weight.

Nothing about a mixing matrix is checked or normalised. A zero column K[:, h] means stratum h infects nobody, though its own incidence still evolves through K[h, h]. A zero row K[g, :] means stratum g receives no force at all, so its incidence falls to zero and stays there.

Extend it by adding a method: any operator on the convolved window is a valid coupling.

Arguments

  • mixing: the coupling operator.

  • g: the reversed generation interval, a vector or a strata × lags matrix.

  • window: the recent incidence, oldest to newest.

Examples

@example
using ComposableTuringIDModels, LinearAlgebra
window = [10.0 20.0; 5.0 8.0]        # 2 strata, 2 lags
g = [0.4, 0.6]
renewal_pressure(I, g, window)       # uncoupled
source
ComposableTuringIDModels.spectral_density Function

Spectral density of a HilbertSpaceGP covariance kernel at frequency ω, for marginal standard deviation σ and length scale .

The kernels are KernelFunctions.jl types. A kernel enters the Hilbert-space approximation only through this spectral density — the Fourier transform of the stationary covariance — so switching kernel just reweights the shared basis by . Adding a new kernel means adding a spectral_density method; nothing else changes.

ω may be a scalar or a vector (the call broadcasts). The one-dimensional squared-exponential, Matérn-3/2 and Matérn-5/2 spectral densities are [ DocumenterCitations.CitationSiteNode("riutortmayol2023practical-cite-5")

]

with   for Matérn order . The squared-exponential (SqExponentialKernel) gives infinitely differentiable, very smooth paths; Matern32Kernel (once-differentiable) and Matern52Kernel (twice-differentiable) give progressively rougher ones — the three choices most often used for a smooth epidemiological latent process. HilbertSpaceGP weights each basis function by .

Requires   and  . Both are asserted rather than allowed through: at   the Matérn densities are and return NaN, which would silently propagate into the basis weights. A new method should assert the same.

Examples

@example
using ComposableTuringIDModels
spectral_density(SqExponentialKernel(), [0.0, 1.0, 2.0], 1.0, 0.8)
source
ComposableTuringIDModels.spread_draws Function

Convert an MCMCChains.Chains object to a tidy DataFrame (one row per draw, with draw, chain, and iteration columns).

Arguments

  • chn: the Chains object to convert.

Examples

@example
using ComposableTuringIDModels
nothing
source
ComposableTuringIDModels.standardised_index Function

Standardise the integer index   to zero mean and unit standard deviation.

This is the input grid both HilbertSpaceGP and ExactGP hand to their covariance kernel, so a given length scale means the same thing for both. It also makes scale-free: the half-range approaches as grows rather than scaling like  , so a short stays representable by a fixed number of basis functions regardless of series length.

It is public so that a comparison against another Gaussian-process implementation — an AbstractGPs.jl GP, say — can be built on the same coordinates rather than on a reimplementation of this formula. Requires n > 1, since the standard deviation of a single point is zero.

Arguments

  • n: the series length.

Examples

@example
using ComposableTuringIDModels
standardised_index(5)
source