PyBNF priors (pybnf.priors)

The pybnf.priors package holds PyBNF’s prior distributions. A free parameter’s prior is one distribution family combined with one scale (see the Prior, Distribution Family, and Parameter Scale entries in CONTEXT.md): the family is evaluated entirely in the sampling space u, and the owning FreeParameter holds the scale and applies the theta <-> u transform around it.

The package contains sixteen distribution families – normal, uniform, laplace, cauchy, gamma, exponential, chisquare, rayleigh, half_normal, half_cauchy, beta, inv_gamma, weibull, gumbel, logistic, and student_t – one small module per family. Each is a Prior subclass of the same shape: it self-registers with @register_prior_family, declares its field_names and support, and provides a build classmethod plus a hand-written logpdf_jax. Because the leaves are uniform, this page documents the package facade and the shared infrastructure rather than enumerating all sixteen; the user-facing catalog – which *_var keyword names each family, and which take reflecting bounds – is in Priors and Parameter Initialization.

Package facade

The priors package: Prior = distribution family x scale (ADR-0010).

A free parameter’s prior is one Prior family (in family.py files, self-registered via @register_prior_family) combined with one Scale (LINEAR/LOG10). Importing this package fires the family decorators, populating PRIOR_FAMILY_REGISTRY; from that registry we derive PRIOR_KEYWORD_MAP – the single source of truth mapping a legacy *_var config keyword to its (family_cls, scale) pair, consumed by both config._load_variables and parse.py’s grammar.

The keyword naming is regular: a family with base b yields {b}_var (linear) and log{b}_var (log10). var/logvar are the two static no-prior keywords (Simplex start points), mapped to NoPrior.

class pybnf.priors.Bijector[source]

A monotone unconstraining map u = b(z) from R onto a prior’s support.

to_constrained is b (z -> u); to_unconstrained is b^{-1} (u -> z, used once to seed NUTS from the latin-hypercube start point); logdet is the change-of-variables Jacobian log|b'(z)| added to the HMC target. The *_jax peers are the JAX-traceable forms the differentiable log-density uses.

logdet(z)[source]

log|du/dz| at z (numpy; un-Jacobians the recorded posterior density).

logdet_jax(z)[source]

JAX-traceable logdet() (the differentiable Jacobian term).

to_constrained(z)[source]

Map an unconstrained z back to support-space u (numpy; draw-writing).

to_constrained_jax(z)[source]

JAX-traceable to_constrained() (the differentiable target).

to_unconstrained(u)[source]

Map a support-space u to the unconstrained z (numpy; init seeding).

class pybnf.priors.FrozenPrior[source]

A Prior backed entirely by a scipy.stats frozen distribution in the sampling space u (ADR-0010).

Every location/scale/shape family (Normal, Laplace, Cauchy, Gamma, Exponential, ChiSquare, Rayleigh) has the same density/sampling/inverse-CDF/support shape – a thin delegation to its frozen distribution – so it lives here once. A subclass sets self.frozen in __init__ (and has_bounded_support as a class attribute) and provides the family-specific build classmethod. NoPrior (no distribution) and Uniform (a custom latin-hypercube ppf) do not use this base.

logpdf(u)[source]

Log prior density at sampling-space value u.

ppf(q)[source]

Inverse CDF at quantile q (in [0, 1]), in sampling space u.

rvs(rng)[source]

Draw one sample in sampling space u using rng.

rng is the caller’s numpy.random.Generator; it is passed to scipy as random_state so prior sampling draws from the algorithm’s seeded Generator rather than NumPy’s legacy global RNG.

support()[source]

The (lo, hi) support in sampling space u (may be infinite).

class pybnf.priors.Linear[source]
forward(theta)[source]

Map a stored value theta into the sampling space u.

inverse(u)[source]

Map a sampling-space value u back to a stored value theta.

inverse_jax(u)[source]

JAX-traceable peer of inverse() (the u -> theta map, ADR-0059).

The gradient-based hmc sampler evaluates the model’s NLL at theta = scale.inverse(u) and needs that step inside the autodiff graph, so a log-scaled parameter composes with NUTS. 10.0 ** u already traces under JAX, but np.exp / np.log10 do not, so each scale supplies a jnp peer. No change-of-variables Jacobian is added for this transform – the prior is defined in u (ADR-0010), so theta enters only through the likelihood. The default raises, mirroring inverse().

class pybnf.priors.Ln[source]
forward(theta)[source]

Map a stored value theta into the sampling space u.

inverse(u)[source]

