Interface contracts: valid nodes and leaves
This page is the reference for what makes a type a valid participant in composition, stated as the exact method contract the package relies on. A composer combines named child distributions into an event tree; the tree walkers reach every node and leaf through a small set of methods, so any type that implements those methods composes with the built-ins with no extra work.
The reusable interface-conformance suite ComposedDistributions.TestUtils checks these contracts over every built-in node shape and a user-defined node, and the package runs it in test/interfaces.jl, so the prose here and the tests stay in sync. To add a valid member, subtype the right abstract, implement the methods listed for its role, and run test_composed_interface (or test_interface for a plain leaf) over an instance.
The type landscape
The composer nodes share one supertype, AbstractComposedDistribution{F, S}. The named-child composers and the univariate one_of family sit under it; leaves and leaf wrappers are plain univariate distributions under no composer supertype.
Distribution{F, S}
└── AbstractComposedDistribution{F, S} named children → an event tree
├── AbstractMultiChild{S} positional, tree-walked together
│ ├── Sequential named steps in series (a chain)
│ └── Parallel named branches off one origin (fan-out)
├── Choose data-selected disjoint alternatives
└── AbstractOneOf one univariate time-to-event marginal
├── Resolve a fixed-probability mixture
└── Compete racing hazards (soonest cause fires)
plain univariate leaves (no composer supertype):
Shared a tied leaf (one free parameter across branches)
NoEvent an absorbing no-event branch
any Distributions.jl UnivariateDistributionAbstractComposedDistribution is parametric on variate form F (Univariate / Multivariate), so one supertype spans the univariate one_of members and the multivariate event-tree composers while preserving Distribution{F, S}. AbstractMultiChild is an intermediate that groups the two positional multi-child composers (Sequential, Parallel) the tree walkers dispatch over together; Choose is a sibling, not a multi-child node; and AbstractOneOf re-roots the univariate one_of family under the composed supertype, so it stays a UnivariateDistribution while sharing the composed abstract. The tree walkers still dispatch on AbstractOneOf wherever the two one_of nodes behave alike (one event slot per outcome, the shared origin, the per-outcome draw) and on the concrete type only where the scoring arithmetic differs. Downstream extension packages (CensoredDistributions and its siblings) dispatch on these supertypes, so the names and shape match the shared contract. test_abstract_membership pins the membership down as a test, so a type filed under the wrong supertype fails.
The composer-node contract
A realisation of a composed tree is one flat vector of leaf values laid out depth-first, and each node reads and writes only its own contiguous slice by an offset. Three methods carry this, reached by the qualified name (ComposedDistributions.child_nleaves and friends, which are public but not exported):
child_nleaves(node)returns a positiveInt, the flat-slot count (one per leaf below the node);child_logpdf(node, x, offset, n)returns a finite scalar over the node'sn-wide slicex[offset + 1 : offset + n], independent of the surrounding padding;child_rand!(out, offset, rng, node)fills exactly that slice in place and returnsnothing, leaving the padding either side untouched.
A node delegates to each child by the same three methods, passing each child its own offset, so it nests inside any other node automatically. component_names(node) returns a Tuple of the child names.
A univariate leaf is the base case: it occupies one slot (child_nleaves == 1), child_rand! writes its single draw, and child_logpdf scores x[offset + 1]. Any Distributions.jl distribution is therefore a valid leaf with no package-specific hooks.
Adding a valid composer node
Implement the three
child_*methods so they read and write only the node's own slice, delegating to each child by the same methods.Implement
component_namesif the node carries named children.Verify against the suite by running the same conformance checks the built-ins pass.
using ComposedDistributions, Distributions, Random
import ComposedDistributions: child_nleaves, child_logpdf, child_rand!
# A minimal node combining two branches side by side.
struct Both{A, B}
first::A
second::B
end
child_nleaves(b::Both) = child_nleaves(b.first) + child_nleaves(b.second)
function child_logpdf(b::Both, x, offset, ::Int)
n1 = child_nleaves(b.first)
n2 = child_nleaves(b.second)
return child_logpdf(b.first, x, offset, n1) +
child_logpdf(b.second, x, offset + n1, n2)
end
function child_rand!(out, offset, rng::AbstractRNG, b::Both)
n1 = child_nleaves(b.first)
child_rand!(out, offset, rng, b.first)
child_rand!(out, offset + n1, rng, b.second)
return nothing
end
node = Both(Gamma(2.0, 1.0), LogNormal(0.5, 0.4))
out = zeros(child_nleaves(node))
child_rand!(out, 0, Random.default_rng(), node)
child_logpdf(node, out, 0, child_nleaves(node))The one_of-outcome family: AbstractOneOf
The two one_of-outcome nodes share the supertype AbstractOneOf: Resolve (the fixed-probability mixture, cause and timing independent) and Compete (racing hazards, with the winning probability derived from the hazards). AbstractOneOf subtypes AbstractComposedDistribution{Univariate, Continuous}, so the one_of family is the univariate arm of the composer hierarchy. Both are univariate marginals, so each occupies a single flat slot and satisfies the node contract through the univariate-leaf base case.
A valid member subtypes AbstractOneOf, stores its outcome names, and implements the standard univariate interface (logpdf, rand, and the moments it can compute) so the marginal is a proper distribution.
using ComposedDistributions, Distributions
r = resolve(:death => (Gamma(1.5, 1.0), 0.3), :disch => (Gamma(2.0, 1.5), 0.7))
c = compete(:death => Gamma(2.0, 3.0), :recover => Gamma(3.0, 2.0))
r isa ComposedDistributions.AbstractOneOf
c isa ComposedDistributions.AbstractOneOfThe introspection contract
A composed tree exposes its structure through name introspection, and every built-in node keeps these in agreement:
component_names(node)— theTupleof immediate child names;event_names— the flat per-event name tuple (one entry per leaf edge, plus the origin);event_tree— the same names as a nested record;event— fetch a child or descend a name path;params_table— the free parameters flattened to a Tables.jl table, one row per parameter.
using ComposedDistributions, Distributions
import ComposedDistributions: component_names
tree = compose((onset_admit = Gamma(2.0, 1.0),
admit_death = LogNormal(0.5, 0.4)))
component_names(tree) # (:onset_admit, :admit_death)
event(tree, :onset_admit) # Gamma(2.0, 1.0)
params_table(tree) # a Tables.jl table of the free parametersThe leaf-wrapper contract
A leaf wrapper wraps one inner base distribution and stays transparent to the prior and parameter surface. The package reaches the inner leaf through two methods (public, not exported):
free_leaf(d)returns the free inner leaf (aDistribution), peeling any wrapping;rewrap_leaf(d, inner)reconstructs an equivalent wrapper around a new inner leaf.
Together they must round-trip: rewrap_leaf(d, free_leaf(d)) reproduces a node whose density matches d. A plain leaf is its own free leaf (free_leaf(d) == d, rewrap_leaf(d, inner) == inner); Truncated peels to its untruncated base and rebuilds the bounds; Shared peels through to its inner leaf and rebuilds the tie.
using ComposedDistributions, Distributions
import ComposedDistributions: free_leaf, rewrap_leaf
d = shared(:inc, Gamma(2.0, 1.0))
free_leaf(d) # Gamma(2.0, 1.0)
free_leaf(rewrap_leaf(d, Gamma(3.0, 1.5))) # Gamma(3.0, 1.5)Keeping the hierarchy honest
The reusable ComposedDistributions.TestUtils suite is the machine-checkable statement of these contracts, and the package runs it in test/interfaces.jl. test_interface runs the public checklist over the fixture set (example_fixtures); test_node_interface runs the node-extension checklist; test_composed_interface wraps both and asserts the AbstractComposedDistribution membership; and test_abstract_membership asserts the whole hierarchy (every composer under AbstractComposedDistribution, Sequential / Parallel under AbstractMultiChild, the one_of family under AbstractOneOf, Choose a sibling, and plain leaves and Shared standalone). Drop the same suite into your own tests to verify a custom leaf or composer conforms, and run it after adding a type to a family:
using ComposedDistributions.TestUtils: test_composed_interface, test_abstract_membership
test_abstract_membership()Conformance suite reference
The reusable suite lives in the ComposedDistributions.TestUtils submodule.
ComposedDistributions.TestUtils Module
ComposedDistributions.TestUtilsPublic interface-conformance harness for the composers.
TestUtils.test_interface(d) runs one interface checklist over a composed distribution (or a bare leaf), so a downstream author writing a new leaf or composer can drop it into their own @testset to verify conformance against the package's public interface. test_node_interface(node) is the companion check for a new composer node, asserting its child_nleaves / child_logpdf / child_rand! methods round-trip on a flat event vector. test_interface, example_fixtures, test_rejects_invalid, test_node_interface, test_composed_interface and test_abstract_membership are exported from this submodule.
The harness is dependency-light — Test (a stdlib), Tables, and the package's own public surface — and each check returns its @testset result so a caller can assert on it. The package runs the same checklist over its fixture set in test/interfaces.jl (see example_fixtures).
Examples
using ComposedDistributions, Distributions
using ComposedDistributions.TestUtils
tree = compose((onset_admit = Gamma(2.0, 1.0),
admit_death = LogNormal(0.5, 0.4)))
TestUtils.test_interface(tree)
nothing # hideExports
Imports
Base
ComposedDistributions.TestUtils.test_interface Function
Run the public interface-conformance checklist over a composed distribution.
test_interface(d; name) runs one @testset of interface assertions against d (a composed distribution or a bare leaf), so a downstream author writing a new leaf / composer can verify conformance by dropping it into their own tests.
The checklist asserts, where applicable to the node's shape:
a
rand(d)realisation is a scalar (univariate) or a labelled NamedTuple (multivariate composer);the overall
mean/var/stdare shaped as the fixture'soveralldeclares (a scalar for a univariate-collapsible node, a per-endpoint NamedTuple for aParallel);logpdfis finite on the supplied in-supportdraw;a univariate
cdfis monotone and in[0, 1];paramsworks andparams_tableis a Tables.jl table (Tables.istable(params_table(d)));event_names(flat) andevent_treeagree in leaf count;event(d, path...)round-trips the supplied known path;observed_distributioncollapses a chain to a univariate scalar.
Pass the fixture metadata (an example_fixtures entry, or the keyword arguments directly) so the harness knows the in-support draw, a known event path, a Choose kind, and the overall moment shape. Returns the @testset object.
Examples
using ComposedDistributions, Distributions
using ComposedDistributions.TestUtils: test_interface
d = compose((onset_admit = Gamma(2.0, 1.0), admit_death = LogNormal(0.5, 0.4)))
test_interface(d; draw = [1.5, 0.8], path = (:onset_admit,),
overall = :vector, has_endpoint = false)ComposedDistributions.TestUtils.test_composed_interface Function
Assert a composed distribution satisfies the AbstractComposedDistribution contract.
test_composed_interface(node; kwargs...) checks node subtypes AbstractComposedDistribution, exposes component_names, and passes both the node-extension checklist (test_node_interface) and the public interface checklist (test_interface; pass the same fixture keyword arguments). Returns the @testset object.
ComposedDistributions.TestUtils.test_node_interface Function
Assert a composer node satisfies the public node-extension contract.
test_node_interface(node) checks the three methods a new composer node implements (child_nleaves, child_logpdf, child_rand!) round-trip on a flat event vector, the same way the composers walk one. It asserts that
child_nleaves(node)is a positiveInt;child_rand!fills exactly the node'soffset + 1 : offset + nslot, leaving any padding either side untouched;child_logpdf(node, x, offset, n)is a finite scalar on that drawn vector and does not depend on the surrounding padding.
Pass offset and pad to place the node inside a wider vector, and rng for a reproducible draw. Returns the @testset object.
Examples
using ComposedDistributions, Distributions
using ComposedDistributions.TestUtils: test_node_interface
node = compose((onset_admit = Gamma(2.0, 1.0), admit_death = LogNormal(0.5, 0.4)))
test_node_interface(node)ComposedDistributions.TestUtils.test_abstract_membership Function
Assert the built-in composer types subtype the right family supertype.
test_abstract_membership() is the meta-test that the abstract hierarchy stays consistent: every composer node subtypes AbstractComposedDistribution, the two positional multi-child composers (Sequential / Parallel) subtype AbstractMultiChild, and the one_of family (Resolve / Compete) subtypes AbstractOneOf. Choose is a sibling, not a multi-child node. A type filed under the wrong family fails here. Returns the @testset object.
ComposedDistributions.TestUtils.test_rejects_invalid Function
Assert each composer rejects invalid construction in its inner constructor.
test_rejects_invalid() checks that the standard composers validate their structural invariants on every construction path, so a malformed node errors at build time rather than later: Sequential needs at least one component, Resolve at least two outcomes with branch probabilities in [0, 1], and Choose at least two alternatives with unique names. Returns the @testset object.
ComposedDistributions.TestUtils.test_ad_safety Function
Assert a parameterised log density differentiates under an injected AD backend.
test_ad_safety(f, θ; ad_gradient, name) evaluates ad_gradient(f, θ) (e.g. ForwardDiff.gradient) on a closure f(θ::Vector) -> Real reconstructing a distribution from its parameter vector and returning a scalar log density, and asserts the gradient is finite. Returns the @testset object.
ComposedDistributions.TestUtils.test_registry_coverage Function
Assert the fixture registry covers every public composer type.
test_registry_coverage(fixtures = example_fixtures()) checks that every type in registry_types appears (possibly nested) in at least one fixture, so a new public composer type added without a test_interface fixture fails here. The walk descends composer children. Returns the @testset object.
ComposedDistributions.TestUtils.registry_types Function
The public composer types the fixture registry must cover.
registry_types() returns the Vector of the package's own public composer types that test_interface is expected to exercise. test_registry_coverage asserts every entry appears in at least one example_fixtures fixture, so a new public composer type added without a fixture fails.
ComposedDistributions.TestUtils.example_fixtures Function
The example fixture set over every composer shape, for test_interface.
Returns a Vector of test_interface-ready fixtures covering the composer shapes (Sequential, Parallel, Resolve, Compete, choose), a bare leaf, a nested mix, and a deep-nesting matrix (a Sequential of Parallel, a Choose of Sequentials). test_registry_coverage asserts these cover every public composer type. The package runs the conformance checklist over these in test/interfaces.jl; a downstream author can read them as worked examples of the metadata test_interface expects (a draw, an event path, the overall moment shape, an ad probe).