You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
PEtab v2: "Prior distributions are truncated by the lowerBound and upperBound if the prior's domain exceeds the parameter bounds." So a normal prior with finite bounds is a truncated normal — a very common pattern. PyBNF cannot represent it: FreeParameter attaches reflecting bounds only to finite-support (Uniform) families, so Normal / Laplace / log-* priors are always unbounded. Step 1 of the importer (#407, parameters → FreeParameter, shipped in f151914 / ADR-0019) therefore raises NotImplementedError on exactly this case.
This is the prior-side twin of #410 (per-observable noise): a core PyBNF capability that v2 compliance requires, with standalone value — bounded normal / laplace / log-normal priors (positivity, numerical stability, excluding an unphysical tail) are a natural modeling need independent of PEtab. It is arguably more immediately blocking than #410: #410 gates the observables chunk, whereas this gates Step 1 itself on a single bounded normal prior.
Current state
FreeParameter.__init__: self.bounded = bounded if self._prior.has_bounded_support else False — a non-Uniform prior can never be box-bounded.
The reflecting-bounds machinery (_reflect, the closed-form triangle-wave fold) is already family-agnostic in sampling space u; it is simply gated off for unbounded-support families.
The importer raises on finite bounds truncating an unbounded prior (ADR-0019, _reject_truncation); Uniform priors truncate exactly by box intersection.
What's needed
Allow reflecting/truncation bounds on the unbounded-support families, with correct truncated-prior semantics:
Prior density. A truncated prior renormalizes over [lb, ub] (p(θ)/Z). Z is parameter-independent (the bounds are fixed), so it cancels in the MCMC acceptance ratio and in MAP optimization — consistent with PyBNF's existing no-Jacobian / target-defined-in-u philosophy (ADR-0003). It matters only for reported log-prior/log-posterior values and any evidence computation.
Initial prior sampling. Must draw from the truncated region — rejection or inverse-CDF over [lb, ub] (e.g. scipy truncnorm, or a generic truncated-ppf), not an unbounded draw folded back in. This is the part that genuinely changes behaviour.
Proposal arithmetic. The triangle-wave reflection already keeps proposals in-box and stays symmetric, so plain Metropolis over the truncated posterior is correct — verify it composes unchanged.
Cleanly decouple Support (the family's nonzero region) from Reflecting Bounds (the FreeParameter box) — ADR-0010 already separated these; this extends the box to unbounded-support families.
Design questions (for an ADR before building)
Config surface. How does a native .conf declare a bounded normal? The b/u flag applies only to uniform_var today — add bounds to normal_var / laplace_var / lognormal_var (extra lb ub, or a flag)? Unbounded stays the default (backward compatible).
Where truncation lives. On the Prior (a TruncatedPrior wrapper over a family, support [scale.forward(lb), scale.forward(ub)] in u), or on the FreeParameter (extend reflecting bounds + override sampling)? Truncation is a hybrid — it changes both the support and the normalization — so the ADR-0010 "support on the family, reflecting bounds on FreeParameter" seam needs a deliberate call.
Normalizer policy. Since Z cancels for inference, track only the unnormalized density (matching today's convention) and rely on correct truncated sampling, or compute Z for faithful reported values?
Sampling implementation. scipy truncnorm for normal; generic truncated-ppf for the others (the families already wrap scipy frozen dists, so a truncation wrapper in u-space is natural).
Relevant ADRs: 0003 (prior in the parameter's own scale, no Jacobian — where the normalizer-cancels logic lives), 0010 (Prior family vs FreeParameter scale/bounds split; the Support-vs-Reflecting-Bounds decoupling), 0019 (importer Step 1, where truncation is currently raised). The design should land a new ADR.
Motivation
PEtab v2: "Prior distributions are truncated by the lowerBound and upperBound if the prior's domain exceeds the parameter bounds." So a
normalprior with finite bounds is a truncated normal — a very common pattern. PyBNF cannot represent it:FreeParameterattaches reflecting bounds only to finite-support (Uniform) families, so Normal / Laplace / log-* priors are always unbounded. Step 1 of the importer (#407,parameters→FreeParameter, shipped inf151914/ ADR-0019) therefore raisesNotImplementedErroron exactly this case.This is the prior-side twin of #410 (per-observable noise): a core PyBNF capability that v2 compliance requires, with standalone value — bounded
normal/laplace/log-normalpriors (positivity, numerical stability, excluding an unphysical tail) are a natural modeling need independent of PEtab. It is arguably more immediately blocking than #410: #410 gates the observables chunk, whereas this gates Step 1 itself on a single bounded normal prior.Current state
FreeParameter.__init__:self.bounded = bounded if self._prior.has_bounded_support else False— a non-Uniform prior can never be box-bounded._reflect, the closed-form triangle-wave fold) is already family-agnostic in sampling spaceu; it is simply gated off for unbounded-support families._reject_truncation); Uniform priors truncate exactly by box intersection.What's needed
Allow reflecting/truncation bounds on the unbounded-support families, with correct truncated-prior semantics:
[lb, ub](p(θ)/Z).Zis parameter-independent (the bounds are fixed), so it cancels in the MCMC acceptance ratio and in MAP optimization — consistent with PyBNF's existing no-Jacobian / target-defined-in-uphilosophy (ADR-0003). It matters only for reported log-prior/log-posterior values and any evidence computation.[lb, ub](e.g. scipytruncnorm, or a generic truncated-ppf), not an unbounded draw folded back in. This is the part that genuinely changes behaviour.FreeParameterbox) — ADR-0010 already separated these; this extends the box to unbounded-support families.Design questions (for an ADR before building)
.confdeclare a bounded normal? Theb/uflag applies only touniform_vartoday — add bounds tonormal_var/laplace_var/lognormal_var(extralb ub, or a flag)? Unbounded stays the default (backward compatible).Prior(aTruncatedPriorwrapper over a family, support[scale.forward(lb), scale.forward(ub)]inu), or on theFreeParameter(extend reflecting bounds + override sampling)? Truncation is a hybrid — it changes both the support and the normalization — so the ADR-0010 "support on the family, reflecting bounds on FreeParameter" seam needs a deliberate call.Zcancels for inference, track only the unnormalized density (matching today's convention) and rely on correct truncated sampling, or computeZfor faithful reported values?truncnormfor normal; generic truncated-ppf for the others (the families already wrap scipy frozen dists, so a truncation wrapper inu-space is natural).Scope & relationships
_reject_truncationraise (ADR-0019) becomes a real mapping of a bounded PEtab prior → a boundedFreeParameter.