Skip to content

fix: quantile-panelled quadrature for Compete's wide-window accuracy (#294)#298

Open
seabbs-bot wants to merge 1 commit into
mainfrom
fix/cd-294-quad-accuracy
Open

fix: quantile-panelled quadrature for Compete's wide-window accuracy (#294)#298
seabbs-bot wants to merge 1 commit into
mainfrom
fix/cd-294-quad-accuracy

Conversation

@seabbs-bot

Copy link
Copy Markdown
Collaborator

Summary

  • probs/mean/var on a racing-hazard (Compete) node integrated over
    the full shared quadrature window with a single fixed 64-node
    Gauss-Legendre rule. Once that window is wide (a genuinely heavy-tailed
    cause with a finite but huge quantile, e.g. Pareto(0.5, 1.0); or an
    ordinary composite cause whose moment-based fallback window is itself
    large), the fixed nodes spread across the whole domain and starve the
    region where the mass actually sits, returning a badly wrong split (off
    by 100%, potentially naming the wrong winning cause) while still looking
    plausible (finite, in [0, 1], summing to <= 1).
  • The quadrature now splits the window at each cause's own quantile (or,
    lacking a quantile method, mean/std) markers and integrates panel-wise,
    so node density follows the mass regardless of which cause's tail set
    the window (_hazard_panelled_integrate) — mirroring the
    quantile-panelled approach ConvolvedDistributions already uses
    internally for its own analogous convolution quadrature (those helpers
    are that package's internals, not public, so this reimplements the same
    idea directly against the public GaussLegendre/integrate API, and
    pools breaks from every cause since a race's mass can concentrate around
    any cause's own scale, not just the one whose quantile set the window).
  • _HazardCauseDelay's cdf (the forward convolve_series stream)
    benefits the same way, since its integration bound can be arbitrarily
    far out for a heavy-tailed node.
  • probs computes the panel breaks once per node and reuses them across
    every cause's integral, rather than recomputing the same ladder per
    cause.

Closes #294.

Verification note

Independently checked against QuadGK's adaptive quadrature during
implementation (not a project dependency, used only to validate): the new
split matches to ~10 significant figures for the Pareto case and closely
for the skewed-composite case. This also showed that #288's own
"converged" reference for the Pareto case (a single 65536-node rule over
the raw ~1e8 window) was itself not reliably converged — the affine map
onto such a wide interval is ill-conditioned regardless of node count
(checked up to n = 1_048_576, still drifting). The test's reference for
that case now integrates over a smaller, well-conditioned truncated
domain instead, justified because the race's winning-probability integral
is bounded by whichever cause's own survival is smallest: once the faster
cause has essentially resolved, the joint survival is already negligible,
well before the heavy-tailed cause's own extreme quantiles.

Test plan

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

…294)

probs/mean/var on a racing-hazard (Compete) node integrated over the full
shared quadrature window with a single fixed 64-node Gauss-Legendre rule.
Once that window is wide (a heavy-tailed cause with a finite but huge
quantile, e.g. Pareto(0.5, 1.0); or an ordinary composite cause whose
moment-based fallback window is itself large), the fixed nodes spread
across the whole domain and starve the region where the mass actually
sits, returning a badly wrong split (off by 100%, potentially naming the
wrong winning cause) while still looking plausible.

The quadrature now splits the window at each cause's own quantile (or,
lacking a quantile method, mean/std) markers and integrates panel-wise,
so node density follows the mass regardless of which cause's tail set the
window -- mirroring the quantile-panelled approach ConvolvedDistributions
already uses internally for its own analogous convolution quadrature.
_HazardCauseDelay's cdf (the forward convolve stream) benefits the same
way, since its integration bound can be arbitrarily far out for a
heavy-tailed node.

Verified against QuadGK's adaptive quadrature during implementation: the
new split matches to ~10 significant figures for the Pareto case and
closely for the skewed-composite case, confirming the previous PR's own
"converged" reference (a single 65536-node rule over the raw window) was
itself not reliably converged for the most extreme case -- the affine map
onto such a wide interval is ill-conditioned regardless of node count.