Map a sampling-space value u back to a stored value theta.

inverse_jax(u)[source]

JAX-traceable peer of inverse() (the u -> theta map, ADR-0059).

The gradient-based hmc sampler evaluates the model’s NLL at theta = scale.inverse(u) and needs that step inside the autodiff graph, so a log-scaled parameter composes with NUTS. 10.0 ** u already traces under JAX, but np.exp / np.log10 do not, so each scale supplies a jnp peer. No change-of-variables Jacobian is added for this transform – the prior is defined in u (ADR-0010), so theta enters only through the likelihood. The default raises, mirroring inverse().

class pybnf.priors.Log10[source]
forward(theta)[source]

Map a stored value theta into the sampling space u.

inverse(u)[source]

Map a sampling-space value u back to a stored value theta.

inverse_jax(u)[source]

JAX-traceable peer of inverse() (the u -> theta map, ADR-0059).

The gradient-based hmc sampler evaluates the model’s NLL at theta = scale.inverse(u) and needs that step inside the autodiff graph, so a log-scaled parameter composes with NUTS. 10.0 ** u already traces under JAX, but np.exp / np.log10 do not, so each scale supplies a jnp peer. No change-of-variables Jacobian is added for this transform – the prior is defined in u (ADR-0010), so theta enters only through the likelihood. The default raises, mirroring inverse().

class pybnf.priors.NoPrior[source]

The absence of a prior: a var/logvar Simplex start point.

Contributes nothing to the log prior, cannot be sampled, and has no support. It still pairs with a Scale (logvar is Log10); the scale lives on the FreeParameter, not here.

classmethod build(p1, p2, scale, p3=None)[source]

Factory matching the family build signature; p1/p2/p3/scale are ignored – a no-prior parameter carries only a start value.

field_names = ()

The config field names for the family’s distribution parameters, in build() order – the new-era parameter: record names each one (ADR-0043), so a positional p1 p2 becomes mean: .. , sd: ... Concrete families override; the length must match n_params (the record builds p1/p2/p3 from the first three).

frozen = None

The underlying scipy frozen distribution, or None.

has_bounded_support = False

Whether the family’s support is finite – drives reflecting-bounds eligibility and latin-hypercube participation. Uniform overrides.

has_prior = False

Whether the family has a proper distribution (False only for NoPrior).

logpdf(u)[source]

Log prior density at sampling-space value u.

logpdf_jax(u)[source]

A no-prior carrier contributes 0 to the HMC target (ADR-0059), the JAX peer of logpdf(). Returned as a JAX scalar so it sums cleanly with the family logpdfs without forcing a host/device transfer.

ppf(q)[source]

Inverse CDF at quantile q (in [0, 1]), in sampling space u.

rvs(rng)[source]

Draw one sample in sampling space u using rng.

rng is the caller’s numpy.random.Generator; it is passed to scipy as random_state so prior sampling draws from the algorithm’s seeded Generator rather than NumPy’s legacy global RNG.

support()[source]

The (lo, hi) support in sampling space u (may be infinite).

class pybnf.priors.Prior[source]

A distribution family operating in the sampling space u.

Subclasses expose logpdf/rvs/ppf in u and report their support (in u) and has_bounded_support. frozen is the underlying scipy.stats frozen distribution (or None for NoPrior), surfaced for FreeParameter._distribution back-compat.

field_names = ('p1', 'p2')

The config field names for the family’s distribution parameters, in build() order – the new-era parameter: record names each one (ADR-0043), so a positional p1 p2 becomes mean: .. , sd: ... Concrete families override; the length must match n_params (the record builds p1/p2/p3 from the first three).

frozen = None

The underlying scipy frozen distribution, or None.

has_bounded_support = False

Whether the family’s support is finite – drives reflecting-bounds eligibility and latin-hypercube participation. Uniform overrides.

has_prior = True

Whether the family has a proper distribution (False only for NoPrior).

abstractmethod logpdf(u)[source]

Log prior density at sampling-space value u.

logpdf_jax(u)[source]

JAX-traceable log prior density at sampling-space u (ADR-0059).

The gradient-based reference sampler (job_type = hmc) composes its target log-density entirely from these per-family JAX logpdfs plus the model’s JAX NLL, so jax.grad differentiates it. Every family in the edition-2 catalog overrides this with a hand-written JAX density, oracle-checked against its scipy logpdf (ADR-0059 item 4), so this base implementation is the fallback for a family that has not supplied one – it raises a pointed error rather than silently producing a wrong target. u is a JAX scalar; the return is a JAX scalar.

