Skip to content

Delay chains and the linear chain trick

Introduction

A multi-step delay is a chain: each step adds an independent delay onto the previous event. Sequential composes the steps, and the chain is one distribution over the whole origin-to-final gap. When every step is an Exponential with the same rate, the total is an Erlang (a Gamma with integer shape). This is the linear chain trick, the identity behind representing a Gamma-distributed delay as a series of exponential compartments.

This tutorial builds a chain, reads its structure and moments, and collapses it to its total. It builds on Composing distributions.

julia
using ComposedDistributions
using Distributions

A chain of exponential steps

We build a four-step chain, each step an Exponential with mean theta. sequential names the steps; a bare Vector passed to compose is the same chain with default step names.

julia
theta = 1.5

steps = [Symbol("step_", i) => Exponential(theta) for i in 1:4]

chain = sequential(steps...)
Sequential (4 steps)
├─ step_1: Distributions.Exponential{Float64}(θ=1.5)
├─ step_2: Distributions.Exponential{Float64}(θ=1.5)
├─ step_3: Distributions.Exponential{Float64}(θ=1.5)
└─ step_4: Distributions.Exponential{Float64}(θ=1.5)

The flat event layout is the origin plus one event per step.

julia
event_names(chain)
(:event_1, :event_2, :event_3, :event_4, :event_5)

Additive moments

The overall moments of a chain sum over the steps, so a four-step exponential chain has mean 4 * theta and variance 4 * theta^2.

julia
mean(chain), var(chain)
(6.0, 9.0)

Collapsing to the total

observed_distribution collapses the chain to the single distribution of its origin-to-final gap, integrating the intermediate events out. For the exponential chain this total is an Erlang, so its moments match Gamma(4, theta).

julia
total = observed_distribution(chain)

(chain_mean = mean(total), gamma_mean = mean(Gamma(4, theta)))
(chain_mean = 6.0, gamma_mean = 6.0)

The variance matches too.

julia
(chain_var = var(total), gamma_var = var(Gamma(4, theta)))
(chain_var = 9.0, gamma_var = 9.0)

The linear chain trick is exactly this identity: a chain of k exponential steps of mean theta is a Gamma(k, theta) delay, so a smooth delay can be represented as a series of memoryless compartments and back again.

Reusing the chain

A chain is a composer, so it drops into a larger tree as a branch and the same steps can be reused across models.

julia
tree = compose((incubation = chain, reporting = Exponential(2.0)))

event_names(tree)
(:event_1, :event_2, :event_3, :event_4, :event_5, :event_6)

Summary

  • A Sequential chain composes per-step delays into one distribution over the whole gap.

  • The overall mean and var are additive over the steps.

  • observed_distribution collapses the chain to its convolved total.

  • A chain of k equal exponential steps is a Gamma(k, theta), the linear chain trick, and the chain nests inside a larger tree.

Where next