Skip to content

feat: add piecewise-constant hazard multiplier and gate - #110

Open
seabbs-bot wants to merge 1 commit into
mainfrom
feat/piecewise-hazard-105
Open

feat: add piecewise-constant hazard multiplier and gate#110
seabbs-bot wants to merge 1 commit into
mainfrom
feat/piecewise-hazard-105

Conversation

@seabbs-bot

@seabbs-bot seabbs-bot commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Adds piecewise_effect(; breaks, multipliers): a piecewise-constant
    proportional-hazards multiplier for modify under the log link, with
    a closed-form cumulative hazard (a sum of base log-survival
    differences over segments) — no quadrature backend needed, unlike a
    callable effect(t), which always falls to the numeric path. Segment
    i applies on [breaks[i-1], breaks[i]) (right-continuous: a
    breakpoint value belongs to the segment it opens, matching the
    issue's own t < 1.0 ? ... : ... illustrative convention).
  • Adds gate(dist, opens, closes): the finite-window special case
    (multipliers 0/1/0 at the two breakpoints) — hazard off before
    opens, the base's own hazard inside the window, off again after
    closes. This deliberately produces a sub-stochastic law; its
    residual "never occurred" mass reads through total_mass/
    is_defective (Queryable sub-stochastic laws: total_mass and is_defective #107) exactly like any other defective wrapper, per
    the issue's ask.
  • quantile/rand use the same monotone-bisection pattern already
    established for the identity/numeric paths (no new numeric idiom).
  • Accumulator element types promote every contributing type (query,
    support minimum, multipliers, breakpoints, base survival) together,
    so a Dual/tracked multiplier or breakpoint differentiates cleanly
    without narrowing back to Float64 mid-loop.

Stacking / history

This was originally stacked on feat/total-mass-defective-107
(PR #108), since gate's defective-law behaviour builds directly on
that PR's total_mass/is_defective accessor rather than duplicating
it. #108 has since merged, so this PR has been rebased onto main
and re-targeted at it: the diff is now just the piecewise/gate additions
on top of the merged #107/#108 accessors. A stale NEWS.md bullet from
the original branch was dropped during the rebase — main now tracks
release notes via GitHub Releases, so merged-PR bodies auto-document
the change.

Scope notes

  • mean/var are not defined for the piecewise path, matching the
    existing precedent for the numeric/callable-effect path (also
    undefined) — no analytic partial-moment machinery exists generically
    in this package yet.
  • Not wired into test/ad/ADFixtures (the six-backend AD sweep
    registry); gradient correctness is verified directly via targeted
    ForwardDiff tests in this PR instead. Could be added as a
    follow-up if the maintainers want the full backend matrix on this
    surface too.
  • MD#104 (horizon-cap modifier, blocked on the CD#252 epic) was kept
    out of scope — piecewise_effect's machinery isn't a drop-in for
    that issue (it needs the CD#252 epic's tree-level defective-law
    primitives, not just a modifier-level closed form), so nothing here
    builds ahead of it.

Closes #105.

Test plan

  • task test-fast — 2309/2309 pass (post-rebase against main):
    piecewise_effect validation, log-link-only enforcement, closed form
    checked against a brute-force trapezoidal cumulative hazard, the
    right-continuous segment-boundary convention, pdf/cdf/ccdf
    consistency and properness, quantile/rand round-trip, gate's
    defective-law behaviour (survival flat before opens and after
    closes, total_mass/is_defective, a beyond-mass quantile
    throwing).
  • task test-quality — Aqua/formatting/JET/doctests pass (116/116).
  • task test-ad — all AD backends pass (1386/1386).

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

@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/ModifiedDistributions.jl", rev="feat/piecewise-hazard-105")
using ModifiedDistributions

Option 2: Local Checkout

If you have the repo locally:

git checkout feat/piecewise-hazard-105
julia --project=. -e "using Pkg; Pkg.instantiate()"

@github-actions

Copy link
Copy Markdown
Contributor

📖 Documentation preview is ready!

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

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

@codecov

codecov Bot commented Jul 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.55072% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/Modified.jl 98.55% 1 Missing ⚠️
Flag Coverage Δ
ad-enzyme-forward 24.07% <1.44%> (-3.07%) ⬇️
ad-enzyme-reverse 24.07% <1.44%> (-3.07%) ⬇️
ad-forwarddiff 14.41% <1.44%> (-1.76%) ⬇️
ad-mooncake-forward 14.41% <1.44%> (-1.76%) ⬇️
ad-mooncake-reverse 14.41% <1.44%> (-1.76%) ⬇️
ad-reversediff 14.41% <1.44%> (-1.76%) ⬇️
unit 97.72% <100.00%> (+0.30%) ⬆️

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

Files with missing lines Coverage Δ
src/Modified.jl 94.17% <98.55%> (+1.22%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@seabbs-bot

Copy link
Copy Markdown
Collaborator Author

Triage: blocked behind #108, which is itself blocked on a maintainer ruling. Please do not merge yet; nothing here needs changing.

Two separate blocks:

  1. Base branch. baseRefName is feat/total-mass-defective-107 (feat: add total_mass/is_defective accessors for sub-stochastic laws #108), not main, so this cannot merge until feat: add total_mass/is_defective accessors for sub-stochastic laws #108 does. I have deliberately not rebased it — moving it would presuppose feat: add total_mass/is_defective accessors for sub-stochastic laws #108's outcome.
  2. The defect convention. This is not just a stacking convenience. The diff defines total_mass(d::_PiecewiseModified) extending feat: add total_mass/is_defective accessors for sub-stochastic laws #108's generic, and states gate's entire contract in terms of total_mass/is_defective. So it inherits the unmade ruling on what a defective law is and which package owns the accessor (see my note on feat: add total_mass/is_defective accessors for sub-stochastic laws #108 for the dependency-graph argument).

One point specifically for that ruling rather than for this PR's author: #105 asks for a gated leaf to "emit the same sub-stochastic marginal type the algebra defines for defective laws rather than a private flat-survival object". gate here returns an ordinary Modified{<:UnivariateDistribution{Continuous}, <:PiecewiseEffect, typeof(LogLink)} whose total_mass is below one — no distinct type. That is a reasonable reading, and it may well be the right answer, but it is an answer to the open "what is a defective law" question rather than an implementation of a settled one.

The piecewise_effect half looks separable from all of this: its closed-form cumulative hazard has no defect coupling. If the ruling takes a while, splitting piecewise_effect onto main and leaving gate here would unblock most of the value. That is a maintainer's call, not a change I have made.

CI note: the only failing required-ish check was downgrade-compat, which main's kit sync (#122) has since fixed — a rebase will clear it. test (pre, *) is continue-on-error by design in the org's reusable tests.yml and fails on every open PR here, including the dependabot bump; it does not gate.

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

@seabbs

seabbs commented Jul 31, 2026

Copy link
Copy Markdown
Member

As flagged on the other PR sonds like we need a new package to hold some concepts for dependong on or we need to move this to composeddistributions

piecewise_effect(; breaks, multipliers) is a piecewise-constant
proportional-hazards multiplier for modify under the log link, with a
closed-form cumulative hazard (a sum of base log-survival differences
over segments) rather than the numeric quadrature path a callable
effect(t) always takes. Segment i applies from breaks[i-1] up to (but
not including) breaks[i], matching the right-continuous step-function
convention the issue's own illustrative example uses.

gate(dist, opens, closes) is a finite-window special case (multipliers
0/1/0 at the opens/closes breakpoints): the hazard is off before opens,
runs unmodified inside the window, and switches off again at closes,
leaving a deliberately sub-stochastic law whose residual "never
occurred" mass reads through total_mass/is_defective (#107) like any
other defective wrapper.

Stacked on the not-yet-merged #107 branch (PR #108) since gate reuses
its total_mass/is_defective accessor directly; total_mass gets one
additional dispatch for the piecewise path (exact when the trailing
multiplier is zero, the general numeric search otherwise).

Closes #105.

Co-authored-by: Sam Abbott <contact@samabbott.co.uk>
@seabbs-bot
seabbs-bot force-pushed the feat/piecewise-hazard-105 branch from f0b9c68 to e823f17 Compare August 6, 2026 10:30
@seabbs-bot
seabbs-bot changed the base branch from feat/total-mass-defective-107 to main August 6, 2026 10:31
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.

Piecewise-constant hazard multipliers with closed forms, including gating windows

2 participants