n_params = 2

How many config numbers the family’s parameterization takes – 2 for the location/scale/bounds families; the one-parameter families (exponential/chisquare/ rayleigh, the half-* scale priors) override to 1 so the positional grammar admits a single number (ADR-0010/#417); the three-parameter families (student_t) override to 3. A n_params >= 3 family is authored only through the new-era parameter: record – the legacy positional *_var grammar carries at most two numbers, so var_keyword_grammar omits it (ADR-0057).

abstractmethod ppf(q)[source]

Inverse CDF at quantile q (in [0, 1]), in sampling space u.

abstractmethod rvs(rng)[source]

Draw one sample in sampling space u using rng.

rng is the caller’s numpy.random.Generator; it is passed to scipy as random_state so prior sampling draws from the algorithm’s seeded Generator rather than NumPy’s legacy global RNG.

abstractmethod support()[source]

The (lo, hi) support in sampling space u (may be infinite).

support_lo_u = -inf

The family’s natural lower support endpoint in sampling space u (the lower edge of support(), a family constant independent of the distribution’s parameters). -inf for the doubly-unbounded families; the positive-support families (gamma/exponential/chisquare/rayleigh, the half-* scale priors, inv_gamma/weibull, and beta’s [0,1]) override to 0.0. The owning FreeParameter’s Scale.inverse maps it to the theta-space floor a one-sided truncation measures bounds against (ADR-0047).

class pybnf.priors.Scale[source]

Base class for a parameter scale (an abstract theta <-> u transform).

forward(theta)[source]

Map a stored value theta into the sampling space u.

inverse(u)[source]

Map a sampling-space value u back to a stored value theta.

inverse_jax(u)[source]

JAX-traceable peer of inverse() (the u -> theta map, ADR-0059).

The gradient-based hmc sampler evaluates the model’s NLL at theta = scale.inverse(u) and needs that step inside the autodiff graph, so a log-scaled parameter composes with NUTS. 10.0 ** u already traces under JAX, but np.exp / np.log10 do not, so each scale supplies a jnp peer. No change-of-variables Jacobian is added for this transform – the prior is defined in u (ADR-0010), so theta enters only through the likelihood. The default raises, mirroring inverse().

class pybnf.priors.TruncatedPrior(inner, lo_u, hi_u)[source]

An inner Prior family confined to [lo_u, hi_u] in u.

Density is the inner family’s, renormalized over the box (logpdf = inner.logpdf - log Z inside, -inf outside). Sampling and the inverse CDF go through the truncated inverse-CDF, so a draw lands inside the box by construction (not an unbounded draw folded back). Z is parameter-independent, so it cancels in the MCMC acceptance ratio and in MAP optimization (ADR-0003); it is kept only so reported densities are genuinely those of the truncated distribution.

has_bounded_support = True

Whether the family’s support is finite – drives reflecting-bounds eligibility and latin-hypercube participation. Uniform overrides.

has_prior = True

Whether the family has a proper distribution (False only for NoPrior).

logpdf(u)[source]

Renormalized log density: inner.logpdf(u) - log Z in the box, else -inf. The -log Z constant cancels for inference but makes the reported value the true truncated-density log-pdf.

logpdf_jax(u)[source]

JAX log-density of the truncated family (ADR-0059): the inner family’s logpdf_jax renormalized by -log Z inside [lo_u, hi_u], -inf outside – the JAX peer of logpdf(). The safe-u double-where evaluates the inner density at the box midpoint outside the box (a point guaranteed in-support, since Z > 0 requires the box to carry inner mass), then masks, so jax.grad stays finite (0) outside the box (the same autodiff guard the positive-support families use).

This is the density of the truncated prior; like Uniform, the hmc sampler reparameterizes the box u = lo_u + (hi_u-lo_u) sigmoid(z) (pybnf.priors.bijector), so NUTS samples an unbounded z and a truncation wall is unreachable – the retained mass is sampled divergence-free even when it leans against a bound (ADR-0059 item 5).

ppf(q)[source]

Truncated inverse CDF over [lo_u, hi_u]: map q in [0, 1] onto the retained CDF interval, then invert with the inner family’s ppf.

rvs(rng)[source]

Draw one sample in the box by inverse-CDF: ppf(U) with U ~ U(0,1) drawn from the caller’s Generator (so the draw lands inside the box by construction, not by folding an unbounded draw back in).

support()[source]

The (lo, hi) support in sampling space u (may be infinite).

pybnf.priors.bijector_for_support(lo, hi)[source]

The unconstraining bijector for a prior whose sampling-space support is (lo, hi).

Keys on the finiteness of each endpoint (the support shape), so one factory serves every family: an unbounded support is the identity, one finite endpoint is a log/exp half-line map, two finite endpoints are the logit/sigmoid box map.

pybnf.priors.build_prior(keyword, p1, p2, p3=None)[source]

Resolve a *_var keyword + config values to a Prior and its scale.

Returns (prior, scale). The single entry point used by FreeParameter so the keyword->(family, scale) mapping lives in exactly one place (ADR-0010, M2.3).

p3 carries the third distribution parameter of a three-parameter family (student_t’s scale, after df/location); it is None for the one- and two-parameter families and the no-prior carriers (ADR-0057). The family build classmethods all accept it (trailing-optional), so it is passed uniformly.

An unrecognised keyword falls back to (NoPrior, LINEAR) – a linear, unbounded, no-prior value carrier – preserving the legacy _make_distribution behavior, which returned None (no usable distribution) for any type outside the four *_var families. Real config keywords are validated upstream by parse.py’s grammar; a registry that failed to generate the known keywords would be caught by the keyword-map unit tests, not silently swallowed here.

pybnf.priors.var_keyword_grammar()[source]

Partition the prior families’ *_var keywords for parse.py’s grammar (ADR-0010). Returns (bounded_keywords, unbounded_keywords, one_param_keywords): each family b contributes {b}_var (linear) and log{b}_var (log10), routed by has_bounded_support – bounded-support families take the optional b/u flag, unbounded ones don’t. one_param_keywords is the subset (of unbounded_keywords) whose family takes a single config number (n_params == 1: exponential/chisquare/rayleigh, the half-* scale priors) – so the grammar requires exactly one number for them and two for the rest.

A n_params >= 3 family (student_t, ADR-0057) is omitted entirely: the legacy positional <family>_var = id p1 p2 grammar carries at most two numbers (and a three-token value already means a bounded box plus its reflecting-bounds flag), so a third parameter has no unambiguous positional home. Such a family is authored only through the new-era parameter: record (ADR-0043), which names each field and reads them via Prior.field_names – the family stays in PRIOR_KEYWORD_MAP (so the record path resolves it), it just gets no positional keyword.

The no-prior var/logvar keywords are handled separately by parse.py.

The Prior abstraction

The Prior abstraction: a distribution family evaluated entirely in the sampling space u (ADR-0010).

A Prior is scale-agnostic – it knows nothing about theta or log10. The owning FreeParameter holds the Scale and applies the theta <-> u transform, calling prior.logpdf(scale.forward(theta)) and scale.inverse(prior.rvs(rng)). This keeps each family’s math pure and the scale in one place (ADR-0003).

Concrete families (Normal, Uniform, …) live one-per-file and self-register with @register_prior_family. NoPrior is the first-class null-object for var/logvar Simplex start points: a free parameter with a scale but no distribution.

class pybnf.priors.base.FrozenPrior[source]

A Prior backed entirely by a scipy.stats frozen distribution in the sampling space u (ADR-0010).

Every location/scale/shape family (Normal, Laplace, Cauchy, Gamma, Exponential, ChiSquare, Rayleigh) has the same density/sampling/inverse-CDF/support shape – a thin delegation to its frozen distribution – so it lives here once. A subclass sets self.frozen in __init__ (and has_bounded_support as a class attribute) and provides the family-specific build classmethod. NoPrior (no distribution) and Uniform (a custom latin-hypercube ppf) do not use this base.

logpdf(u)[source]

Log prior density at sampling-space value u.

ppf(q)[source]

Inverse CDF at quantile q (in [0, 1]), in sampling space u.

rvs(rng)[source]

Draw one sample in sampling space u using rng.

rng is the caller’s numpy.random.Generator; it is passed to scipy as random_state so prior sampling draws from the algorithm’s seeded Generator rather than NumPy’s legacy global RNG.

support()[source]

The (lo, hi) support in sampling space u (may be infinite).

class pybnf.priors.base.NoPrior[source]

The absence of a prior: a var/logvar Simplex start point.

Contributes nothing to the log prior, cannot be sampled, and has no support. It still pairs with a Scale (logvar is Log10); the scale lives on the FreeParameter, not here.

classmethod build(p1, p2, scale, p3=None)[source]

Factory matching the family build signature; p1/p2/p3/scale are ignored – a no-prior parameter carries only a start value.

field_names = ()

The config field names for the family’s distribution parameters, in build() order – the new-era parameter: record names each one (ADR-0043), so a positional p1 p2 becomes mean: .. , sd: ... Concrete families override; the length must match n_params (the record builds p1/p2/p3 from the first three).

frozen = None

The underlying scipy frozen distribution, or None.

has_bounded_support = False

Whether the family’s support is finite – drives reflecting-bounds eligibility and latin-hypercube participation. Uniform overrides.

has_prior = False

Whether the family has a proper distribution (False only for NoPrior).

logpdf(u)[source]

Log prior density at sampling-space value u.

logpdf_jax(u)[source]

A no-prior carrier contributes 0 to the HMC target (ADR-0059), the JAX peer of logpdf(). Returned as a JAX scalar so it sums cleanly with the family logpdfs without forcing a host/device transfer.

ppf(q)[source]

Inverse CDF at quantile q (in [0, 1]), in sampling space u.

rvs(rng)[source]

Draw one sample in sampling space u using rng.

rng is the caller’s numpy.random.Generator; it is passed to scipy as random_state so prior sampling draws from the algorithm’s seeded Generator rather than NumPy’s legacy global RNG.

support()[source]

The (lo, hi) support in sampling space u (may be infinite).

class pybnf.priors.base.Prior[source]

A distribution family operating in the sampling space u.

Subclasses expose logpdf/rvs/ppf in u and report their support (in u) and has_bounded_support. frozen is the underlying scipy.stats frozen distribution (or None for NoPrior), surfaced for FreeParameter._distribution back-compat.

field_names = ('p1', 'p2')

The config field names for the family’s distribution parameters, in build() order – the new-era parameter: record names each one (ADR-0043), so a positional p1 p2 becomes mean: .. , sd: ... Concrete families override; the length must match n_params (the record builds p1/p2/p3 from the first three).

frozen = None

The underlying scipy frozen distribution, or None.

has_bounded_support = False

Whether the family’s support is finite – drives reflecting-bounds eligibility and latin-hypercube participation. Uniform overrides.

has_prior = True

Whether the family has a proper distribution (False only for NoPrior).

abstractmethod logpdf(u)[source]

Log prior density at sampling-space value u.

logpdf_jax(u)[source]

JAX-traceable log prior density at sampling-space u (ADR-0059).

The gradient-based reference sampler (job_type = hmc) composes its target log-density entirely from these per-family JAX logpdfs plus the model’s JAX NLL, so jax.grad differentiates it. Every family in the edition-2 catalog overrides this with a hand-written JAX density, oracle-checked against its scipy logpdf (ADR-0059 item 4), so this base implementation is the fallback for a family that has not supplied one – it raises a pointed error rather than silently producing a wrong target. u is a JAX scalar; the return is a JAX scalar.

n_params = 2

How many config numbers the family’s parameterization takes – 2 for the location/scale/bounds families; the one-parameter families (exponential/chisquare/ rayleigh, the half-* scale priors) override to 1 so the positional grammar admits a single number (ADR-0010/#417); the three-parameter families (student_t) override to 3. A n_params >= 3 family is authored only through the new-era parameter: record – the legacy positional *_var grammar carries at most two numbers, so var_keyword_grammar omits it (ADR-0057).

abstractmethod ppf(q)[source]

Inverse CDF at quantile q (in [0, 1]), in sampling space u.

abstractmethod rvs(rng)[source]

Draw one sample in sampling space u using rng.

rng is the caller’s numpy.random.Generator; it is passed to scipy as random_state so prior sampling draws from the algorithm’s seeded Generator rather than NumPy’s legacy global RNG.

abstractmethod support()[source]

The (lo, hi) support in sampling space u (may be infinite).

support_lo_u = -inf

The family’s natural lower support endpoint in sampling space u (the lower edge of support(), a family constant independent of the distribution’s parameters). -inf for the doubly-unbounded families; the positive-support families (gamma/exponential/chisquare/rayleigh, the half-* scale priors, inv_gamma/weibull, and beta’s [0,1]) override to 0.0. The owning FreeParameter’s Scale.inverse maps it to the theta-space floor a one-sided truncation measures bounds against (ADR-0047).

Parameter scale

Parameter Scale – the space a free parameter is sampled, proposed, and stored in (ADR-0003, ADR-0010).

A Scale owns the theta <-> u transform between a parameter’s stored value theta and the sampling space u the prior family and the proposal arithmetic both operate in. Linear is the identity; Log10 is base-10 log (u = log10(theta)); Ln is natural log (u = ln(theta)). The transform lives here, in one place, so the log/exp boundary is not smeared across FreeParameter.add / _reflect / prior_logpdf / sample_value (which all go through _scale) – so a new base composes for free there.

The scales are concrete singletons – LINEAR, LOG10, LN – not a registry. Each carries an explicit name so its base is never ambiguous in output (ADR-0022: every log scale names its base; there is no bare “log”); the native parameter: record (ADR-0043) selects one by name (parameter_scale: linear|log10|ln). Log10.inverse is 10.0 ** u to match exp10 and the inline 10** of the proposal arithmetic, bit-for-bit; Ln.inverse is np.exp(u).

class pybnf.priors.scale.Linear[source]
forward(theta)[source]

Map a stored value theta into the sampling space u.

inverse(u)[source]

Map a sampling-space value u back to a stored value theta.

inverse_jax(u)[source]

JAX-traceable peer of inverse() (the u -> theta map, ADR-0059).

The gradient-based hmc sampler evaluates the model’s NLL at theta = scale.inverse(u) and needs that step inside the autodiff graph, so a log-scaled parameter composes with NUTS. 10.0 ** u already traces under JAX, but np.exp / np.log10 do not, so each scale supplies a jnp peer. No change-of-variables Jacobian is added for this transform – the prior is defined in u (ADR-0010), so theta enters only through the likelihood. The default raises, mirroring inverse().

class pybnf.priors.scale.Ln[source]
forward(theta)[source]

Map a stored value theta into the sampling space u.

inverse(u)[source]

Map a sampling-space value u back to a stored value theta.

inverse_jax(u)[source]

JAX-traceable peer of inverse() (the u -> theta map, ADR-0059).

The gradient-based hmc sampler evaluates the model’s NLL at theta = scale.inverse(u) and needs that step inside the autodiff graph, so a log-scaled parameter composes with NUTS. 10.0 ** u already traces under JAX, but np.exp / np.log10 do not, so each scale supplies a jnp peer. No change-of-variables Jacobian is added for this transform – the prior is defined in u (ADR-0010), so theta enters only through the likelihood. The default raises, mirroring inverse().

class pybnf.priors.scale.Log10[source]
forward(theta)[source]

Map a stored value theta into the sampling space u.

inverse(u)[source]

Map a sampling-space value u back to a stored value theta.

inverse_jax(u)[source]

JAX-traceable peer of inverse() (the u -> theta map, ADR-0059).

The gradient-based hmc sampler evaluates the model’s NLL at theta = scale.inverse(u) and needs that step inside the autodiff graph, so a log-scaled parameter composes with NUTS. 10.0 ** u already traces under JAX, but np.exp / np.log10 do not, so each scale supplies a jnp peer. No change-of-variables Jacobian is added for this transform – the prior is defined in u (ADR-0010), so theta enters only through the likelihood. The default raises, mirroring inverse().

class pybnf.priors.scale.Scale[source]

Base class for a parameter scale (an abstract theta <-> u transform).

forward(theta)[source]

Map a stored value theta into the sampling space u.

inverse(u)[source]

Map a sampling-space value u back to a stored value theta.

inverse_jax(u)[source]

JAX-traceable peer of inverse() (the u -> theta map, ADR-0059).

The gradient-based hmc sampler evaluates the model’s NLL at theta = scale.inverse(u) and needs that step inside the autodiff graph, so a log-scaled parameter composes with NUTS. 10.0 ** u already traces under JAX, but np.exp / np.log10 do not, so each scale supplies a jnp peer. No change-of-variables Jacobian is added for this transform – the prior is defined in u (ADR-0010), so theta enters only through the likelihood. The default raises, mirroring inverse().

Bounded support

Two family-agnostic decorators confine an otherwise unbounded family to a finite region: truncated renormalizes the density over a box for the gradient-free samplers, and bijector supplies the change-of-variables the HMC/NUTS reference sampler uses instead.

The TruncatedPrior decorator: a family confined to a finite box in the sampling space u (ADR-0020, issue #411).

PEtab v2 truncates a prior by the parameter’s lowerBound/upperBound, so a normal prior with finite bounds is a truncated normal. PyBNF’s unbounded-support families (Normal, Laplace, and their log forms) could not carry bounds; this wraps any such family in a decorator that renormalizes the density over [lo_u, hi_u] and samples inside the box by inverse-CDF.

Truncation is a hybrid concern (ADR-0010): it changes both the family’s support and its normalization. This decorator owns both halves of the distribution math in one family-agnostic place, while the owning FreeParameter owns the matching reflecting box (the bounds in theta). The decorator works for any inner family backed by a scipy.stats frozen distribution – it reads only cdf/ppf/logpdf – so no per-family code is needed (the M2.3 ethos).

The bounds lo_u/hi_u are in sampling space u: the owning FreeParameter maps the theta box through its Scale before wrapping, so a log-scale parameter truncates in log10 space (the same space its family and proposal arithmetic already live in).

class pybnf.priors.truncated.TruncatedPrior(inner, lo_u, hi_u)[source]

An inner Prior family confined to [lo_u, hi_u] in u.

Density is the inner family’s, renormalized over the box (logpdf = inner.logpdf - log Z inside, -inf outside). Sampling and the inverse CDF go through the truncated inverse-CDF, so a draw lands inside the box by construction (not an unbounded draw folded back). Z is parameter-independent, so it cancels in the MCMC acceptance ratio and in MAP optimization (ADR-0003); it is kept only so reported densities are genuinely those of the truncated distribution.

has_bounded_support = True

Whether the family’s support is finite – drives reflecting-bounds eligibility and latin-hypercube participation. Uniform overrides.

has_prior = True

Whether the family has a proper distribution (False only for NoPrior).

logpdf(u)[source]

Renormalized log density: inner.logpdf(u) - log Z in the box, else -inf. The -log Z constant cancels for inference but makes the reported value the true truncated-density log-pdf.

logpdf_jax(u)[source]

JAX log-density of the truncated family (ADR-0059): the inner family’s logpdf_jax renormalized by -log Z inside [lo_u, hi_u], -inf outside – the JAX peer of logpdf(). The safe-u double-where evaluates the inner density at the box midpoint outside the box (a point guaranteed in-support, since Z > 0 requires the box to carry inner mass), then masks, so jax.grad stays finite (0) outside the box (the same autodiff guard the positive-support families use).

This is the density of the truncated prior; like Uniform, the hmc sampler reparameterizes the box u = lo_u + (hi_u-lo_u) sigmoid(z) (pybnf.priors.bijector), so NUTS samples an unbounded z and a truncation wall is unreachable – the retained mass is sampled divergence-free even when it leans against a bound (ADR-0059 item 5).

ppf(q)[source]

Truncated inverse CDF over [lo_u, hi_u]: map q in [0, 1] onto the retained CDF interval, then invert with the inner family’s ppf.

rvs(rng)[source]

Draw one sample in the box by inverse-CDF: ppf(U) with U ~ U(0,1) drawn from the caller’s Generator (so the draw lands inside the box by construction, not by folding an unbounded draw back in).

support()[source]

The (lo, hi) support in sampling space u (may be infinite).

Support-aware unconstraining bijections for gradient-based sampling (ADR-0059 item 5).

The gradient-free samplers (am / dream / p_dream) keep a draw inside a constrained prior support with reflecting bounds – a Metropolis device with no HMC analogue. NUTS, by contrast, samples an unbounded momentum-driven trajectory; at a -inf support wall (a positive-support prior’s u <= 0, beta’s [0, 1], a TruncatedPrior box) the leapfrog integrator diverges.

The fix is a change of variables: HMC samples an unconstrained z in R, mapped to the prior’s support by a monotone bijection u = b(z), and the target gains the Jacobian term log|b'(z)|. Because b lands strictly inside the open support for every finite z, the -inf wall (and the divergence it caused) is never reached.

The transform is a property of the support shape, not the family, so it lives here once and keys purely on support() (the (lo, hi) finiteness pattern) – the same family-agnostic ethos as truncated. Four cases cover the whole catalog:

support (lo,hi)

u = b(z)

z = b^{-1}(u)

log|b'(z)|

(-inf, inf)

z

u

0

(lo, inf)

lo + exp(z)

log(u - lo)

z

(-inf, hi)

hi - exp(z)

log(hi - u)

z

(lo, hi)

lo + (hi-lo)*sigmoid(z)

logit((u-lo)/(hi-lo))

log(hi-lo)+logsig(z)+logsig(-z)

Each bijector exposes a numpy to_unconstrained / to_constrained / logdet (host-side init seeding and draw-writing) and a JAX-traceable to_constrained_jax / logdet_jax (the differentiable target the hmc sampler composes), mirroring Scale’s inverse / inverse_jax split. jax is imported lazily so importing this module (and the whole priors package) never requires the optional pybnf[jax] extra.

class pybnf.priors.bijector.Bijector[source]

A monotone unconstraining map u = b(z) from R onto a prior’s support.

to_constrained is b (z -> u); to_unconstrained is b^{-1} (u -> z, used once to seed NUTS from the latin-hypercube start point); logdet is the change-of-variables Jacobian log|b'(z)| added to the HMC target. The *_jax peers are the JAX-traceable forms the differentiable log-density uses.

logdet(z)[source]

log|du/dz| at z (numpy; un-Jacobians the recorded posterior density).

logdet_jax(z)[source]

JAX-traceable logdet() (the differentiable Jacobian term).

to_constrained(z)[source]

Map an unconstrained z back to support-space u (numpy; draw-writing).

to_constrained_jax(z)[source]

JAX-traceable to_constrained() (the differentiable target).

to_unconstrained(u)[source]

Map a support-space u to the unconstrained z (numpy; init seeding).

class pybnf.priors.bijector.BoxBijector(lo, hi)[source]

Finite box support (lo, hi): u = lo + (hi-lo)*sigmoid(z), with log|b'(z)| = log(hi-lo) + logsigmoid(z) + logsigmoid(-z). Covers uniform / loguniform boxes, beta’s [0, 1], and a two-sided TruncatedPrior. sigmoid(z) in (0, 1) for every finite z, so u stays strictly inside the box – both walls are unreachable.

logdet(z)[source]

log|du/dz| at z (numpy; un-Jacobians the recorded posterior density).

logdet_jax(z)[source]

JAX-traceable logdet() (the differentiable Jacobian term).

to_constrained(z)[source]

Map an unconstrained z back to support-space u (numpy; draw-writing).

to_constrained_jax(z)[source]

JAX-traceable to_constrained() (the differentiable target).

to_unconstrained(u)[source]

Map a support-space u to the unconstrained z (numpy; init seeding).

class pybnf.priors.bijector.IdentityBijector[source]

Unbounded support (-inf, inf): u = z, no Jacobian. The real-support families (normal/laplace/cauchy/student_t/gumbel/logistic, and any lognormal_var whose prior is a normal in u) need no reparameterization, so HMC samples them in u directly.

logdet(z)[source]

log|du/dz| at z (numpy; un-Jacobians the recorded posterior density).

logdet_jax(z)[source]

JAX-traceable logdet() (the differentiable Jacobian term).

to_constrained(z)[source]

Map an unconstrained z back to support-space u (numpy; draw-writing).

to_constrained_jax(z)[source]

JAX-traceable to_constrained() (the differentiable target).

to_unconstrained(u)[source]

Map a support-space u to the unconstrained z (numpy; init seeding).

class pybnf.priors.bijector.LowerBoundedBijector(lo)[source]

Half-line support (lo, inf): u = lo + exp(z), log|b'(z)| = z. Covers the positive families (gamma/exponential/chisquare/rayleigh/weibull/inv_gamma/the half-* scale priors), whose lo is 0. exp(z) > 0 for every finite z, so u > lo strictly – the u <= lo wall is unreachable.

logdet(z)[source]

log|du/dz| at z (numpy; un-Jacobians the recorded posterior density).

logdet_jax(z)[source]

JAX-traceable logdet() (the differentiable Jacobian term).

to_constrained(z)[source]

Map an unconstrained z back to support-space u (numpy; draw-writing).

to_constrained_jax(z)[source]

JAX-traceable to_constrained() (the differentiable target).

to_unconstrained(u)[source]

Map a support-space u to the unconstrained z (numpy; init seeding).

class pybnf.priors.bijector.UpperBoundedBijector(hi)[source]

Half-line support (-inf, hi): u = hi - exp(z), log|b'(z)| = z. The mirror of LowerBoundedBijector (no catalog family is upper-only by default, but a one-sided upper TruncatedPrior lands here); u < hi strictly.

logdet(z)[source]

log|du/dz| at z (numpy; un-Jacobians the recorded posterior density).

logdet_jax(z)[source]

JAX-traceable logdet() (the differentiable Jacobian term).

to_constrained(z)[source]

Map an unconstrained z back to support-space u (numpy; draw-writing).

to_constrained_jax(z)[source]

JAX-traceable to_constrained() (the differentiable target).

to_unconstrained(u)[source]

Map a support-space u to the unconstrained z (numpy; init seeding).

pybnf.priors.bijector.bijector_for_support(lo, hi)[source]

The unconstraining bijector for a prior whose sampling-space support is (lo, hi).

Keys on the finiteness of each endpoint (the support shape), so one factory serves every family: an unbounded support is the identity, one finite endpoint is a log/exp half-line map, two finite endpoints are the logit/sigmoid box map.