Source code for pybnf.priors.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)``.
"""
import numpy as np
[docs]
class Scale:
"""Base class for a parameter scale (an abstract ``theta <-> u`` transform)."""
is_log = False
name = 'linear'
[docs]
def forward(self, theta):
"""Map a stored value ``theta`` into the sampling space ``u``."""
raise NotImplementedError
[docs]
def inverse(self, u):
"""Map a sampling-space value ``u`` back to a stored value ``theta``."""
raise NotImplementedError
[docs]
def inverse_jax(self, u):
"""JAX-traceable peer of :meth:`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 :meth:`inverse`."""
raise NotImplementedError
[docs]
class Linear(Scale):
is_log = False
name = 'linear'
[docs]
def forward(self, theta):
return theta
[docs]
def inverse(self, u):
return u
[docs]
def inverse_jax(self, u):
return u
[docs]
class Log10(Scale):
is_log = True
name = 'log10'
[docs]
def forward(self, theta):
return np.log10(theta)
[docs]
def inverse(self, u):
return 10.0 ** u
[docs]
def inverse_jax(self, u):
# 10.0 ** u traces unchanged under JAX (no jnp needed), and matches
# ``inverse`` bit-for-bit -- kept as an explicit peer for symmetry.
return 10.0 ** u
[docs]
class Ln(Scale):
is_log = True
name = 'ln'
[docs]
def forward(self, theta):
return np.log(theta)
[docs]
def inverse(self, u):
return np.exp(u)
[docs]
def inverse_jax(self, u):
import jax.numpy as jnp
return jnp.exp(u)
LINEAR = Linear()
LOG10 = Log10()
LN = Ln()