Release notes
ComposedDistributions.jl follows a hybrid changelog approach:
GitHub Releases: All releases (automatically generated by TagBot)
NEWS.md: Major releases requiring detailed context (shown below)
See GitHub Releases for minor/patch release information.
Unreleased
breaking: removed the
ComposedDistributionsFlexiChainsExtweakdep extension and itschain_to_params/param_draws/strip_prefix/update(template, chain)surface (#221). DistributionsInference.jl already hosts a generic, tested replacement (readback/readback_draws, built on its own fit-protocol extension) that round-trips a composed tree — pooled, shared-tag, or Dirichlet-branch_probs— through a real chain with no ComposedDistributions-specific code; this package carrying its own 388-line parallel tree-walk duplicated that machinery rather than adding anything. Drops theFlexiChainsandDynamicPPLweakdeps entirely (neither has any other user left in this package). UseDistributionsInference.readback/readback_drawsinstead; see the fitting guide.breaking:
updateis nowpublic, notexported (#221). Several ecosystem packages (and plenty outside it) have their ownupdate-shaped verb; exporting a name this generic risked the same ambiguous-binding clash #233 hit withas_turingwhen two packages both export a same-named generic function. Reach it asComposedDistributions.updateor withusing ComposedDistributions: update.test: three new AD gradient scenarios close coverage gaps in the
ADFixturesregistry (test/ADFixtures/src/ADFixtures.jl): aShared-tagged uncertain leaf driven through the fulllogdensitycodec (tag-dedup's reverse-mode gradient accumulation was untested), aTruncated-wrapped uncertain leaf through the #216 leaf-wrapper registry's codec path, and aCensoredleaf marginal (#215 landed with value-level tests only for both wrappers). Also ledgers a real, currently-CI-red regression: Enzyme reverse has crashed with an internal LLVM compiler error (EnzymeInternalErrorinnodecayed_phis!) on the "Uncertain-leaf logdensity codec" scenario since #190, reproducing on every CI run but not locally — declared broken inbackend_broken_scenarios()so CI reflects the real state instead of failing outright (#223).docs: corrected
docs/benchmarks.md's claim that the performance- history timeline updates "on every push tomainand on tagged releases" (#231) —benchmark-history.yamlis currently parked toworkflow_dispatch-only pending #41 (an unregistered, chained dependency the kit's benchmark scratch-registry bootstrap does not yet resolve), so the automatic trigger the page described is not actually running.docs/benchmarks_notes.md(the file that exists for exactly this kind of note) now explains the parked trigger and links #41, so a reader seeing "not enough comparable revisions to compute ratios yet" understands why rather than concluding the page is broken.breaking: removed the
ComposedDistributionsLogDensityProblemsExtweakdep extension and theas_turing/ComposedDistributionsDynamicPPLExtsurface (#220, #233). Both duplicated machinery DistributionsInference.jl already provides generically over any fit-protocol object (as_logdensityimplementingLogDensityProblemsdirectly;as_turingbuilding the DynamicPPL model), via itsComposedDistributionsfit-protocol extension —export as_turingalso collided with DistributionsInference's own exportedas_turingwhen both packages were loaded together. UseDistributionsInference.as_logdensity/as_turinginstead; this package's own Turing-freeComposedLogDensity/as_logdensity/logdensitycore is unchanged (stillpublic, not exported) and is what DistributionsInference builds on.breaking: stopped re-exporting ConvolvedDistributions' surface (
convolved,product/Product,discretise_pmf,DelayPMF,AnalyticalSolver,NumericSolver,GaussLegendre,AbstractSolverMethod,integrate,gl_integrate; #228) — a caller now reaches these with its ownusing ConvolvedDistributions.convolve_series/difference/Convolved/Differencestay reachable: this package extends/constructs them itself for composed tree types.breaking:
convolve_series(chain, series)(and theResolve/Competemarginal form) no longer discretises a continuous observed delay for you with an implicit interval-censored-secondary scheme; it collapses the tree and delegates straight toConvolvedDistributions.convolve_series, which throws for a continuous delay and asks you to discretise first (#226) — the same contract a bare distribution already has under ConvolvedDistributions 0.2. Discretise explicitly withConvolvedDistributions.discretise_pmf(delay, maxlag)(or a CensoredDistributions.jl double-interval-censored PMF for a day-binned primary) and pass the result toconvolve_series(pmf, series). Theintervalkeyword is dropped from the chain-argument methods to match. A replacement composed-chain convenience is tracked as a CensoredDistributions.jl extension (EpiAware/CensoredDistributions.jl#886) rather than carried here, since the discretisation scheme is a censoring choice.feat:
register_leaf_wrapper!is a new public hook so a leaf-wrapper package extension (ModifiedDistributions'Affine/Weighted/Transformed/Modified) can tell the generated flat-vector codec (flat_dimension,unflatten,flatten,reconstruct) how to peel its wrapper types and what extra parameters they own, without adding a direct dispatch method to_leaf_free_type/_extra_names_of— the codec's@generatedfunctions cannot reliably see such a method if it is added after the generator has already compiled, a Julia@generated-function semantics gap confirmed by direct experiment (#188/#189). The hook takes plain data (a type-parameter index and a fixed extra-names tuple), never a callable, since even a stored closure hits the same world-age wall when called from a generator. A core (in-module) leaf wrapper (Truncated,Distributions.Censored) keeps its own direct-dispatch method but now routes its recursion through the same registry-aware resolver, so a core wrapper placed directly around a registered extension leaf (e.g.truncated(thin(Gamma(...)))) peels correctly too, not just the reverse nesting.chore: removed the
design/folder (#224) — its one note, the time-/covariate-varying design rationale, is superseded by the landedVarying/instantiateimplementation and its docstrings and the Time-, strata-, and covariate-varying distributions guide; dangling pointers to the removed file insrc/ComposedDistributions.jl,src/composers/varying.jl, and that guide are cleaned up alongside it.chore: renamed
src/composers/hazard_one_of.jltosrc/composers/Compete.jl(#230), matching the file-per-type convention the other composers already follow (Resolve.jl,Sequential.jl, ...); no code changes, only the file name and its in-comment references.chore: renamed the public
_param_names/_leaf_ctorleaf-protocol hooks toparam_names/leaf_ctor(#229) — a leading underscore reads as "internal" in Julia, the opposite of whatpublic.jlwas declaring, anddocs/src/developer/leaf-protocol.mdalready documented the clean names. The old underscored names remain asconstaliases (the same function object, following the existinguncertain_specs/_uncertain_specspattern), so an existing override such asComposedDistributions._param_names(::MyLeaf) = ...keeps working.test: added a guard against
params_table/codec ordering drift (#192, the #190 review follow-up): the runtimeparams_tablewalk and the generatedunflatten/flattencodec are two independent, hand-maintained implementations of the same walk-order and dedup rule, andext/ComposedDistributionsDynamicPPLExt.jlassumes their orderings coincide with nothing checking it. A new consistency test (test/composers/codec_consistency.jl) covers every composer/leaf shape; a newas_turing/NUTS round trip on ashared(...)-tagged tree (test/composers/turing_ext.jl) covers the one path that actually depends on the coupling. Full unification into one shared walker was assessed and set aside as disproportionate at this point (see the test file's header for the reasoning);_param_names/_param_names_of, the other duplicated piece the review flagged, turned out to serve genuinely different purposes (a public, instance-dispatched extension point vs. an internal, type-dispatched table the generated codec needs) and are not safely mergeable, so they get a direct comparison test instead.breaking: the
ComposedDistributionsLoweredDistributionsExtextension and theLoweredDistributionsweakdep are removed (LD#51, the #22 hub-owned decision).LoweredDistributionsnow hosts thelowerbridge for the composer types itself, in its ownLoweredDistributionsComposedDistributionsExt(moved verbatim from this package). Anyone who imported the extension module directly from this package must loadLoweredDistributionsand rely on its extension instead; functionality is otherwise unchanged when both packages are loaded together. This is the last source-bridge weakdep this package carries — the remainingBijectors/DynamicPPL/FlexiChains/LogDensityProblems/Mooncakeinference extensions are separately staged to move out to DistributionsInference.jl (#185).chore: dropped the root
[sources]pins forConvolvedDistributionsandEpiAwareADToolsnow that both are registered in General (ConvolvedDistributions 0.2.0, EpiAwareADTools 0.1.0) — both root dependencies resolve from the registry again, so this package is registrable (#41).feat:
reconstruct(d, x)is a new generated primary for the flat-vector codec — a composed distribution rebuilt straight from an estimated flat vector in one call, with no intermediateDict-typed accumulation (#178, PR 2 of the type-domain codec design).unflatten/flatten/flat_dimensionare now themselves@generated: each walks the tree's TYPE once per distinct shape (names, tags and groups are compile-time since PR 1) and emits the slot indices as literals, producing a concretely-typed (@inferred-stable) nestedNamedTupleinstead of the old runtimeDict{Symbol, Any}walk.logdensitynow routes through the generatedunflatten, and this fixes #162: Enzyme reverse could not compile the old walk's type-unstable, heap-boxed reconstruction, and now differentiates it like every other backend. Measured on four baseline tree shapes (a plain chain, a tag shared across branches, a non-centred pooling group, and a Resolve stick-breaking node), the generated codec is 4.7-11.9x faster and allocates 4-14x less than the two-stepupdate(d, unflatten(d, x))it replaces on the hot path; first-call compile latency for a fresh pooled+Resolve tree shape is unchanged (~0.6-0.7s, dominated byDistributions/Dirichletcompilation, not the generated function itself). The modifier-leaf codec path (ModifiedDistributions'Affine/Weighted/Transformed/Modified, e.g.thin(...)) is deferred to PR 4 (#189): a@generatedfunction's generator can compile against a world snapshot taken before a later-loaded package extension adds its type-level leaf- protocol methods, a genuine Julia semantics gap around generated functions and extensions, not a bug in this PR's scope.breaking: the
ComposedDistributionsModifiedDistributionsExtreverse extension and theModifiedDistributionsweakdep are removed (#170 step 2), ending the extension cycle between the two packages (Julia 1.12 fails on cross-module method overwrites when both packages' extensions activate together). ModifiedDistributions' ownModifiedDistributionsComposedDistributionsExtnow hosts the full leaf protocol for its modifier leaves (Affine/Weighted/Transformed/Modified), reading it through the leaf-protocol public API published in #174. Anyone who imported the extension module directly from this package must load ModifiedDistributions and rely on its extension instead; functionality is otherwise unchanged when both packages are loaded together.refactor!: the composer/wrapper structs carry their layout-affecting names and tags in TYPE parameters rather than runtime fields, lifting
Sequential/Parallel/Choose/Resolve/Compete's outcome/step/branch names,Shared's tag, andPool's group/non-centred flag into the type domain (part of #178, PR 1 of the type-domain codec design). The public constructors (Sequential(components, names),shared(tag, dist),pool(group, population; noncentred), ...) are UNCHANGED, and every consumer that already read through the accessors (component_names/shared_tag, plus the newpool_group/pool_noncentred) needs no change. This is breaking only for code that constructed these structs directly and read the moved fields by name (e.g.d.names,shared_leaf.tag,pool_spec.group); such code should switch to the accessors. This groundwork is a step towards a generated, allocation-free flat-vector codec that fixes #162.fix:
as_logdensityand the chain readback (chain_to_params/update(template, chain)/param_draws) now reject a tree where apoolgroup, asharedtag, and a root-level edge name are not disjoint (#177). All three land in the same root-lifted NamedTuple namespace at readback, so a same-named pair previously clobbered each other silently instead of erroring; the collision is caught once at construction or readback entry, not on the gradient hot path. Reusing one tag to tie a parameter across branches, or one group across several pooled members, is unaffected.feat:
update(d, x::AbstractVector)is a flat-vector shorthand forupdate(d, unflatten(d, x)), collapsing an uncertain tree at a sampler draw in a single call (#178).feat: the composer leaf protocol is now published as considered public API (#170). The downstream contract a leaf-wrapper package implements is de-underscored and documented as the stable surface:
uncertain_specs,shared_tag,leaf_param_names,leaf_mean,leaf_varandleaf_detail_linesjoin the already-publicfree_leaf/rewrap_leaf/component_names/param_names/leaf_ctor(the underscored spellings stay as internal aliases, so nothing breaks). The scalarthin-factor hook is generalised to a map-basedextra_leaf_params/set_extra_leaf_paramsprotocol, each extra parameter carrying its own value and support, which also fixes a latentBoundsErrorwhen a leaf carries an extra parameter alongside an uncertain native one. A new developer page documents the protocol. This release is non-breaking; the removal of the ModifiedDistributions reverse extension and its weakdep (ending the MD-CD extension cycle) follows in a later release.fix:
minimumandmaximumon aParallelnow return a per-branch NamedTuple of support bounds, matching howmean/varreport per-endpoint moments, and the other composed types raise a clearArgumentErrornaming the generic instead of the opaqueMethodErroraboutiterate. Also tightens theParallelevent_namestest so it genuinely asserts the event tuple.feat:
@eventsdeclares an event-tree TOPOLOGY as a readable operator diagram, structure only with no distributions attached (#156).→(\to) chains events into aSequential,|branches into a one_of outcome,&runs branches inParallel, and parentheses group for precedence; a bare identifier is an event name and becomes a named hole.update(skeleton; name = dist, ...)fills the holes and builds the concrete composed tree through the existing verbs, so one delay topology is reused across pathogens or settings. Whether a|node becomes a fixed-probabilityResolveor a racing-hazardCompeteis decided at fill time by the fill value type ((dist, prob)tuples versus bare distributions, the last branch alone free to take the residual), so|stays one syntax. A fill value is any valid leaf, including anuncertain/@uncertainleaf or a ModifiedDistributions modifier leaf, which composes through the existing extension with no MD-specific code in@events. The fill validates that every hole is filled and no unknown key is passed.docs: a "Fitting a composed distribution" guide walks the inference tooling in one place: the
as_logdensitylog-density over a tree's estimated parameters, sampling it through theLogDensityProblemsinterface with LogDensityProblemsAD, sampling with Turing throughas_turing, and reading a fitted chain back onto the tree withchain_to_params/update.feat:
@uncertain exprreads a distribution-valued constructor argument as that parameter's prior, so an uncertain composed tree reads naturally (#155). It rewrites syntax only, leaving the type sorting to the runtime positionaluncertain(D, args...)method: each callD(pos_args...)whose head is a distribution type and one of whose positional arguments is a distribution literal becomesuncertain(D, pos_args...), so@uncertain Gamma(Normal(0.7, 0.2), 1.0)makesshapeuncertain and fixesscale. The walk is recursive, so it composes withcompose, the verbs and the ModifiedDistributions wrappers (a modifier wraps the rewritten uncertain leaf); an all-literal constructor and a keyword-carrying or qualified call are left unchanged. PureExprrewriting, so no new dependency.test: extend the AD gradient scenarios (
test/ADFixtures) across the ForwardDiff / ReverseDiff / Enzyme / Mooncake matrix. A new:latentcategory differentiates the fullas_logdensity/logdensitycodec over an uncertain-leaf tree (the flat-vector to nested-NamedTupleunflatten/updatepath) and a centred pool (the_pool_centred_logpriorpopulation term), and the:marginalgroup gains aChoosescored at a selected alternative. The centred pool and theChoosedifferentiate on all four backends; the uncertain-leaf codec differentiates on ForwardDiff, ReverseDiff and Mooncake reverse but is marked broken on Enzyme reverse, which cannot compile its mixed fixed/active heap reconstruction (an opaque-pointer LLVM error).feat:
as_turing(dist, data; prefix, loglik)builds aDynamicPPLmodel over a composed distribution's estimated parameters, so a composed posterior is sampleable withsample(as_turing(dist, data), NUTS(), ...)(#9). It is a light wrapper on theas_logdensitycodec. Each estimated parameter is a named~site drawn from its own prior and the data likelihood is added with@addlogprob!from the codec's reconstruction, so the model's total log-density equalslogdensity(as_logdensity(dist, data), x)by construction. The~site names match the inference readback exactly (d.onset_admit.shape, an uncertain node'sd.<edge>.branch_probs.stick_k, a shared leaf once under its tag), so a fitted chain reads back throughchain_to_params/update(dist, chain)unchanged. Supported rows carry a concrete prior (ordinary uncertain leaves and stick-breaking branch probabilities); a pooled tree is rejected for now (a centred pool has no fixed prior, and the readback does not yet consume a pooled chain), with a pointer to theas_logdensity+ LogDensityProblemsAD path. The model lives in a newComposedDistributionsDynamicPPLExtextension triggered byDynamicPPLalone.feat: a
LogDensityProblemsweak-dependency extension exposes aComposedLogDensity(fromas_logdensity) as a standardLogDensityProblemsproblem, so a composed distribution's posterior over its estimated parameters is sampleable by any LogDensityProblems consumer (AdvancedHMC, DynamicHMC, Pathfinder, Turing'sexternalsampler) with gradients supplied by LogDensityProblemsAD (#13). The extension implementsdimension(the estimated flat-parameter count),logdensity(the codec's evaluator), and the zeroth-ordercapabilities. This is the Turing-free inference substrate that complements the DynamicPPL path, and it needs no new hard dependency.feat: a
LoweredDistributionsweak-dependency extension lowers a composed distribution to a backend-agnostic dynamical-systems representation, solower(compose(...))yields a phase-type or continuous-time Markov chain for the whole delay structure that a Catalyst / ODE / Petri / Jump backend can consume (#149). The scalar composers lower exactly, since composition of phase-types is closed.Sequentialconvolves its steps into a series phase-type,Resolvemixes its outcomes into a hyper-phase-type weighted by the branch probabilities,Competeraces its causes through the competing-risks Kronecker sum, andSharedlowers its wrapped leaf. The vector composers lower to a jointCTMC,Parallelthrough the Kronecker sum of its independent branches andChoosethrough the block-diagonal union of its selector alternatives. Nesting aParallelorChooseinside a scalar composer raises a clear error rather than a silent misrepresentation.fix:
logdensity/unflatten(src/composers/logdensity.jl) now differentiate under Mooncake, both forward and reverse.unflattencalls theSymbolpath-splitter_split_edgeunconditionally on every row, and the codec's length guards build theirDimensionMismatchmessage by interpolating the tree object; both recurse into Base's UTF-8 string-indexing continuation machinery, for which Mooncake's whole-program rule derivation has no rule (asub_ptrintrinsic hit), on every call for_split_edge, and on any reachable branch for the guards' messages, regardless of whether it is taken._split_edgeand the fourDimensionMismatch-throwing call sites (logdensity.jl,named_outputs.jl,Parallel.jl,Sequential.jl) are shielded from Mooncake with@zero_derivativeinComposedDistributionsMooncakeExt(#146).feat:
composegains a varargs-pairs spelling,compose(:a => d1, :b => d2, ...)(#145), a thin convenience over the primaryNamedTupleform so call sites migrating from CensoredDistributions' pairs-basedcomposekeep working unmodified. See the FAQ for the migration note.feat:
_uncertain_specsand_leaf_detail_linesare nowpublic(#142), sanctioning the leaf-introspection recursion a leaf-wrapper package extends alongsidefree_leaf/rewrap_leaf. Without extending_uncertain_specs, an uncertain prior attached to a wrapped leaf was silently dropped bybuild_priors; without_leaf_detail_lines, a wrapped leaf'sinspectdetail fell back to its raw struct dump.test: pin three seams now that ConvolvedDistributions 0.2 is adopted: a Modified-wrapped chain step (affine / weight / thin) lowering through
observed_distribution/convolve_series(#117); the recurrent-operator seam end to end (a time-varying chain resolved per step, collapsed, discretised, and driving the vector convolution surface, plus one hand-rolled renewal step, refs #82); and the restored realcdfassertion on adifferenceof two chains that the #122 workaround stood in for while ConvolvedDistributions #45 was open (#137).feat:
to_constrained(prob, z)completes the PPL-neutral codec's HMC surface: given an assembledComposedLogDensityand an unconstrained flat vector, it returns the constrained ESTIMATED parameters and the log-determinant Jacobian a sampler needs (logdensity(prob, x) + logjacis the unconstrained-space target). The transform is built per row from each row's prior viaBijectors.bijector(a stick-breakingBetarow, a positive-support prior, a non-centred pooled latent/hyperparameter), or, for a centred-pooled row, from its population's family. It lives in a newComposedDistributionsBijectorsExtweakdep extension, so the core codec stays free of aBijectorsdependency.Breaking (upstream-driven): adopt the ConvolvedDistributions AD-seam move (#137): ConvolvedDistributions 0.2 relocated its AD-safe hook family out to the new
EpiAwareADToolspackage under underscore-free names, so the racing-hazard node now calls and extendsEpiAwareADTools.logccdf_ad_safe/ccdf_ad_safe(wasConvolvedDistributions._logccdf_ad_safe/._ccdf_ad_safe).EpiAwareADToolsis a new dependency; it is unregistered, so it is git-pinned in the root and isolated (test/ad,test/jet,benchmark) environments until it registers. No user-facing API change.Breaking (upstream-driven): adopt ConvolvedDistributions 0.2, which makes the bare-distribution
convolve_series(delay, series)discrete-only — a continuous delay now throws, because discretising it is an explicit modelling choice (single- vs double-interval censoring) upstream will not make silently (ConvolvedDistributions #31/#47). The composed-tree convenience is preserved:convolve_series(::Sequential, series; events)andconvolve_series(::Resolve/::Compete, series)collapse to their continuous observed total and discretise it for you with the interval-censored-secondary scheme (discretise_pmfover lags0:(length(series) - 1)) before convolving, so the composed output is unchanged from before. For day-binned (double-interval-censored) primaries, discretise the total yourself and pass the PMF toconvolve_series(pmf, series).discretise_pmfandDelayPMFare now re-exported. Compat bumped to0.2; because 0.2 is unregistered the source is git-pinned (re-adding what #107 removed) until it registers.feat: re-export ConvolvedDistributions 0.2's Mellin product family (the
productconstructor forZ = X * Y), so the convolution surface is complete through ComposedDistributions alone. Theproductconstructor is exported; theProducttype stays unexported (a bareProductwould clash with Distributions' deprecatedProduct) but is public and reachable asComposedDistributions.Product, mirroring ConvolvedDistributions. Composing aProductleaf into a tree is not yet wired (#139).fix:
probs/occurrence_probabilityon a racing-hazard (Compete) node no longer return winning probabilities that sum slightly above one. The per-cause split is mathematically sub-stochastic (sums to1 - ∏ S_k(∞) ≤ 1), but Gauss-Legendre quadrature could overshoot to e.g. 1.0000322 for proper causes; the split is now rescaled to a valid probability vector when it exceeds one, leaving a genuine sub-one defective deficit intact (#115).refactor: the racing-hazard (
Compete) moment, winning-probability and cause-cdf quadratures now call the publicintegrate(::GaussLegendre, f, lo, hi)rather than reaching into ConvolvedDistributions' internalGaussLegendre(; n).ruleandgl_integrate. Results are identical (the same fixed 64-node rule); this drops the coupling to an unexported upstream type that could change without a breaking bump (#109).refactor: renamed the internal
src/composers/intervene.jltostructural_edits.jl, naming theupdate/prune/spliceverbs it holds (theinterveneverb is gone from the public API). No user-facing change (#114).test: end-to-end continuous delay-stack scenarios. A committed
test/composers/stack_scenarios.jltestset drives a handful of named, epi-flavoured continuous stacks (an onset→admission→deathSequentialchain, aParallelof independent reporting branches, a death-vs-dischargeResolve, a competing-causesCompete, nested composes mixing them, a renewalconvolve_series, atiegroup and adifference) through the whole verb surface together — construction via both spellings,rand/logpdfround-trip, a seeded large-N Monte-Carlo moment check against the analytic / quadrature values, the introspection / edit / prior surface, and a ForwardDiff gradient per stack.Overall moments of a composed tree now honour an
affinemodifier: a chain with anaffine(delay; scale, shift)step reports the scale/shift-adjusted mean/var (matching whatranddraws) instead of peeling the affine off to the inner delay's moment. A hazard-modified (Modified) leaf has no analytic moment yet, so a chain containing one now errors informatively rather than silently returning the unmodified free-leaf moment, pending ModifiedDistributions#44's numeric cumulative-hazard path (#120).Breaking (upstream-driven): following ConvolvedDistributions' rename, the re-exported
convolve_distributionsis split into two verbs —convolved(dists...; method)for the distribution form (the sumX + Y, a chain's observed total) andconvolve_series(delay, series; interval)for the timeseries form (convolving a numeric series through a delay). The composed-tree methods follow suit:convolved(::Sequential)collapses a chain to its total, andconvolve_series(::Sequential, series; events)drives the renewal / latent series. No alias is kept (the package is unreleased).sequential,parallel,resolve,competeandchoosenow accept a positionalNamedTuplespelling (resolve((death = (Gamma(1.5, 1.0), 0.3), disch = Gamma(2.0, 1.5)))) as the equivalent of thename => valuePairs, for hand-written children;choose'sselectorstays a keyword. The one_of constructors also build their outcome tuples withmaprather than a generator comprehension, so constructing aResolve/Competeinside a differentiated function is Enzyme-safe (nocollect_to!``Arraytemporary Enzyme's type analysis rejects).Behaviour change: the standalone
(name, time)view of a one_of draw is now a keyword onrand,rand(node; outcome = true), rather than the separate publicrand_outcomeverb (now removed). A keyword-freerand(node)is unchanged (it still returns the full named event record), so only call sites that reached for the compact pair need updating fromComposedDistributions.rand_outcome(rng, node)torand(rng, node; outcome = true).Breaking:
randof a standaloneResolveorCompetenode now returns the named event record of the outcome that fired — aNamedTuplekeyed byevent_names(node)(a positional origin slot then one slot per outcome, the fired outcome's time present and the othersmissing) — instead of the scalar marginal time-to-resolution (#96, syncing to CensoredDistributions' #639). The record names which outcome occurred, sologpdf(node, rand(node))round-trips and identifies the outcome. To recover the old scalar draw, sample the marginalrand(as_mixture(node)); the(outcome, time)pair view isrand(node; outcome = true). A one_of node nested inside aSequential/Parallelis unchanged (it stays one scalar value slot, its marginal); a newlogpdf(node, ::NamedTuple)scores a standalone record.params_tableis now a superset schema carrying both the uncertain-firstpriorcolumn and CensoredDistributions':thinrows via the_thin_factor/_set_thin_factorhooks (no-op here, so no:thinrow appears; the hooks let a thinning modifier layer plug in)._leaf_detail_linesbecomes the per-leafinspectdetail extension point, and the racing-hazard moment / winning-probability / cause-cdf quadratures thread a shared 64-node Gauss-Legendre rule (#96).Partial pooling across strata (#78). A new
pool(group, population)spec, placed insideuncertainwhere a prior would go, declares a parameter partially pooled across the leaves that name the same group: each member's parameter is drawn from one sharedpopulationdistribution whose own free parameters are the estimated hyperparameters (carrying their priors through the ordinaryuncertainspec machinery). It is the middle of the pooling spectrum betweenshared/tie(complete pooling, one value everywhere) and independentuncertainspecs (no pooling, K unlinked values). A location-scale population (Normal/LogNormal) is reparameterised non-centred (oneNormal(0, 1)latent per member, memberkreconstructed asmu + tau*z_korexp(mu + tau*z_k)), keeping the CensoredDistributions-compatible[hyper..., z...]flat layout; a general population takes the centred path (each member's parameter scored directly against the population). The hyperparameters flatten as ordinary uncertain-spec rows on the population.Node-level uncertainty: a
Resolve's branch probabilities can now be estimated (#89). Attach a simplex-valuedDistributions.Dirichletprior withupdate(node, (branch_probs = Dirichlet(α),)). TheDirichletis what you write; the node is estimated through its K-1 stick-breaking coordinates (:stick_1 … :stick_{K-1}, each aBetain (0, 1)), soparams_table, the uncertain-first codec (flatten/unflatten/flat_dimension/as_logdensity) and chain readback all carry the sticks, and the probabilities are recovered from any draw (they always sum to one and the gradient is well-defined on every AD backend). Promote (update(tree, param_priors(tree))) attaches a flatDirichlet(ones(K))perResolve.Compete's winning probability is derived from the hazards andChoose's alternative is data-selected, so neither has a node-level free parameter (documented, no change).convolve_series(chain, series; events)convolves a timeseries to a named INTERIM event of aSequentialchain, not just its endpoint. The cumulative delay to an event is the observed collapse of the chain prefix up to it, so a single event name returns that event's count series and a tuple or vector of names returns aNamedTupleof series (the endpoint reproduces the whole-chain result). Only a plain continuous chain (every step a delay leaf) has per-event cumulative delays; a branching step is rejected, and an unknown event name errors listing the valid events. The discrete-event and thinning/branch-probability variants stay in CensoredDistributions.See-through fitting of
Convolved/Differenceleaf component parameters, replacing the previous fixed-composite treatment.params_tablenow inventories each component's scalar parameters under acomponent_ipath segment (e.g.total.component_1.shape), andupdaterebuilds the composite from the updated components (preserving the solver method). A component may be madeuncertainin place, so the uncertain-first codec (flatten/unflatten/flat_dimension/as_logdensity) estimates a spec'd component parameter like any other leaf parameter. The composite joins the shared_node_children/_rebuilddeferred-leaf walk, sohas_uncertain,has_varyingandinstantiateall see through a composite carrying an uncertain or varying component; it stays a single flat scored slot and an atomic node to the structural edits.Reconciled the
Varying/instantiateseam with theUncertainmachinery (#47):VaryingandUncertainare now presented as the two cases of one deferred leaf concept — a leaf that maps to a distribution and resolves later,Varyingfrom an observed covariate (viainstantiate) andUncertainfrom a latent parameter draw (viarand/update).instantiatenow rebuilds through the shared_node_children/_rebuildreconstruction machinery thatupdateand the structural edits already use, and thehas_varying/has_uncertainguards share one node walk, so resolution is no longer a separate hand-rolled tree traversal. No user-facing API change;instantiate,update,has_varying/has_uncertain, and the codec's rejection of an un-instantiatedVaryingleaf are unchanged.
0.1.0 — initial release
The generic composition algebra ported from CensoredDistributions.jl:
composeand the five composers (Sequential,Parallel,Resolve,Compete,Choose),shared/tie, structural edits (update/prune/splice), introspection (params_table,build_priors,event/event_names/event_tree), moments, and the convolution bridge (observed_distribution,convolved).Added
Varying/Context/instantiate: leaves whose distribution depends on an observed covariate (time, stratum), resolved byinstantiate(tree, ctx);has_varying(tree)guards fitting loops.Resolve/Competeoutcome probabilities read viaDistributions.probs(following the CensoredDistributions rename).Docs site (overview, concepts, three tutorials, FAQ, interface contracts), benchmarks (core + AD) with a docs page, and a Mooncake AD extension.
Typed composer hierarchy:
AbstractComposedDistribution{F, S}roots the composers, withAbstractMultiChild{S}groupingSequential/ParallelandAbstractOneOfthe univariate one_of family; downstream extension packages dispatch on these supertypes. The reusableComposedDistributions.TestUtilsharness (test_interface,test_composed_interface,test_node_interface,test_abstract_membership, ...) verifies a custom leaf or composer conforms to the interface.Added
uncertain/Uncertain: leaf distributions whose parameters are themselves distributions, nestable to any depth.randdraws the marginal so uncertain leaves compose everywhere, and the rest of the univariate surface (scalarlogpdf/cdf/..., the moments, including a composed tree's overall moment) delegates to the template's central values.has_uncertain(tree)flags a tree that still holds an uncertain leaf, for a scoring/fitting loop to guard against a forgotten collapse. Collapse an uncertain leaf to its concrete template by pinning the parameters withupdate(tree, params). Build one with a concrete template (uncertain(Gamma(2.0, 1.0); shape = LogNormal(...))), a positional family form (uncertain(Gamma, LogNormal(...), 1.0)), or the keyword family form.truncated(uncertain(...))pushes inside the template (conditional per-draw semantics).params_tablegained apriorcolumn carrying an uncertain parameter's spec, whichbuild_priorsnow uses ahead of its per-row default.Extended the ConvolvedDistributions verbs to composed trees:
convolve_series(chain, series)convolves a timeseries (e.g. expected infections) through aSequentialchain's observed total delay (the renewal / latent observation layer), anddifference(a, b)forms the difference of two chains' observed totals; aParallel/Choose(no single observed delay) errors with guidance. AConvolved/Differencenode used as a leaf inside a tree scores, samples, and reports moments as a plain univariate leaf, and is treated as fixed structure byparams_table/build_priors/update(fit its components by composing them as explicit chain steps).Added the PPL-neutral LogDensityProblems core codec: the flat-vector <-> nested-NamedTuple bijection (
flatten/unflatten/flat_dimension) and the assembledComposedLogDensity(as_logdensity/logdensity), with no DynamicPPL / Turing dependency. AForwardDiffgradient flows throughlogdensity.Added the inference-readback verbs
chain_to_params/param_draws/strip_prefix(andupdate(template, chain)): read a fitted chain's parameters back onto a composed-distribution template. Turing-free until bothDynamicPPLandFlexiChainsare loaded, when the extension supplies the methods.Uncertain-first estimation: the
uncertainspecs set the estimation boundary.flatten/unflatten/flat_dimension/as_logdensity/logdensitytarget EXACTLY the spec'd parameters, so a fixed leaf contributes no estimated dimension and a tree with no uncertain leaves estimates nothing (flat_dimension == 0;logdensityis then the data likelihood at the fixed tree).updateintroduces uncertainty through the prior interface: a distribution in a parameter slot makes just that parameter uncertain (a partial update), andupdate(tree, param_priors(tree))promotes a whole tree to uncertainty over its free parameters with default priors (the explicit estimate-everything path).params_table/build_priorsare derived views over the object's specs; the readback verbs label exactly the spec'd parameters.
This file tracks notes for major releases and significant milestones; GitHub Releases (auto-generated from merged PRs) cover every release in between.