Skip to content

feat: hold a leaf parameter fixed in the priors front-door (#752) - #761

Merged
seabbs-bot merged 2 commits into
integration/composed-stackfrom
feat/fixed-param-752
Jun 26, 2026
Merged

feat: hold a leaf parameter fixed in the priors front-door (#752)#761
seabbs-bot merged 2 commits into
integration/composed-stackfrom
feat/fixed-param-752

Conversation

@seabbs-bot

Copy link
Copy Markdown
Collaborator

Closes #752.

Problem

The priors front-door samples every inventory parameter through
tilde_assume!!, and a Dirac point mass is rejected by DynamicPPL's
discrete-distribution guard under NUTS. There was no way to hold one leaf
parameter fixed while fitting the others (e.g. fit a Gamma's shape while
pinning its scale).

API

A leaf parameter is held fixed by placing a plain value (not a
distribution) in its prior slot
. The sampling models classify each slot:
a Distribution is sampled via tilde_assume!! as before; a
non-distribution value is substituted directly as a constant. A fixed
parameter therefore never enters the sampler (no tilde, no point mass), so
it is absent from the VarInfo and the chain, while the fitted parameters
flow exactly as today.

Two equivalent ways to set the pin, both extending the existing override
mechanism rather than forking it:

# build_priors gains a `fix` keyword, a nested NamedTuple keyed like the
# tree whose leaf values are the plain constants.
priors = build_priors(template; fix = (onset_admit = (scale = 1.5,),))

# Or edit a built prior by path, the same override the API already uses.
priors = update(build_priors(template), :onset_admit => (scale = 1.5,))

fix takes precedence over a priors override for the same parameter and
is validated against the inventory (a typo errors rather than pinning
nothing silently).

Reading the fit back, the pinned constant is absent from the chain, so
chain_to_params / update(template, chain) / param_draws gain a fix
keyword that fills the constants back into the reconstructed distribution:

ready = update(template, chain; fix = priors)  # pinned scale reappears

Composes with the override API

Yes. The pin lives in the same nested prior NamedTuple that build_priors,
the priors= override, and update(priors, path => fields) already
produce and edit. fix is a convenience that writes plain values into that
structure; the update-by-path route reaches the identical result with no
new surface.

DynamicPPL note

No DynamicPPL limitation blocks the clean approach: because a fixed
parameter is substituted as a constant before any ~, the rejected-Dirac
path is never taken. The guard that motivated #752 is sidestepped entirely.

Tests

Red/green TDD. New tests:

  • build_priors fix pins a parameter as a constant (#752) (unit): fix
    inserts a plain value, takes precedence over a priors override, matches
    the update-by-path pin, and errors on an unknown parameter.
  • fixed leaf parameter: held constant, absent from VarInfo (turing): the
    fixed scale is in the reconstructed leaf and absent from the VarInfo,
    the free shape is sampled.
  • fixed leaf parameter: update from chain keeps the pin (turing): a real
    NUTS fit recovers the free shape (~3.0) with the scale pinned at 1.5; the
    pinned value is absent from the chain yet reappears via
    update(template, chain; fix = priors).

Passing locally: the three new testitems and the build_priors unit test.
The full ComposedParametersModel.jl suite is re-running locally to
confirm no regression; CI runs the whole suite (Turing + AD backends) on
this PR.

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

A plain value in a prior slot should pin a leaf parameter: substituted
as a constant, never sampled, absent from VarInfo and the chain, yet
present in the update'd distribution.
A plain value in a prior slot pins that leaf parameter: it is
substituted as a constant and never sampled. The sampling models
classify each slot, sampling a distribution via tilde and substituting a
non-distribution value directly, so a fixed parameter never enters the
VarInfo or the chain (no tilde, no point mass). This sidesteps the
DynamicPPL guard that rejects a Dirac point mass under NUTS.

build_priors gains a fix keyword (a nested NamedTuple keyed like the
tree whose leaf values are plain constants) that writes the constants
into the prior NamedTuple; it takes precedence over a priors override
and is validated against the inventory. The same pin is reachable by
editing a built prior with update(priors, path => (param = value,)), so
fix composes with the override mechanism rather than forking it.

chain_to_params/update/param_draws gain a fix keyword that fills the
pinned constants back into the reconstructed distribution, since they
are absent from the fitted chain.
@seabbs-bot
seabbs-bot merged commit bebf85e into integration/composed-stack Jun 26, 2026
12 of 21 checks passed
@seabbs-bot
seabbs-bot deleted the feat/fixed-param-752 branch June 26, 2026 15:11
@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/CensoredDistributions.jl", rev="feat/fixed-param-752")
using CensoredDistributions

Option 2: Local Checkout

If you have the repo locally:

git checkout feat/fixed-param-752
julia --project=. -e "using Pkg; Pkg.instantiate()"

@codecov

codecov Bot commented Jun 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 58.97436% with 16 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
ext/CensoredDistributionsFlexiChainsExt.jl 0.00% 10 Missing ⚠️
ext/CensoredDistributionsDynamicPPLExt.jl 0.00% 6 Missing ⚠️
Flag Coverage Δ
ad-enzyme-forward 23.51% <0.00%> (-0.21%) ⬇️
ad-enzyme-reverse 23.51% <0.00%> (-0.21%) ⬇️
ad-forwarddiff 29.89% <0.00%> (-0.17%) ⬇️
ad-mooncake-forward 22.40% <0.00%> (-0.22%) ⬇️
ad-mooncake-reverse 26.96% <0.00%> (-0.26%) ⬇️
ad-reversediff 23.87% <0.00%> (-0.19%) ⬇️
unit 90.03% <100.00%> (+0.10%) ⬆️

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

Files with missing lines Coverage Δ
src/composers/introspection.jl 83.11% <100.00%> (+0.82%) ⬆️
ext/CensoredDistributionsDynamicPPLExt.jl 0.00% <0.00%> (ø)
ext/CensoredDistributionsFlexiChainsExt.jl 0.00% <0.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.

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.

1 participant