Public Documentation
Documentation for LoweredDistributions's public interface.
Contents
Index
LoweredDistributions.AbstractChainTrickLoweredDistributions.AbstractLoweringLoweredDistributions.CTMCLoweredDistributions.ChainStageLoweredDistributions.CoxianLoweredDistributions.ErlangChainLoweredDistributions.PhaseTypeLoweredDistributions.compartment_stagesLoweredDistributions.ctmcLoweredDistributions.jump_problemLoweredDistributions.linear_chain_reactionsLoweredDistributions.lowerLoweredDistributions.ode_problemLoweredDistributions.petri_netLoweredDistributions.phase_typeLoweredDistributions.reaction_systemLoweredDistributions.state_indexLoweredDistributions.transition_probability
Public API
LoweredDistributions.AbstractChainTrick Type
abstract type AbstractChainTrick <: LoweredDistributions.AbstractLoweringPhase-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
sourceLoweredDistributions.AbstractLowering Type
abstract type AbstractLoweringRoot 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
sourceLoweredDistributions.CTMC Type
struct CTMC{St<:Tuple, M<:(AbstractMatrix)} <: LoweredDistributions.AbstractLoweringA 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
ctmc: the constructor verb.transition_probability: theexp(Q t)kernel.
Fields
states::Tuple: Ordered state names; theQrows / columns follow this order.Q::AbstractMatrix: The generator matrix (rows sum to zero).
LoweredDistributions.ChainStage Type
struct ChainStageA single Erlang stage of a linear-chain delay representation.
An Erlang(stages = k Exponential sub-compartments in series, each leaving at rate = 1/\theta. An Exponential(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
compartment_stages: builds these from a delay.ErlangChain: theAbstractChainTrickwrapping a vector of these.
Fields
name::Symbol: Step name this stage was extracted from.rate::Float64: Per-stage exit rate. stages::Int64: Number of Exponential sub-compartments.
LoweredDistributions.Coxian Type
struct Coxian{R<:(AbstractVector{<:Real}), P<:(AbstractVector{<:Real})} <: LoweredDistributions.AbstractChainTrickA 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.
LoweredDistributions.ErlangChain Type
struct ErlangChain{S<:AbstractVector{ChainStage}} <: LoweredDistributions.AbstractChainTrickAn exact or moment-matched Erlang chain: compartment_stages wrapped as an AbstractChainTrick.
See also
compartment_stages: builds the wrapped stage vector.phase_type: the adaptive fit that returns this forc² ≤ 1.
Fields
stages::AbstractVector{ChainStage}: The per-step stages, in chain order.
LoweredDistributions.PhaseType Type
struct PhaseType{A<:(AbstractVector{<:Real}), M<:(AbstractMatrix{<:Real})} <: LoweredDistributions.AbstractChainTrickA 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 viaPhaseType(::Coxian)/PhaseType(::ErlangChain).phase_type: the adaptive fit that returns this forc² > 1.
Fields
α::AbstractVector{<:Real}: Initial distribution over phases.S::AbstractMatrix{<:Real}: Sub-generator over the transient phases.
LoweredDistributions.compartment_stages Function
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 Erlang(k, θ) leaf (an integer-shape Gamma) gives k stages at rate 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: theChainStagename (default:delay).moment_match: lower a non-Erlang leaf to the nearest Erlang chain instead of throwing (defaultfalse).
Examples
using LoweredDistributions, Distributions
compartment_stages(Gamma(3.0, 1.5))See also
ChainStage: the per-stage record.ErlangChain: wraps the returned vector as anAbstractChainTrick.phase_type: the adaptive fit that also coversc² > 1.
LoweredDistributions.ctmc Function
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 => transitionspairs;transitionsis a singleto => rateedge or a tuple / NamedTuple ofto => rateedges (rate >= 0).
Examples
using LoweredDistributions
model = ctmc(
:well => (:ill => 0.2),
:ill => (:well => 0.3, :dead => 0.1))
transition_probability(model, 5.0)See also
CTMC: the model type.transition_probability: theexp(Q t)kernel.
LoweredDistributions.jump_problem Function
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: theAbstractLoweringto build the jump process for, or aDistributionthat lowers to one.tspan: the(t0, t1)simulation span.
Keyword Arguments
u0: the initial per-state population counts (aVector{<:Integer}). Defaults to a single individual in the first state, which is what everyErlangChain/Coxian/CTMClowering starts from; a generalPhaseTypewith a mixture initial distributionα(e.g. fromphase_type's hyperexponential fit) has no single deterministic starting state, so it needs an explicitu0here — see "Distributional interpretation" above for what thatu0does and does not guarantee.
Examples
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
lower,CTMC,AbstractChainTrick: produce themthis reads.ode_problem,petri_net: the SciMLBase and AlgebraicPetri alternatives for the same lowering.
LoweredDistributions.linear_chain_reactions Function
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: theAbstractChainTrickto build reactions for, or aDistributionthat lowers to one.from,to: the upstream and downstream Catalyst species.
Keyword Arguments
prefix: aSymbolprefixing the generated sub-compartment species names (default:stage).
Examples
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
reaction_system: wraps this into a fullReactionSystem.lower,AbstractChainTrick: produce thechainthis reads.
LoweredDistributions.lower Function
lower(
d::Distributions.Exponential
) -> CTMC{St, M} where {St<:Tuple{Vararg{Symbol}}, M<:(Matrix)}lower(dist::Distribution)Lower a Distribution to a backend-agnostic dynamical-systems representation (an AbstractLowering).
Exponentiallowers to a two-state degenerateCTMC(on -> absorbedat the exponential's rate) — the trivial case of the CTMC fast path.Gamma(including integer-shape Erlang) lowers to anErlangChainwhenc² = var / mean² ≤ 1(always true forshape ≥ 1), and to aphase_typefit otherwise.Any other
Distributionlowers via the adaptivephase_typefit.
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: theDistributionto lower.
Examples
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 > 1See also
AbstractLowering,AbstractChainTrick: the hierarchy.ctmc,ErlangChain,phase_type: the representations this dispatches to.
lower(
d::Distributions.Distribution,
::Type{PhaseType};
phases,
max_phases
) -> PhaseTypelower(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: theDistributionto lower.PhaseType: thePhaseTypetype itself, selecting the canonical form.
Keyword Arguments
phases: fix the Erlang phase count instead of deriving it from the distribution's dispersion. Withphases = kthe result is always ak-stage Erlang whose rate matchesmean(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 countround(1 / c²)does depend on the value and so steps discontinuously as a parameter crosses a rounding boundary. When set,phasesoverridesmax_phases.max_phases: the largest Erlang phase count to build a sub-generator for (default1_000, a1_000 × 1_000matrix). Only thec² ≤ 1branch can reach it; thec² > 1hyperexponential fit is always two phases. Ignored whenphasesis set.
Examples
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 CTMCSee also
lower: the adaptive dispatch this canonicalises.phase_type: the two-moment fit both share.Fitting a lowered distribution under AD, stably: the
phaseskeyword worked through a Turing recipe, with the failure modes this method'sUnion-avoidance andmax_phasesguard against.
LoweredDistributions.ode_problem Function
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: theAbstractLoweringto 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 mass0).
Examples
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
lower,CTMC,AbstractChainTrick: produce themthis reads.reaction_system: the Catalyst reaction-network alternative for the same lowering.
LoweredDistributions.petri_net Function
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: theAbstractLoweringto build the Petri net for, or aDistributionthat lowers to one.
Keyword Arguments
prefix: aSymbolprefixing the generated state names (default:state).
Examples
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
ode_problem,reaction_system: the SciMLBase and Catalyst alternatives for the same lowering.
LoweredDistributions.phase_type Function
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 c² exactly. A sequential chain (an ErlangChain or Coxian) cannot reach c² > 1 — only a branching (mixture) structure can, hence the different representation.
Examples
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
ErlangChain,PhaseType: the two representations this returns.lower: dispatches here for the over-dispersed case.
LoweredDistributions.reaction_system Function
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: theAbstractChainTrickto build the system for, or aDistributionthat lowers to one (seelinear_chain_reactionsfor the scope of that form).from,to: the upstream and downstream Catalyst species.
Keyword Arguments
prefix: aSymbolprefixing the generated sub-compartment species names (default:stage).name: theReactionSystem's name (default:lowered).
Examples
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
linear_chain_reactions: the lower-level species/reaction builder this wraps.
LoweredDistributions.state_index Function
state_index(m::CTMC, s::Symbol) -> Union{Nothing, Int64}state_index(m::CTMC, s::Symbol)The index of state s in m.states, or nothing if s is not present.
Arguments
m: theCTMCmodel.s: the state name to look up.
Examples
using LoweredDistributions
m = ctmc(:well => (:ill => 0.2), :ill => (:well => 0.3, :dead => 0.1))
state_index(m, :ill)LoweredDistributions.transition_probability Function
transition_probability(m::CTMC, t::Real) -> AnyThe 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
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 oneSee also
CTMC: the model type.