Skip to content

feat: rand(d, n) batch sampler with a guaranteed scoring round trip (#276)#296

Open
seabbs-bot wants to merge 1 commit into
mainfrom
feat/276-batch-records
Open

feat: rand(d, n) batch sampler with a guaranteed scoring round trip (#276)#296
seabbs-bot wants to merge 1 commit into
mainfrom
feat/276-batch-records

Conversation

@seabbs-bot

Copy link
Copy Markdown
Collaborator

Closes #276.

What

rand(d, n) on a Sequential/Parallel composer now returns a Tables.jl
column table of n labelled records that scores straight back through
logpdf(d, table), closing the simulate/score round trip. Before this,
rand(tree, n) stack-overflowed (no batch method) and a Vector of records fed
to logpdf collided with the flat single-record method.

tree = compose((onset_fever = Gamma(2.0, 1.0), onset_rash = LogNormal(0.5, 0.4)))
logpdf(tree, rand(tree, 100))          # works, ≈ sum of per-row logpdf

DECISION for review: batch column layout (Sam can veto cheaply)

The issue says the batch "commits to ONE of the two existing layouts." The two
candidates:

  • value names (onset_fever, path_step_a, …) — one column per emitted
    leaf value, the layout rand(d) and logpdf(d, ::NamedTuple) already use for
    Sequential/Parallel.
  • event names (origin, targets…) — origin slot + one per edge, the layout
    the one_of nodes and the fitting/record path use.

I chose value names. Reasons: (1) logpdf(tree, rand(tree, n)) round-trips
by construction, which is the property users hit first; (2) standardising on
event-names would change Sequential/Parallel's released single-record
convention — breakage nobody asked for; (3) per-composer batch layouts differing
(one_of stays event-name keyed) simply mirrors their single-record conventions
already differing, which is the self-consistent place to be.

Event-name escape hatch — no third path added. The event-name view stays
reachable through the existing event_names/event_tree (schema) and, for a
record's absolute positions, event_times/event_increments (the transforms
just merged in #290). I deliberately did not add a new value→event record
converter: a general per-record key remap isn't 1:1 for arity-mismatched trees,
so it would be an ill-defined third path. Happy to add a scoped one if you want
it with defined semantics.

Dispatch safety

The vector-of-records logpdf is defined on the concrete Sequential and
Parallel types (not a Union) with an AbstractVector{<:NamedTuple} element
type, so it is strictly more specific than the flat logpdf(d, ::AbstractVector)
method and cannot shadow it for scalar-valued trees. Aqua's ambiguity check
passes. There is a regression test for that exact collision, both directions
(a flat [1.0, 2.0] stays flat; a Vector of records is a batch).

Tests

test/composers/batch_records.jl (4 @testitems): column-table round trip for
Sequential/Parallel/mixed, a nested-resolve tree, value-name columns + rng
reproducibility, and the shadowing regression. task test-fast and
task test-quality pass.

Files

New src/composers/batch_records.jl; a 3-line Tables.istable branch added to
logpdf(d, ::NamedTuple) in named_outputs.jl (mirrors Choose's table path).

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

…276)

`rand(d, n)` draws n labelled records from a Sequential/Parallel composer into a
Tables.jl column table that scores straight back through `logpdf(d, table)`,
closing the simulate/score round trip for the multi-child composers. Previously
`rand(tree, n)` stack-overflowed (no batch method) and a Vector of value-name
records fed to `logpdf` collided with the flat single-record method.

The batch commits to the VALUE-name column layout the single-record
`rand(d)`/`logpdf(d, ::NamedTuple)` already use, so the round trip holds by
construction. `logpdf(d, ::NamedTuple)` gains a Tables.istable branch (mirroring
Choose) to score a column table per row; a vector-of-records `logpdf` dispatches
on the concrete Sequential/Parallel types (not a Union) so it stays strictly
more specific than the flat single-record method and never shadows it. The
event-name view stays reachable through event_names/event_tree; a record's
absolute positions through event_times — no third conversion path added.

Co-authored-by: Sam Abbott <contact@samabbott.co.uk>
@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/PR296/

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

@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/276-batch-records")
using ComposedDistributions

Option 2: Local Checkout

If you have the repo locally:

git checkout feat/276-batch-records
julia --project=. -e "using Pkg; Pkg.instantiate()"

@codecov

codecov Bot commented Jul 21, 2026

Copy link
Copy Markdown

Codecov Report

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

Files with missing lines Patch % Lines
src/composers/batch_records.jl 75.00% 2 Missing ⚠️
Flag Coverage Δ
ad-enzyme-forward 0.00% <0.00%> (ø)
ad-enzyme-reverse 24.51% <0.00%> (-0.88%) ⬇️
ad-forwarddiff 22.77% <0.00%> (-0.82%) ⬇️
ad-mooncake-forward 0.00% <0.00%> (ø)
ad-mooncake-reverse 22.81% <0.00%> (-0.82%) ⬇️
ad-reversediff 22.77% <0.00%> (-0.82%) ⬇️
unit 86.60% <77.77%> (+0.46%) ⬆️

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

Files with missing lines Coverage Δ
src/composers/named_outputs.jl 89.70% <100.00%> (+1.82%) ⬆️
src/composers/batch_records.jl 75.00% <75.00%> (ø)

... and 4 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

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 · 1 4 11
ForwardDiff · · · · · 2 5
ReverseDiff (tape) · · · · · 7 ·
Mooncake reverse · · · · · 4 3
Enzyme reverse · · · · · 1 6
Evaluation — 17 benchmarks (by time change)
Benchmark base PR time memory
Composition / Nested / rand 3.04 μs 7.62 μs 🔴 2.51× ⚪ 1.0×
Composition / Choose / construct 17.0 ns 30.0 ns 🔴 1.76×
Composition / Compete / construct 17.0 ns 30.0 ns 🔴 1.76×
Composition / Sequential / rand 779.0 ns 1.33 μs 🔴 1.71× ⚪ 1.0×
Composition / Compete / logccdf 206.0 ns 350.0 ns 🔴 1.7×
Composition / Nested / compose 1.86 μs 3.07 μs 🔴 1.65× ⚪ 1.0×
Composition / Nested / logpdf 926.0 ns 1.51 μs 🔴 1.63× ⚪ 1.0×
Composition / Parallel / logpdf 68.0 ns 110.0 ns 🔴 1.62× ⚪ 1.0×
Composition / Resolve / construct 19.0 ns 30.0 ns 🔴 1.58×
Composition / Sequential / logpdf 70.0 ns 110.0 ns 🔴 1.57× ⚪ 1.0×
Composition / Resolve / logpdf 93.0 ns 140.0 ns 🔴 1.51×
Composition / Compete / rand 308.0 ns 461.0 ns 🔴 1.5× ⚪ 1.0×
Composition / Resolve / rand 310.0 ns 451.0 ns 🔴 1.45× ⚪ 1.0×
Composition / Choose / logpdf 43.0 ns 60.0 ns 🔴 1.4×
Composition / Parallel / rand 994.0 ns 1.29 μs 🔴 1.3× ⚪ 1.0×
Composition / Sequential / construct 1.27 μs 1.49 μs 🔴 1.17× ⚪ 1.0×
Composition / Parallel / construct 1.73 μs 1.55 μs 🟢 0.9× ⚪ 1.0×
AD gradients — 28 benchmarks (by time change)
Benchmark base PR time memory
AD gradients / Resolve mixture marginal logpdf / ForwardDiff 4.08 μs 7.34 μs 🔴 1.8× ⚪ 1.0×
AD gradients / Compete racing-hazard marginal logpdf / ForwardDiff 4.36 μs 7.65 μs 🔴 1.75× ⚪ 1.0×
AD gradients / Compete racing-hazard marginal logpdf / Enzyme reverse 5.75 μs 9.9 μs 🔴 1.72× ⚪ 1.0×
AD gradients / Pool non-centred reconstruction logpdf / Enzyme reverse 1.26 μs 2.08 μs 🔴 1.66× ⚪ 1.0×
AD gradients / Pool non-centred reconstruction logpdf / ForwardDiff 606.0 ns 1.0 μs 🔴 1.65× ⚪ 1.0×
AD gradients / Resolve stick-breaking branch-prob logpdf / ForwardDiff 4.22 μs 6.93 μs 🔴 1.64× ⚪ 1.0×
AD gradients / Choose selected-branch logpdf / ForwardDiff 431.0 ns 701.0 ns 🔴 1.63× ⚪ 1.0×
AD gradients / Sequential Gamma+LogNormal logpdf / Mooncake reverse 181.26 μs 288.4 μs 🔴 1.59× ⚪ 1.0×
AD gradients / Resolve stick-breaking branch-prob logpdf / Mooncake reverse 56.89 μs 90.39 μs 🔴 1.59× ⚪ 1.0×
AD gradients / Resolve mixture marginal logpdf / Enzyme reverse 13.7 μs 21.73 μs 🔴 1.59× ⚪ 1.0×
AD gradients / Resolve stick-breaking branch-prob logpdf / Enzyme reverse 13.79 μs 21.82 μs 🔴 1.58× ⚪ 1.0×
AD gradients / Resolve mixture marginal logpdf / Mooncake reverse 55.87 μs 87.96 μs 🔴 1.57× ⚪ 1.0×
AD gradients / Censored leaf marginal logpdf / Enzyme reverse 106.84 μs 162.6 μs 🔴 1.52× ⚪ 1.0×
AD gradients / Sequential Gamma+LogNormal logpdf / Enzyme reverse 66.71 μs 101.19 μs 🔴 1.52× ⚪ 1.0×
AD gradients / Censored leaf marginal logpdf / ForwardDiff 7.41 μs 11.11 μs 🔴 1.5× ⚪ 1.0×
AD gradients / Censored leaf marginal logpdf / Mooncake reverse 240.29 μs 359.62 μs 🔴 1.5× ⚪ 1.0×
AD gradients / Pool non-centred reconstruction logpdf / Mooncake reverse 15.04 μs 22.5 μs 🔴 1.5× ⚪ 1.0×
AD gradients / Compete racing-hazard marginal logpdf / Mooncake reverse 34.65 μs 51.15 μs 🔴 1.48× ⚪ 1.0×
AD gradients / Choose selected-branch logpdf / ReverseDiff (tape) 9.2 μs 13.49 μs 🔴 1.47× ⚪ 1.0×
AD gradients / Resolve mixture marginal logpdf / ReverseDiff (tape) 41.52 μs 60.58 μs 🔴 1.46× ⚪ 1.0×
AD gradients / Compete racing-hazard marginal logpdf / ReverseDiff (tape) 32.93 μs 47.73 μs 🔴 1.45× ⚪ 1.0×
AD gradients / Resolve stick-breaking branch-prob logpdf / ReverseDiff (tape) 36.65 μs 52.7 μs 🔴 1.44× ⚪ 1.0×
AD gradients / Pool non-centred reconstruction logpdf / ReverseDiff (tape) 17.26 μs 24.8 μs 🔴 1.44× ⚪ 1.0×
AD gradients / Choose selected-branch logpdf / Enzyme reverse 2.26 μs 3.22 μs 🔴 1.43× ⚪ 1.0×
AD gradients / Sequential Gamma+LogNormal logpdf / ReverseDiff (tape) 18.99 μs 26.76 μs 🔴 1.41× ⚪ 1.0×
AD gradients / Censored leaf marginal logpdf / ReverseDiff (tape) 19.31 μs 26.96 μs 🔴 1.4× ⚪ 1.0×
AD gradients / Sequential Gamma+LogNormal logpdf / ForwardDiff 7.48 μs 10.33 μs 🔴 1.38× ⚪ 1.0×
AD gradients / Choose selected-branch logpdf / Mooncake reverse 13.31 μs 17.16 μs 🔴 1.29× ⚪ 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.

Batch table sampler with a guaranteed scoring round trip

1 participant