Skip to content

Public Documentation

Documentation for LoweredDistributions's public interface.

Contents

Index

Public API

LoweredDistributions.AbstractChainTrick Type
julia
abstract type AbstractChainTrick <: LoweredDistributions.AbstractLowering

Phase-type branch of AbstractLowering.

Representations built from a finite absorbing Markov chain over exponential phases: ErlangChain, Coxian, and PhaseType. Every concrete subtype converts to the canonical PhaseType(α, S) view (see PhaseType(::AbstractChainTrick) methods), which is what a downstream backend (e.g. the Catalyst extension) consumes.


Fields

source
LoweredDistributions.AbstractLowering Type
julia
abstract type AbstractLowering

Root of the distribution-lowering hierarchy.

A backend-agnostic dynamical-systems representation that lower produces from a Distributions.Distribution. CTMC subtypes this directly; the phase-type representations subtype it through AbstractChainTrick.


Fields

source
LoweredDistributions.CTMC Type
julia
struct CTMC{St<:Tuple, M<:(AbstractMatrix)} <: LoweredDistributions.AbstractLowering

A continuous-time Markov chain generator over states.

CTMC holds a transition-intensity (generator) matrix Q over an ordered set of states. Off-diagonal Q[i, j] is the i -> j transition rate, and each diagonal is Q[i, i] = -sum_{j != i} Q[i, j]. Holding times are exponential, so the transition-probability matrix is P(t) = exp(Q t) in closed form (see transition_probability).

Build it with ctmc.

See also


Fields

  • states::Tuple: Ordered state names; the Q rows / columns follow this order.

  • Q::AbstractMatrix: The generator matrix (rows sum to zero).

source
LoweredDistributions.ChainStage Type
julia
struct ChainStage

A single Erlang stage of a linear-chain delay representation.

An Erlang(, ) delay is stages = k Exponential sub-compartments in series, each leaving at rate = 1/\theta. An Exponential() delay is the stages = 1 case. The mean dwell time is stages / rate; name records which step of a chain the stage came from (:delay for a single leaf).

See also


Fields

  • name::Symbol: Step name this stage was extracted from.

  • rate::Float64: Per-stage exit rate .

  • stages::Int64: Number of Exponential sub-compartments .

source
LoweredDistributions.Coxian Type
julia
struct Coxian{R<:(AbstractVector{<:Real}), P<:(AbstractVector{<:Real})} <: LoweredDistributions.AbstractChainTrick

A Coxian phase-type: stages exponential phases visited in order, each absorbing with probability 1 - probs[i] instead of continuing to phase i + 1 (probs[end] is ignored — the last phase always absorbs on exit).

A sequential (never-branching) chain like this can only reach a squared coefficient of variation c² ≤ 1 (the same regime as ErlangChain, which it generalises to heterogeneous per-phase rates); an over-dispersed c² > 1 delay needs the branching a general PhaseType allows (see phase_type).

See also

  • PhaseType: PhaseType(::Coxian) gives the canonical (α, S) embedding.

Fields

  • rates::AbstractVector{<:Real}: Per-phase exit rate.

  • probs::AbstractVector{<:Real}: Per-phase probability of continuing to the next phase.

source
LoweredDistributions.ErlangChain Type
julia
struct ErlangChain{S<:AbstractVector{ChainStage}} <: LoweredDistributions.AbstractChainTrick

An exact or moment-matched Erlang chain: compartment_stages wrapped as an AbstractChainTrick.

See also


Fields

  • stages::AbstractVector{ChainStage}: The per-step stages, in chain order.
source
LoweredDistributions.PhaseType Type
julia
struct PhaseType{A<:(AbstractVector{<:Real}), M<:(AbstractMatrix{<:Real})} <: LoweredDistributions.AbstractChainTrick

A general phase-type distribution: an initial distribution α over transient phases plus a sub-generator S (the absorbing continuous-time Markov chain of a finite mixture / chain of exponential phases).

