Skip to content

feat: elapsed_between distribution accessor over a named chain (#274)#295

Open
seabbs-bot wants to merge 1 commit into
mainfrom
feat/cd-274-elapsed-between
Open

feat: elapsed_between distribution accessor over a named chain (#274)#295
seabbs-bot wants to merge 1 commit into
mainfrom
feat/cd-274-elapsed-between

Conversation

@seabbs-bot

Copy link
Copy Markdown
Collaborator

Closes #274.

What

elapsed_between(chain, from, to) returns the LAW of the elapsed distance between two named events of a Sequential chain, as an ordinary univariate distribution — the convolution of the steps strictly between the two events. elapsed_between(chain, to) is the origin-to-to form (the convolution of the chain prefix up to to).

chain = sequential(:origin_onset => Gamma(2.0, 1.0),
    :onset_admit => LogNormal(0.5, 0.4),
    :admit_exit => Gamma(1.5, 1.0))

elapsed_between(chain, :onset, :admit)   # the single onset_admit step
law = elapsed_between(chain, :admit)     # origin -> admit, a Convolved
rand(law); logpdf(law, 3.2)              # a scoreable univariate law

This is the distribution-level counterpart to the sample-level difference of two event positions, and deliberately not difference: two events on one chain descend from a shared origin, so their absolute positions are not independent and difference(chain, chain) would double-count the shared leading steps. elapsed_between convolves only the intervening steps.

Where / fences

  • New accessor in a new file src/composers/event_intervals.jl, built on the public convolved verb and the chain's event_names/observed leaves. It does not edit convolved_interop.jl or anything discretise_pmf-adjacent.
  • Convolved#85 caveat: tests use continuous components only. Discrete-component behaviour follows whatever Sam rules on Convolved#85 for convolved's value support — this PR encodes no assumption about it.
  • Mirror-not-move: the event → leaf-prefix mapping mirrors the cumulative-prefix logic in convolved_interop.jl's _event_prefix_delay rather than calling it, so the accessor does not couple to the gated file. If the two later diverge, that is convolution-layer migration cleanup (Convolved#85).

Errors (all clear ArgumentErrors)

Branching chain (events don't line up with delay steps), out-of-order or equal from/to, an unknown event name, an uncertain-leaf chain (pin first), and a non-chain operand (Parallel/Choose/bare distribution) each error with a message naming the way forward. The cross-branch/data-selected cases the issue flags are covered by the Parallel/Choose guards.

Verification

Two new testitems (test/composers/event_intervals.jl), continuous components only. Full non-AD suite green (1554/1554).

This was opened by a bot. Please ping @seabbs for any questions.

Add elapsed_between(chain, from, to) and the origin-to-`to` form
elapsed_between(chain, to): the LAW of the elapsed distance between two
named events of a Sequential chain, as an ordinary univariate
distribution (the convolution of the steps strictly between the two
events). This is the distribution-level counterpart to the sample-level
difference of two event positions, and deliberately NOT difference: two
events on one chain descend from a shared origin, so difference(chain,
chain) would double-count the shared leading steps.

New accessor in src/composers/event_intervals.jl, built on the public
convolved verb and the chain's event_names/observed leaves; it does not
touch the convolution-layer internals. The event -> leaf-prefix mapping
mirrors (not calls) _event_prefix_delay so the accessor does not couple
to convolved_interop.jl.

A branching chain, an out-of-order or unknown event pair, an
uncertain-leaf chain, or a non-chain operand is rejected with a clear
error. Tests use continuous components only.

Co-authored-by: Sam Abbott <contact@samabbott.co.uk>
@github-actions

Copy link
Copy Markdown
Contributor

Try this Pull Request!

Option 1: Julia Package Manager

Open Julia and type:

import Pkg
Pkg.activate(temp=true)
Pkg.add(url="https://github.com/EpiAware/ComposedDistributions.jl", rev="feat/cd-274-elapsed-between")
using ComposedDistributions

Option 2: Local Checkout

If you have the repo locally:

git checkout feat/cd-274-elapsed-between
julia --project=. -e "using Pkg; Pkg.instantiate()"

@codecov

codecov Bot commented Jul 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.66667% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/composers/event_intervals.jl 91.66% 2 Missing ⚠️
Flag Coverage Δ
ad-enzyme-forward 0.00% <0.00%> (ø)
ad-enzyme-reverse 24.93% <0.00%> (-0.46%) ⬇️
ad-forwarddiff 23.16% <0.00%> (-0.43%) ⬇️
ad-mooncake-forward 0.00% <0.00%> (ø)
ad-mooncake-reverse 23.21% <0.00%> (-0.43%) ⬇️
ad-reversediff 23.16% <0.00%> (-0.43%) ⬇️
unit 86.38% <91.66%> (+0.24%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/composers/event_intervals.jl 91.66% <91.66%> (ø)

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

Copy link
Copy Markdown
Contributor

📖 Documentation preview is ready!

View the docs for this PR at: https://EpiAware.github.io/ComposedDistributions.jl/previews/PR295/

This preview will be updated automatically when you push new commits.

@github-actions

Copy link
Copy Markdown
Contributor

Benchmark comparison vs base

Minimum time per call. Buckets are PR time as a % of base, so lower is faster (🟢 faster, ⚪ within 5%, 🔴 slower). Counts of benchmarks per bucket:

Group 🟢 <50% 🟢 50–75% 🟢 75–95% ⚪ 95–105% 🔴 105–125% 🔴 125–150% 🔴 >150%
Evaluation · 1 2 3 7 2 2
ForwardDiff · · 1 5 1 · ·
ReverseDiff (tape) · · 1 6 · · ·
Mooncake reverse · · 7 · · · ·
Enzyme reverse · · 2 5 · · ·
Evaluation — 17 benchmarks (by time change)
Benchmark base PR time memory
Composition / Choose / construct 20.0 ns 30.0 ns 🔴 1.5×
Composition / Compete / construct 20.0 ns 30.0 ns 🔴 1.5×
Composition / Parallel / construct 1.88 μs 2.7 μs 🔴 1.44× ⚪ 1.0×
Composition / Sequential / rand 1.43 μs 1.87 μs 🔴 1.31× ⚪ 1.0×
Composition / Nested / rand 6.85 μs 4.92 μs 🟢 0.72× ⚪ 1.0×
Composition / Choose / logpdf 49.0 ns 60.0 ns 🔴 1.22×
Composition / Sequential / construct 1.46 μs 1.72 μs 🔴 1.18× ⚪ 1.0×
Composition / Parallel / rand 1.68 μs 1.9 μs 🔴 1.13× ⚪ 1.0×
Composition / Parallel / logpdf 100.0 ns 110.0 ns 🔴 1.1× ⚪ 1.0×
Composition / Sequential / logpdf 100.0 ns 110.0 ns 🔴 1.1× ⚪ 1.0×
Composition / Resolve / rand 500.0 ns 450.0 ns 🟢 0.9× ⚪ 1.0×
Composition / Compete / logccdf 320.0 ns 350.0 ns 🔴 1.09×
Composition / Compete / rand 491.0 ns 451.0 ns 🟢 0.92× ⚪ 1.0×
Composition / Nested / compose 2.96 μs 3.16 μs 🔴 1.07× ⚪ 1.0×
Composition / Resolve / construct 29.0 ns 30.0 ns ⚪ 1.03×
Composition / Nested / logpdf 1.59 μs 1.57 μs ⚪ 0.99× ⚪ 1.0×
Composition / Resolve / logpdf 140.0 ns 140.0 ns ⚪ 1.0×
AD gradients — 28 benchmarks (by time change)
Benchmark base PR time memory
AD gradients / Censored leaf marginal logpdf / Mooncake reverse 467.28 μs 359.86 μs 🟢 0.77× ⚪ 1.0×
AD gradients / Sequential Gamma+LogNormal logpdf / Mooncake reverse 386.36 μs 299.28 μs 🟢 0.77× ⚪ 1.0×
AD gradients / Censored leaf marginal logpdf / Enzyme reverse 181.02 μs 144.15 μs 🟢 0.8× ⚪ 1.0×
AD gradients / Pool non-centred reconstruction logpdf / ForwardDiff 1.13 μs 981.0 ns 🟢 0.87× ⚪ 1.0×
AD gradients / Pool non-centred reconstruction logpdf / Mooncake reverse 24.55 μs 21.91 μs 🟢 0.89× ⚪ 1.0×
AD gradients / Compete racing-hazard marginal logpdf / Mooncake reverse 56.01 μs 50.2 μs 🟢 0.9× ⚪ 1.0×
AD gradients / Sequential Gamma+LogNormal logpdf / ForwardDiff 10.45 μs 11.39 μs 🔴 1.09× ⚪ 1.0×
AD gradients / Resolve stick-breaking branch-prob logpdf / Mooncake reverse 97.93 μs 90.18 μs 🟢 0.92× ⚪ 1.0×
AD gradients / Resolve mixture marginal logpdf / Mooncake reverse 94.0 μs 87.34 μs 🟢 0.93× ⚪ 1.0×
AD gradients / Choose selected-branch logpdf / Mooncake reverse 18.82 μs 17.54 μs 🟢 0.93× ⚪ 1.0×
AD gradients / Sequential Gamma+LogNormal logpdf / Enzyme reverse 109.19 μs 103.06 μs 🟢 0.94× ⚪ 1.0×
AD gradients / Pool non-centred reconstruction logpdf / ReverseDiff (tape) 25.97 μs 24.57 μs 🟢 0.95× ⚪ 1.0×
AD gradients / Compete racing-hazard marginal logpdf / ReverseDiff (tape) 49.87 μs 47.54 μs ⚪ 0.95× ⚪ 1.0×
AD gradients / Choose selected-branch logpdf / ForwardDiff 681.0 ns 711.0 ns ⚪ 1.04× ⚪ 1.0×
AD gradients / Resolve stick-breaking branch-prob logpdf / ReverseDiff (tape) 54.77 μs 52.77 μs ⚪ 0.96× ⚪ 1.0×
AD gradients / Choose selected-branch logpdf / ReverseDiff (tape) 13.93 μs 13.42 μs ⚪ 0.96× ⚪ 1.0×
AD gradients / Censored leaf marginal logpdf / ForwardDiff 12.04 μs 11.62 μs ⚪ 0.96× ⚪ 1.0×
AD gradients / Resolve mixture marginal logpdf / ReverseDiff (tape) 63.12 μs 61.4 μs ⚪ 0.97× ⚪ 1.0×
AD gradients / Resolve stick-breaking branch-prob logpdf / ForwardDiff 6.87 μs 6.69 μs ⚪ 0.97× ⚪ 1.0×
AD gradients / Pool non-centred reconstruction logpdf / Enzyme reverse 1.98 μs 2.02 μs ⚪ 1.02× ⚪ 1.0×
AD gradients / Choose selected-branch logpdf / Enzyme reverse 3.41 μs 3.36 μs ⚪ 0.99× ⚪ 1.0×
AD gradients / Resolve mixture marginal logpdf / ForwardDiff 6.81 μs 6.72 μs ⚪ 0.99× ⚪ 1.0×
AD gradients / Sequential Gamma+LogNormal logpdf / ReverseDiff (tape) 27.64 μs 27.99 μs ⚪ 1.01× ⚪ 1.0×
AD gradients / Compete racing-hazard marginal logpdf / Enzyme reverse 9.89 μs 9.76 μs ⚪ 0.99× ⚪ 1.0×
AD gradients / Resolve stick-breaking branch-prob logpdf / Enzyme reverse 21.09 μs 21.31 μs ⚪ 1.01× ⚪ 1.0×
AD gradients / Censored leaf marginal logpdf / ReverseDiff (tape) 29.15 μs 28.9 μs ⚪ 0.99× ⚪ 1.0×
AD gradients / Compete racing-hazard marginal logpdf / ForwardDiff 7.7 μs 7.75 μs ⚪ 1.01× ⚪ 1.0×
AD gradients / Resolve mixture marginal logpdf / Enzyme reverse 20.97 μs 21.02 μs ⚪ 1.0× ⚪ 1.0×

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Distribution of elapsed distance between two named events

1 participant