Flips the four @test_broken assertions from #288's delta round to real,
passing assertions, replacing the Pareto case's reference with one
integrated over a smaller, well-conditioned truncated domain (justified:
the race resolves once the faster cause's own survival is negligible,
well before the heavy-tailed cause's extreme quantiles).

Closes #294.
@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="fix/cd-294-quad-accuracy")
using ComposedDistributions

Option 2: Local Checkout

If you have the repo locally:

git checkout fix/cd-294-quad-accuracy
julia --project=. -e "using Pkg; Pkg.instantiate()"

@codecov

codecov Bot commented Jul 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.11765% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/composers/Compete.jl 94.11% 3 Missing ⚠️
Flag Coverage Δ
ad-enzyme-forward 0.00% <0.00%> (ø)
ad-enzyme-reverse 24.21% <0.00%> (-1.18%) ⬇️
ad-forwarddiff 22.50% <0.00%> (-1.09%) ⬇️
ad-mooncake-forward 0.00% <0.00%> (ø)
ad-mooncake-reverse 22.54% <0.00%> (-1.10%) ⬇️
ad-reversediff 22.50% <0.00%> (-1.09%) ⬇️
unit 86.82% <96.00%> (+0.69%) ⬆️

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

Files with missing lines Coverage Δ
src/composers/Compete.jl 82.68% <94.11%> (+5.82%) ⬆️