α sums to one; S has non-positive diagonal, non-negative off-diagonal, and row sums ≤ 0 (the shortfall -sum(S[i, :]) is phase i's exit rate to the absorbing state).

See also

  • Coxian, ErlangChain: narrower representations this type generalises; both convert here via PhaseType(::Coxian) / PhaseType(::ErlangChain).

  • phase_type: the adaptive fit that returns this for c² > 1.


Fields

  • α::AbstractVector{<:Real}: Initial distribution over phases.

  • S::AbstractMatrix{<:Real}: Sub-generator over the transient phases.

source
LoweredDistributions.compartment_stages Function
julia
compartment_stages(
    d::Distributions.Distribution;
    name,
    moment_match
) -> Vector{ChainStage}

Lower a delay distribution to its compartment-stage structure.

An Exponential(θ) leaf gives one stage at rate ; an Erlang(k, θ) leaf (an integer-shape Gamma) gives k stages at rate . Exact only for Exponential / Erlang leaves; any other family throws unless moment_match = true, which lowers to the nearest Erlang chain by matching the mean and squared coefficient of variation c² = var / mean² (stages = round(1 / c²), rate = stages / mean). An over-dispersed delay (c² > 1) still throws — see phase_type for that case.

Arguments

  • d: an Exp/Erlang delay leaf to lower.

Keyword Arguments

  • name: the ChainStage name (default :delay).

  • moment_match: lower a non-Erlang leaf to the nearest Erlang chain instead of throwing (default false).

Examples

julia
using LoweredDistributions, Distributions

compartment_stages(Gamma(3.0, 1.5))

See also

source
LoweredDistributions.ctmc Function
julia
ctmc(
    specs::Pair...
) -> CTMC{St, M} where {St<:Tuple{Vararg{Symbol}}, M<:(Matrix)}

Build a CTMC from from => (to => rate, ...) transition-rate specs.

Each argument is from => transitions, where transitions lists the outgoing to => rate edges of state from (a single edge or a tuple / NamedTuple of them). The state order is the order of first appearance (each from, then any to-only absorbing states).

Enzyme

ctmc(specs...) does not differentiate under Enzyme (forward or reverse): the heterogeneous Pair vararg spec parsing hits an upstream Enzyme compiler limitation. ForwardDiff, ReverseDiff, and Mooncake (both modes) differentiate it correctly; the plain _matrix_exp / transition_probability kernel differentiates cleanly under every backend including Enzyme (see the ADFixtures :recurrent scenarios), which isolates the break to this builder's spec parsing rather than the matrix exponential.

Arguments

  • specs: from => transitions pairs; transitions is a single to => rate edge or a tuple / NamedTuple of to => rate edges (rate >= 0).

Examples

julia
using LoweredDistributions

model = ctmc(
    :well => (:ill => 0.2),
    :ill => (:well => 0.3, :dead => 0.1))
transition_probability(model, 5.0)

See also

source
LoweredDistributions.jump_problem Function
julia
jump_problem(m::AbstractLowering, tspan; u0 = nothing)

Build a JumpProcesses.JumpProblem that exactly simulates a single individual jumping between m's states at its generator rates (a Gillespie/Doob sample path of the underlying continuous-time Markov chain). Solving it (e.g. with SSAStepper()) draws one such path over tspan.

Every off-diagonal generator entry Q[i, j] > 0 becomes one JumpProcesses.MassActionJump transition state_i -> state_j at that constant rate, applied to a per-state population count (u, integers), not the occupation-probability vector ode_problem and petri_net use.

Distributional interpretation. For an m produced by lower started from its default u0 (a single individual deterministically in the first/entry state — every ErlangChain/Coxian/CTMC lowering, and any PhaseType with a one-hot α), the chain is guaranteed to reach the absorbing state, and the time it first does so is one exact draw from the Distribution that m lowers. This does NOT hold for an arbitrary m: a hand-built ctmc need not have an absorbing state at all (the simulation then just runs until tspan ends), and a PhaseType with a genuine mixture α (e.g. from phase_type's hyperexponential fit) has no single deterministic starting state to default from — an explicit u0 there fixes the simulation to start from that one state, giving a draw from the distribution conditional on that starting phase, not from the mixture itself. To draw an exact mixture sample, sample the starting phase from α yourself (e.g. rand(Categorical(α))) and pass the matching one-hot u0.

Only defined when JumpProcesses is loaded (using JumpProcesses); the method lives in the LoweredDistributionsJumpProcessesExt package extension.

Arguments

  • m: the AbstractLowering to build the jump process for, or a Distribution that lowers to one.

  • tspan: the (t0, t1) simulation span.

Keyword Arguments

  • u0: the initial per-state population counts (a Vector{<:Integer}). Defaults to a single individual in the first state, which is what every ErlangChain/Coxian/CTMC lowering starts from; a general PhaseType with a mixture initial distribution α (e.g. from phase_type's hyperexponential fit) has no single deterministic starting state, so it needs an explicit u0 here — see "Distributional interpretation" above for what that u0 does and does not guarantee.

Examples

julia
using LoweredDistributions, JumpProcesses

model = ctmc(:well => (:ill => 0.2), :ill => (:well => 0.3, :dead => 0.1))
prob = jump_problem(model, (0.0, 5.0))
sol = solve(prob, SSAStepper())
sol.u[end]

See also

source
LoweredDistributions.linear_chain_reactions Function
julia
linear_chain_reactions(chain::AbstractChainTrick, from, to; prefix = :stage)
linear_chain_reactions(dist::Distribution, from, to; kwargs...)

Build the Catalyst reactions threading from, through one species per phase of chain (or lower(dist)), to to.

Only defined when Catalyst.jl is loaded (using Catalyst); the methods live in the LoweredDistributionsCatalystExt package extension.

The Distribution form needs lower(dist) isa AbstractChainTrick; a distribution lowering to a bare CTMC (e.g. lower(::Exponential), the degenerate two-state form) is not a from -> to compartment pipe and is out of scope for this bridge — pass ErlangChain(dist) explicitly, or lower it via phase_type / compartment_stages directly, for the Exponential/Erlang case.

Arguments

  • chain: the AbstractChainTrick to build reactions for, or a Distribution that lowers to one.

  • from, to: the upstream and downstream Catalyst species.

Keyword Arguments

  • prefix: a Symbol prefixing the generated sub-compartment species names (default :stage).

Examples

julia
using LoweredDistributions, Distributions, Catalyst

t = Catalyst.default_t()
@species From(t) To(t)
built = linear_chain_reactions(Gamma(3.0, 1.5), From, To; prefix = :I)
length(built.species)

See also

source
LoweredDistributions.lower Function
julia
lower(
    d::Distributions.Exponential
) -> CTMC{St, M} where {St<:Tuple{Vararg{Symbol}}, M<:(Matrix)}
julia
lower(dist::Distribution)

Lower a Distribution to a backend-agnostic dynamical-systems representation (an AbstractLowering).

  • Exponential lowers to a two-state degenerate CTMC (on -> absorbed at the exponential's rate) — the trivial case of the CTMC fast path.

  • Gamma (including integer-shape Erlang) lowers to an ErlangChain when c² = var / mean² ≤ 1 (always true for shape ≥ 1), and to a phase_type fit otherwise.

  • Any other Distribution lowers via the adaptive phase_type fit.

Which representation comes back therefore depends on dist's value, not only on its type, so this method's return type is a Union — see lower(dist, PhaseType) below for the type-stable form automatic differentiation needs.

Arguments

  • dist: the Distribution to lower.

Examples

julia
using LoweredDistributions, Distributions

lower(Exponential(2.0))               # CTMC(2 states)
lower(Gamma(3.0, 1.5))                # ErlangChain(3 compartments)
lower(Gamma(0.5, 1.0))                # PhaseType(2 phases), c² = 2 > 1

See also

source
julia
lower(
    d::Distributions.Distribution,
    ::Type{PhaseType};
    phases,
    max_phases
) -> PhaseType
julia
lower(dist::Distribution, ::Type{PhaseType}; max_phases = 1_000)

Lower dist to the canonical PhaseType (α, S) form, whatever its dispersion — the type-stable lowering, and the one to differentiate through.

lower(dist) picks its representation from dist's value (an ErlangChain for c² ≤ 1, a PhaseType for c² > 1, a CTMC for an Exponential), so its return type is a Union that only resolves at runtime. That Union is fine for ordinary use, and every backend canonicalises to PhaseType anyway, but on a differentiated path it breaks Enzyme outright (forward and reverse), and the c² ≤ 1 branch cannot carry an AD dual at all — ChainStage stores its rate in a concrete Float64 field. This method always returns the same concrete type and never touches ChainStage, so it differentiates on every backend the package tests (ForwardDiff, ReverseDiff, both Mooncake modes, both Enzyme modes).

The fit is the same two-moment fit lower performs — an Erlang chain of round(1 / c²) phases for c² ≤ 1, the balanced-means hyperexponential for c² > 1 — so the phase-type agrees with PhaseType(lower(dist)) to floating-point tolerance wherever that conversion exists. It is not always bit-identical: for an exact Exponential/Erlang leaf, compartment_stages reads the rate straight off the distribution's scale (1 / θ), while this method recomputes it from the moments (k / mean(dist)), which can differ in the last unit in the last place. The structure — phase count, chain shape, matched mean — is the same.

What the canonical form does cost is memory. It holds an explicit k × k sub-generator, where the c² ≤ 1 fit needs k = round(1 / c²) phases, so a tight (near-deterministic) delay asks for a very large dense matrix: Normal(5, 0.001) has c² = 4e-8, hence 25 million phases. An ErlangChain stores the same chain as a phase count and does not grow with k at all. max_phases caps the fit so that case raises an actionable error rather than exhausting memory; raise it if a long chain is genuinely wanted.

Arguments

  • dist: the Distribution to lower.

  • PhaseType: the PhaseType type itself, selecting the canonical form.

Keyword Arguments

  • phases: fix the Erlang phase count instead of deriving it from the distribution's dispersion. With phases = k the result is always a k-stage Erlang whose rate matches mean(dist) (rate = k / mean), so the phase count — and hence the (α, S) dimension — is independent of the distribution's value. This is the AD-stable path for fitting under Turing: the number of compartments is a discrete quantity you cannot differentiate through, so fix it and infer only the (continuous) rate; the result then differentiates on every backend, Enzyme included. The default (nothing) keeps the adaptive two-moment fit, whose phase count round(1 / c²) does depend on the value and so steps discontinuously as a parameter crosses a rounding boundary. When set, phases overrides max_phases.

  • max_phases: the largest Erlang phase count to build a sub-generator for (default 1_000, a 1_000 × 1_000 matrix). Only the c² ≤ 1 branch can reach it; the c² > 1 hyperexponential fit is always two phases. Ignored when phases is set.

Examples

julia
using LoweredDistributions, Distributions

lower(Gamma(3.0, 1.5), PhaseType)     # PhaseType(3 phases), was an ErlangChain
lower(Gamma(0.5, 1.0), PhaseType)     # PhaseType(2 phases), c² = 2 > 1
lower(Gamma(3.0, 1.5), PhaseType; phases = 5)   # a fixed 5-phase Erlang
lower(Exponential(2.0), PhaseType)    # PhaseType(1 phase), was a CTMC

See also

source
LoweredDistributions.ode_problem Function
julia
ode_problem(m::AbstractLowering, tspan; u0 = nothing)

Build the linear forward-Kolmogorov ODE du/dt = Q' u for m as a SciMLBase.ODEProblem, ready to solve with any OrdinaryDiffEq-compatible algorithm.

u is the state-occupation probability vector: one entry per state (m.states order) for a CTMC, or one entry per transient phase plus a trailing absorbed-mass entry for an AbstractChainTrick (its canonical PhaseType view, extended with the absorbing state). sum(u) is conserved at 1 for every t.

Only defined when SciMLBase is loaded (using SciMLBase, or any package that depends on it, e.g. using OrdinaryDiffEq); the method lives in the LoweredDistributionsSciMLBaseExt package extension.

Arguments

  • m: the AbstractLowering to build the ODE for.

  • tspan: the (t0, t1) integration span.

Keyword Arguments

  • u0: the initial state-occupation vector. Defaults to a point mass on the first state (CTMC) or the phase-type's own initial distribution α (AbstractChainTrick, absorbed mass 0).

Examples

julia
using LoweredDistributions, SciMLBase, OrdinaryDiffEqTsit5

model = ctmc(:well => (:ill => 0.2), :ill => (:well => 0.3, :dead => 0.1))
prob = ode_problem(model, (0.0, 5.0))
sol = solve(prob, Tsit5())
sol.u[end]

See also

source
LoweredDistributions.petri_net Function
julia
petri_net(m::AbstractLowering; prefix = :state)

Build an AlgebraicPetri.LabelledPetriNet for m's full state-space generator, plus the name-indexed rate and initial-condition Dicts AlgebraicPetri.vectorfield's own mass-action ODE function needs (it indexes u/p by species/transition name, not position).

Every off-diagonal generator entry Q[i, j] > 0 becomes one Petri net transition state_i -> state_j at that constant rate. Only defined when AlgebraicPetri.jl is loaded (using AlgebraicPetri); the method lives in the LoweredDistributionsAlgebraicPetriExt package extension.

AlgebraicPetri's own vectorfield also accepts a time/state-varying Function rate per transition (its mass-action kinetics is more general than this package needs), but every AbstractLowering's generator is constant in practice — a PhaseType's S is type-constrained to <: AbstractMatrix{<:Real} at construction, and a CTMC's Q is runtime-validated the same way by _validate_generator — so a Function-valued rate should never reach here. The extension asserts this explicitly at the entry point rather than relying on either upstream guarantee alone.

Arguments

  • m: the AbstractLowering to build the Petri net for, or a Distribution that lowers to one.

Keyword Arguments

  • prefix: a Symbol prefixing the generated state names (default :state).

Examples

julia
using LoweredDistributions, Distributions, AlgebraicPetri

built = petri_net(Gamma(3.0, 1.5))
f! = vectorfield(built.petri_net)
du = Dict(k => 0.0 for k in keys(built.u0))
f!(du, built.u0, built.rates, 0.0)

Not a live @example: AlgebraicPetri's own Catalyst weakdep extension caps Catalyst at a version incompatible with this package's own Catalyst extension, so the two cannot load in the same doctest session. This exact sequence — default prefix, vectorfield, and the f! call — is exercised by name in the isolated test/algebraic_petri test environment instead (see test/algebraic_petri/petri.jl).

See also

source
LoweredDistributions.phase_type Function
julia
phase_type(
    d::Distributions.Distribution
) -> Union{ErlangChain{Vector{ChainStage}}, PhaseType}

Adaptively fit a phase-type representation to d by matching its first two moments.

Under-dispersed or exactly-dispersed (c² = var(d) / mean(d)² ≤ 1) delays fit an ErlangChain exactly on the mean (the same moment match compartment_stages performs). Over-dispersed delays (c² > 1) fit a two-phase hyperexponential PhaseType (a mixture of two Exponentials) by the balanced-means method: with p = (1 + sqrt((c² - 1) / (c² + 1))) / 2, rates λ₁ = 2p / mean(d) and λ₂ = 2(1 - p) / mean(d) give a mixture with p/1 - p phase weights that matches mean(d) and exactly. A sequential chain (an ErlangChain or Coxian) cannot reach c² > 1 — only a branching (mixture) structure can, hence the different representation.

Examples

julia
using LoweredDistributions, Distributions

phase_type(Gamma(2.5, 1.0))     # c² = 0.4 ≤ 1 -> ErlangChain
phase_type(Gamma(0.5, 1.0))     # c² = 2 > 1   -> PhaseType (hyperexponential)

See also

source
LoweredDistributions.reaction_system Function
julia
reaction_system(chain::AbstractChainTrick, from, to; prefix = :stage, name = :lowered)
reaction_system(dist::Distribution, from, to; kwargs...)

Build a Catalyst ReactionSystem for chain (or lower(dist)) between the from and to species — linear_chain_reactions wrapped into a complete model, ready for the Catalyst/ModelingToolkit ODE, SDE, or jump conversion.

Only defined when Catalyst.jl is loaded (using Catalyst); the method lives in the LoweredDistributionsCatalystExt package extension.

Arguments

  • chain: the AbstractChainTrick to build the system for, or a Distribution that lowers to one (see linear_chain_reactions for the scope of that form).

  • from, to: the upstream and downstream Catalyst species.

Keyword Arguments

  • prefix: a Symbol prefixing the generated sub-compartment species names (default :stage).

  • name: the ReactionSystem's name (default :lowered).

Examples

julia
using LoweredDistributions, Distributions, Catalyst

t = Catalyst.default_t()
@species From(t) To(t)
rs = reaction_system(Gamma(3.0, 1.5), From, To; name = :chain)
Catalyst.reactions(rs)

See also

source
LoweredDistributions.state_index Function
julia
state_index(m::CTMC, s::Symbol) -> Union{Nothing, Int64}
julia
state_index(m::CTMC, s::Symbol)

The index of state s in m.states, or nothing if s is not present.

Arguments

  • m: the CTMC model.

  • s: the state name to look up.

Examples

julia
using LoweredDistributions

m = ctmc(:well => (:ill => 0.2), :ill => (:well => 0.3, :dead => 0.1))
state_index(m, :ill)
source
LoweredDistributions.transition_probability Function
julia
transition_probability(m::CTMC, t::Real) -> Any

The transition-probability matrix P(t) = exp(Q t) of a CTMC over a time gap t.

P[i, j] is the probability of being in state j a time t after being in state i, marginalising over every intermediate jump.

Examples

julia
using LoweredDistributions

model = ctmc(:well => (:ill => 0.2), :ill => (:well => 0.3, :dead => 0.1))
P = transition_probability(model, 2.0)
sum(P; dims = 2)  # each row sums to one

See also

  • CTMC: the model type.
source