... 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/PR298/

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 · 7 8 1 1 · ·
ForwardDiff · 4 3 · · · ·
ReverseDiff (tape) · · 7 · · · ·
Mooncake reverse · 5 2 · · · ·
Enzyme reverse · 2 5 · · · ·
Evaluation — 17 benchmarks (by time change)
Benchmark base PR time memory
Composition / Choose / construct 30.0 ns 20.0 ns 🟢 0.67×
Composition / Compete / construct 30.0 ns 20.0 ns 🟢 0.67×
Composition / Sequential / rand 1.72 μs 1.19 μs 🟢 0.69× ⚪ 1.0×
Composition / Resolve / construct 30.0 ns 21.0 ns 🟢 0.7×
Composition / Compete / logccdf 350.0 ns 255.0 ns 🟢 0.73×
Composition / Sequential / logpdf 110.0 ns 82.0 ns 🟢 0.75× ⚪ 1.0×
Composition / Parallel / rand 1.72 μs 1.29 μs 🟢 0.75× ⚪ 1.0×
Composition / Resolve / rand 490.0 ns 372.0 ns 🟢 0.76× ⚪ 1.0×
Composition / Nested / logpdf 1.51 μs 1.15 μs 🟢 0.76× ⚪ 1.0×
Composition / Compete / rand 491.0 ns 376.0 ns 🟢 0.77× ⚪ 1.0×
Composition / Resolve / logpdf 140.0 ns 109.0 ns 🟢 0.78×
Composition / Sequential / construct 1.94 μs 1.52 μs 🟢 0.78× ⚪ 1.0×
Composition / Nested / compose 3.28 μs 3.99 μs 🔴 1.22× ⚪ 1.0×
Composition / Parallel / logpdf 110.0 ns 89.0 ns 🟢 0.81× ⚪ 1.0×
Composition / Choose / logpdf 60.0 ns 50.0 ns 🟢 0.83×
Composition / Nested / rand 4.58 μs 4.2 μs 🟢 0.92× ⚪ 1.0×
Composition / Parallel / construct 2.14 μs 2.07 μs ⚪ 0.97× ⚪ 1.0×
AD gradients — 28 benchmarks (by time change)
Benchmark base PR time memory
AD gradients / Compete racing-hazard marginal logpdf / ForwardDiff 7.86 μs 5.31 μs 🟢 0.68× ⚪ 1.0×
AD gradients / Compete racing-hazard marginal logpdf / Enzyme reverse 9.95 μs 6.96 μs 🟢 0.7× ⚪ 1.0×
AD gradients / Compete racing-hazard marginal logpdf / Mooncake reverse 54.6 μs 38.75 μs 🟢 0.71× ⚪ 1.0×
AD gradients / Censored leaf marginal logpdf / Mooncake reverse 337.56 μs 241.82 μs 🟢 0.72× ⚪ 1.0×
AD gradients / Pool non-centred reconstruction logpdf / Enzyme reverse 2.06 μs 1.48 μs 🟢 0.72× ⚪ 1.0×
AD gradients / Resolve mixture marginal logpdf / ForwardDiff 7.03 μs 5.06 μs 🟢 0.72× ⚪ 1.0×
AD gradients / Sequential Gamma+LogNormal logpdf / Mooncake reverse 293.3 μs 211.19 μs 🟢 0.72× ⚪ 1.0×
AD gradients / Pool non-centred reconstruction logpdf / ForwardDiff 1.09 μs 795.0 ns 🟢 0.73× ⚪ 1.0×
AD gradients / Resolve stick-breaking branch-prob logpdf / Mooncake reverse 90.51 μs 66.11 μs 🟢 0.73× ⚪ 1.0×
AD gradients / Resolve mixture marginal logpdf / Mooncake reverse 87.99 μs 65.26 μs 🟢 0.74× ⚪ 1.0×
AD gradients / Choose selected-branch logpdf / ForwardDiff 681.0 ns 506.0 ns 🟢 0.74× ⚪ 1.0×
AD gradients / Resolve mixture marginal logpdf / Enzyme reverse 20.56 μs 15.8 μs 🟢 0.77× ⚪ 1.0×
AD gradients / Resolve stick-breaking branch-prob logpdf / Enzyme reverse 20.65 μs 16.12 μs 🟢 0.78× ⚪ 1.0×
AD gradients / Sequential Gamma+LogNormal logpdf / Enzyme reverse 102.25 μs 80.38 μs 🟢 0.79× ⚪ 1.0×
AD gradients / Resolve stick-breaking branch-prob logpdf / ForwardDiff 7.0 μs 5.51 μs 🟢 0.79× ⚪ 1.0×
AD gradients / Compete racing-hazard marginal logpdf / ReverseDiff (tape) 48.54 μs 38.91 μs 🟢 0.8× ⚪ 1.0×
AD gradients / Choose selected-branch logpdf / Mooncake reverse 16.76 μs 13.73 μs 🟢 0.82× ⚪ 1.0×
AD gradients / Pool non-centred reconstruction logpdf / ReverseDiff (tape) 25.87 μs 21.27 μs 🟢 0.82× ⚪ 1.0×
AD gradients / Pool non-centred reconstruction logpdf / Mooncake reverse 20.61 μs 16.96 μs 🟢 0.82× ⚪ 1.0×
AD gradients / Resolve stick-breaking branch-prob logpdf / ReverseDiff (tape) 52.74 μs 43.93 μs 🟢 0.83× ⚪ 1.0×
AD gradients / Censored leaf marginal logpdf / ForwardDiff 12.32 μs 10.32 μs 🟢 0.84× ⚪ 1.0×
AD gradients / Choose selected-branch logpdf / ReverseDiff (tape) 13.37 μs 11.21 μs 🟢 0.84× ⚪ 1.0×
AD gradients / Resolve mixture marginal logpdf / ReverseDiff (tape) 59.81 μs 50.34 μs 🟢 0.84× ⚪ 1.0×
AD gradients / Sequential Gamma+LogNormal logpdf / ReverseDiff (tape) 27.96 μs 23.6 μs 🟢 0.84× ⚪ 1.0×
AD gradients / Choose selected-branch logpdf / Enzyme reverse 3.22 μs 2.79 μs 🟢 0.86× ⚪ 1.0×
AD gradients / Sequential Gamma+LogNormal logpdf / ForwardDiff 11.27 μs 9.76 μs 🟢 0.87× ⚪ 1.0×
AD gradients / Censored leaf marginal logpdf / ReverseDiff (tape) 28.01 μs 24.7 μs 🟢 0.88× ⚪ 1.0×
AD gradients / Censored leaf marginal logpdf / Enzyme reverse 130.72 μs 123.81 μs 🟢 0.95× ⚪ 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.

Compete's shared fixed-node quadrature is catastrophically wrong (not just imprecise) for a wide window

1 participant