PyBNF noise models (pybnf.noise)¶
The pybnf.noise package holds PyBNF’s per-point noise models –
the negative-log-likelihood kernels that a probabilistic objective function
delegates to (ADR-0011). A per-point noise model is a distribution family ×
additive-noise scale × location interpretation (see the Noise Model,
Additive-Noise Scale, and Location Interpretation entries in
CONTEXT.md), realized as a pure, scale-agnostic kernel
nll(prediction, observation, noise) – one file per family. The
SummationObjective harness in pybnf.objective owns the per-row
iteration and pairs each family with a Noise Parameter Source, delegating
the per-point math to the kernels documented here.
For the user-facing configuration surface – which objfunc code selects
which family, and how to attach a noise-parameter source – see
Noise Models and Objective Functions.
Base kernel¶
The NoiseModel abstraction: a per-point negative-log-likelihood kernel
for one observation given the model’s prediction and a noise parameter (ADR-0011).
A NoiseModel is pure and scale-agnostic – it knows nothing about the
iteration over data points, the weighting, or where its noise parameter comes
from. The owning SummationObjective (in objective.py) drives the per-row
loop and sources the noise parameter, calling the kernel one point at a time.
This mirrors M2.3’s Prior/FreeParameter split (ADR-0010): the harness that
owns the iteration delegates the pure family math to a small object.
A per-point noise model is defined by three orthogonal axes (ADR-0004):
distribution family x the scale the noise is additive on x the
location interpretation. All three are live: lognormal is Gaussian
additive-on-log (the scale axis); mean vs median centering picks up each
family’s own moment correction on a log scale (the location axis, ADR-0031/#419) –
Gaussian’s, Laplace’s, and the count family’s median CDF inversion all differ.
The full NLL splits as nll = data_fit(prediction, observation, noise, extra)
+ sum(param_normalizers(noise, extra).values()). The data-fit term is the
parameter-dependent part; each parameter’s normalizer is constant whenever that
noise parameter is held fixed. So a caller sums data_fit always, plus each
parameter’s normalizer iff that parameter is estimated (a free parameter) rather
than fixed (a data column or a config constant) – see the Noise Parameter
glossary entry. This is why chi_sq (fixed sigma) drops Gaussian’s log sigma
while chi_sq_dynamic (free sigma) keeps it: one family, the normalizer governed
by estimated-ness, now keyed per parameter so a two-parameter family (student_t’s
sigma + df, ADR-0058) gates each independently.
Most families carry one noise parameter (the scalar noise); a multi-parameter
family receives its primary scalar as noise and the rest in a trailing extra
mapping, named by the family’s noise_params (ADR-0058). The single-parameter
defaults below mean the existing families need no body change for the generalization.
- class pybnf.noise.base.NoiseModel[source]¶
A per-point noise distribution family: the NLL kernel for one observation.
Subclasses implement
data_fit(the parameter-dependent term) and, if the family has a separable normalizer, overridelog_normalizer(one parameter) orparam_normalizers(several, keyed by name; ADR-0058).- d_data_fit_d_prediction(prediction, observation, noise, extra=None)[source]¶
d(data_fit)/d(prediction)for one point – the per-point loss slope the gradient path chains throughd(prediction)/d(theta)(layer G, #454/#385).This is the universal scalar-gradient seam: an asymmetric family (Laplace, Student-t) whose
data_fitis not a sum of squares carries no least-squares residual, so its objective gradient is assembled assum_i w_i * d(data_fit_i)/d(prediction_i) * d(prediction_i)/d(theta)rather than from a residual-Jacobian. Overridden by the location-scale families (Gaussian / Laplace / Student-t) and – via its median CDF-inversion implicit derivative (#458) – the count family (NegBinomial); the base raises for a (hypothetical) family with no prediction gradient.
- d_mean_offset_d_noise(noise)[source]¶
d(mean_offset)/d(noise parameter)– how the mean’s moment correction moves with the noise scale, the term the estimated-scale gradient column needs when the prediction is a MEAN on a log scale (#385). It is family-specific (Gaussian’sln(base)*sigma, Laplace’s2 b t/(1 - b**2 t**2)) and 0 on the linear scale (the offset is 0 there), so the estimated-scale column reduces byte-for-byte off the log-mean corner. Each location-scale family overrides it; the base raises likemean_offset().
- d_nll_d_noise_params(prediction, observation, noise, extra=None)[source]¶
{param_name: d(data_fit + that parameter's normalizer)/d param}for each noise parameter – the estimated-scale gradient columns (layer D/G, #451/#454/#385).The objective sums each entry into the scalar gradient iff that parameter is estimated (a free parameter), exactly as
eval_pointadds each parameter’s normalizer iff estimated – so the returned derivative folds in the parameter’s own normalizer (Gaussian’slog sigma, Student-t’s df-block), which is the term that keeps a free scale from running away. Each family’s normalizers depend only on their own parameter, so the cross terms vanish and a per-parameter entry suffices. Overridden by the location-scale families and the count family (NegBinomial’s self-normalizing dispersion score, #458); the base raises for a family with none.
- d_residual_d_prediction(prediction, observation, noise, extra=None)[source]¶
d(residual)/d(prediction)for one point – the residual-Jacobian seam, paired withresidual()(#459). The assembly multiplies it by the forward sensitivityd pred/d thetato build the residual-Jacobian column, exactly as for a Gaussian.Equal to
d_data_fit_d_prediction / residualaway fromz=0; at the residual zero both numerator and denominator vanish, so the override supplies the smooth limit (for Student-t,sqrt((nu+1)/nu) * forward'(pred)/sigma). Overridden by exactly the families that overrideresidual()(Gaussian, Student-t); the base raises.
- abstractmethod data_fit(prediction, observation, noise, extra=None)[source]¶
The parameter-dependent negative-log-likelihood term for one point.
noiseis the family’s primary scalar parameter;extrais a mapping of any secondary parameters ({'df': nu}for student_t),None/ empty for the single-parameter families, which ignore it (ADR-0058).
- log_density(prediction, observation, noise, extra=None)[source]¶
The genuine per-point log-density
log p(observation | prediction, noise)in data space – the complete, normalized value model-comparison (LOO/WAIC, ADR-0056) consumes, as opposed to-nll.nllis built for the sampler, which only needs likelihood ratios, so it drops every term constant in the parameters: the family constant (_density_constant) and, for a family additive on a log scale, the change-of-variables Jacobian (scale.log_abs_dforward). A predictive density needs them, so this restores both – giving a value that matchesscipy.stats.<dist>.logpdf/.logpmf(the oracle each family documents). The count family carries noadditive_on(its PMF is self-normalizing and needs no Jacobian), so the scale term is skipped there. Every parameter’s normalizer is included regardless of estimated-ness (unlike the sampler’seval_point), so for student_t the df-block is always present even when df is fixed – which is why student_t needs no_density_constant(ADR-0058).
- log_normalizer(noise)[source]¶
The single-parameter likelihood normalizer – constant when
noiseis fixed. Zero unless the family has a separable normalizer (e.g. Gaussian’slog sigma); a self-normalizing PMF (NegBinomial) leaves it at 0. A multi-parameter family overridesparam_normalizers()instead.
- mean_offset(noise)[source]¶
The additive-space offset for mean-centering – the family’s moment correction, subtracted from
scale.forward(prediction)to recover the location parameter when the prediction is taken to be the distribution mean (MEAN, vialocation.py). It is family-specific (Gaussian’s differs from Laplace’s, #419), so each location-scale family overrides it; the base raises for a family that has no additive moment correction (e.g. the count family, which realizes its mean centering directly, not through this seam).
- nll(prediction, observation, noise, extra=None)[source]¶
The full per-point negative log-likelihood (data fit + every parameter’s normalizer).
- noise_param_defaults = {}¶
Default fixed values for parameters that may be omitted from a
noise_modelline (ADR-0058):{param_name: value}. The objective fills an omitted such parameter with aConstantSigmaof this value (student_t’s{'df': 4.0}). A parameter named here is optional; one absent here (e.g.sigma) is required. The value lives with the family (a distributional fact); the engine builds the source, keeping source construction out of the pure kernel (ADR-0011).
- noise_params = ('sigma',)¶
This family’s noise-parameter names in declaration order (ADR-0058). The first is the primary scalar passed as
noise; any others arrive in the trailingextramapping. Single-parameter families inherit('sigma',)or override it (Laplace’s('scale',), NegBinomial’s('dispersion',)); the one multi-parameter family is StudentT (('sigma', 'df')). This is the single source of truth for a family’s parameter names – the objective reads it to validate anoise_modelline’s fields.
- param_normalizers(noise, extra=None)[source]¶
{param_name: separable normalizer}– the normalizer each noise parameter contributes, which the objective adds iff that parameter’s source is estimated (ADR-0058). The default attributes the family’s wholelog_normalizer()to its single primary parameter, so a one-parameter family needs no override; StudentT splitslog sigma('sigma') from the df-block ('df').
- residual(prediction, observation, noise, extra=None)[source]¶
The signed square-root-loss residual
rfor one point – the least-squares residual form a trust-region solver (LM / TRF, #386) minimizes (layer G follow-up, #459).Defined so that
1/2 r**2 == data_fitandr * d_residual_d_prediction == d_data_fit_d_prediction– i.e.rreproduces both this point’s loss and its prediction gradient, soscipy.least_squaresminimizes the true objective (not a frozen-weight IRLS surrogate). The canonical form isr = sign(z) * sqrt(2 * data_fit)withz = (mu - forward(obs))/sigmathe additive-space residual: for a Gaussian this is exactly the standardized residualrho(its data fit is1/2 rho**2); for Student-t the sqrt-loss residual is smooth throughz=0(r ~ sqrt((nu+1)/nu) z) and downweights the tails, a clean exact least-squares reformulation (#459).Only a family whose data fit reformulates as a smooth half-square overrides this (Gaussian, Student-t); the base raises. Laplace stays scalar-only – its data fit
|z|/bhas a cusp atz=0(sqrt(2*data_fit) ~ sqrt|z|, infinite slope), so L1 / least-absolute-deviation is inherently not cleanly least-squares (it overrides this with a pointed raise). The count family (NegBinomial) likewise has no least-squares residual. A family the base refuses is routed through the scalard_data_fit_d_prediction()instead;has_least_squares_residual()decides.
- with_location(location)[source]¶
Return a copy of this family reinterpreting the prediction as a different distributional summary – the location axis (ADR-0011/0024/0031). Every noise family implements it (“every means every”): the location-scale families via the additive offset, the count family via a per-point CDF inversion. The base raises for a (hypothetical) family with no location axis.
Distribution families¶
Gaussian observation noise (ADR-0011).
- class pybnf.noise.gaussian.Gaussian(additive_on=<pybnf.noise.scale._Linear object>, location=<pybnf.noise.location._Median object>)[source]¶
Gaussian (normal) observation noise, configured by two of the three axes: the scale its noise is additive on and the location interpretation of the prediction. The defaults (
LINEAR,MEDIAN) are ordinary additive error where the prediction is the median – symmetric, so all locations coincide and the axes are trivial. Reconfigured as (LOG10,MEDIAN) it is (log10) lognormal error with the prediction as the median (thelognormalobjfunc); that one reconfiguration – adding no new distribution family – proves the axes are orthogonal and live (ADR-0011, the analogue of Laplace proving the prior seam). (LNgives the natural-log lognormal density; ADR-0022.)data_fitis the squared residual in the additive space,(mu - forward(obs))^2 / (2 sigma^2), wheremuis the additive-space location parameter;log_normalizerislog sigma. With a fixed sigma the caller drops the normalizer (and, on the log scale, the parameter-independent Jacobian) – sochi_sq/lognormalsum onlydata_fit– while a free sigma keeps the fullnll(chi_sq_dynamic).- d_data_fit_d_prediction(prediction, observation, noise, extra=None)[source]¶
d(data_fit)/d(prediction) = (mu - forward(obs))/sigma**2 * forward'(pred)(#454). The location offset is prediction-independent, so this holds for both MEAN and MEDIAN; on the linear scaleforward'(pred) = 1. Equal torho * d rho/d pred(theresidual_pointpair) – the Gaussian is the one family whose data fit is exactly1/2 rho**2, so the assembly routes it through the residual-Jacobian and this seam exists for symmetry / unit checks.
- d_mean_offset_d_noise(noise)[source]¶
d(mean_offset)/d sigma = ln(base)*sigma(0 on the linear scale, whereln(base)is 0). The term that couples the offset to the noise scale when a free sigma centers a MEAN on a log scale – folded into the estimated-sigma gradient column (#385).
- d_nll_d_noise_params(prediction, observation, noise, extra=None)[source]¶
{'sigma': (1 - rho**2)/sigma - rho * d(offset)/d sigma / sigma}– the estimated-sigma gradient column (#451/#454/#385).d(data_fit)/d sigmahas the symmetric-rho**2/sigmapart plusd(log sigma)/d sigma = 1/sigma(together(1 - rho**2)/sigma), and – when a MEAN is centered on a log scale – a coupling term: the offsetmu = forward(pred) - offsetitself depends on sigma there (offset = ln(base)*sigma**2/2), sorhocarries an extra-rho * (d offset/d sigma)/sigmawithd offset/d sigma = ln(base)*sigma, giving-ln(base)*rho(#385).d(offset)/d sigmais 0 for the MEDIAN and on the linear scale, so this reduces to(1 - rho**2)/sigmabyte-for-byte off the log-mean corner.
- d_residual_d_prediction(prediction, observation, noise, extra=None)[source]¶
d(rho)/d(prediction) = forward'(pred)/sigma(#449/#459):1on the linear scale,1/(pred*ln10*sigma)for log10,1/(pred*sigma)for ln – the scale’s chain factor. The offset is prediction-independent, so MEAN and MEDIAN agree.
- data_fit(prediction, observation, noise, extra=None)[source]¶
The parameter-dependent negative-log-likelihood term for one point.
noiseis the family’s primary scalar parameter;extrais a mapping of any secondary parameters ({'df': nu}for student_t),None/ empty for the single-parameter families, which ignore it (ADR-0058).
- log_normalizer(noise)[source]¶
The single-parameter likelihood normalizer – constant when
noiseis fixed. Zero unless the family has a separable normalizer (e.g. Gaussian’slog sigma); a self-normalizing PMF (NegBinomial) leaves it at 0. A multi-parameter family overridesparam_normalizers()instead.
- mean_offset(noise)[source]¶
The Gaussian moment correction
ln(base)*sigma**2/2(0 on the linear scale): the mean ofbase**N(mu, sigma)isbase**(mu + ln(base)*sigma**2/2), so recoveringmufrom a prediction taken to be that mean subtracts this in additive space (ADR-0022).
- noise_params = ('sigma',)¶
This family’s noise-parameter names in declaration order (ADR-0058). The first is the primary scalar passed as
noise; any others arrive in the trailingextramapping. Single-parameter families inherit('sigma',)or override it (Laplace’s('scale',), NegBinomial’s('dispersion',)); the one multi-parameter family is StudentT (('sigma', 'df')). This is the single source of truth for a family’s parameter names – the objective reads it to validate anoise_modelline’s fields.
- residual(prediction, observation, noise, extra=None)[source]¶
The Gaussian standardized residual
rho = (mu - forward(obs))/sigma– the least-squares residual the assembly stacks (#449/#459). The Gaussian is the one family whosedata_fitis exactly1/2 rho**2, sorhois already the signed square-root-loss residual (sign(z) sqrt(2 data_fit) == rhoidentically); it is returned directly (not through the sqrt round-trip) so the historical path is byte-identical. The offset is prediction-independent: 0 for the MEDIAN (any scale) and a MEAN on the linear scale, the family’s moment correction for a MEAN on a log scale.
- with_location(location)[source]¶
Return a copy of this family reinterpreting the prediction as a different distributional summary – the location axis (ADR-0011/0024/0031). Every noise family implements it (“every means every”): the location-scale families via the additive offset, the count family via a per-point CDF inversion. The base raises for a (hypothetical) family with no location axis.
Laplace observation noise (ADR-0011, ADR-0021).
- class pybnf.noise.laplace.Laplace(additive_on=<pybnf.noise.scale._Linear object>, location=<pybnf.noise.location._Median object>)[source]¶
Laplace (double-exponential) observation noise – the heavy-tailed sibling of Gaussian. Its negative log-likelihood penalizes the residual linearly (
|prediction - observation| / b) rather than quadratically, so it is the maximum-likelihood model behind least-absolute-deviation fitting and is robust to outliers; PEtab v2’snoiseDistribution = laplace.Configured by the same two axes as
Gaussian– the scale its noise is additive on and the location interpretation of the prediction. On the linear scale Laplace is symmetric, so mean and median coincide and the location axis is trivial (the defaultLINEAR,MEDIAN); on a log scale (log-laplace) the distribution is asymmetric in the original space, solocation = meanpicks up the Laplace moment correction (mean_offset), which is not Gaussian’s (#419).data_fitis|mu - forward(obs)| / bwith the scalebas the noise parameter;log_normalizerislog(2 b). With a fixedbthe caller drops the normalizer; with a freeb(thelaplaceobjfunc’sb__FREE) it keeps the fullnll– thelog(2 b)term is exactly what prevents the fit from drivingb -> inf. Value oracle:scipy.stats.laplace.logpdf.- d_data_fit_d_prediction(prediction, observation, noise, extra=None)[source]¶
d(data_fit)/d(prediction) = sign(mu - forward(obs))/b * forward'(pred)(#454).The Laplace data fit
|mu - forward(obs)|/bis non-smooth wheremu == forward(obs); PyBNF takes the subgradient 0 there –np.sign(0) == 0, the symmetric least- absolute-deviation choice – documented like layer E’s positivity decision. The derivative is undefined at the kink, so the optimizer must not rely on it being exactly the slope of a finite difference straddling the kink; an FD acceptance oracle therefore evaluates away from it (data offset from the prediction).forward'(pred) = 1on the linear scale, and the offset is prediction-independent, so MEAN and MEDIAN agree.
- d_mean_offset_d_noise(noise)[source]¶
d(mean_offset)/d b = 2 b t / (1 - b**2 t**2)(0 on the linear scale),t = ln(base). The offset’s own dependence on the scaleb, folded into the estimated-scale gradient column when a free Laplace scale centers a MEAN on a log scale (#385). Sharesmean_offset’sb*ln(base) < 1domain (the mean exists only there); off the log-mean corner (linear scale or median centering) it is 0, so the column reduces byte-for-byte.
- d_nll_d_noise_params(prediction, observation, noise, extra=None)[source]¶
{'scale': -sign(R) * d(offset)/d b / b - |R|/b**2 + 1/b}withR = mu - forward(obs)– the estimated-scale gradient column (#451/#454/#385). The-|R|/b**2isd(data_fit)/d bholding the offset fixed and1/bisd(log(2 b))/d b(the normalizer that keeps a free Laplace scale from running to infinity); the-sign(R) * (d offset/d b)/bis the coupling that appears when a MEAN is centered on a log scale, wheremu = forward(pred) - offsetand the offset depends onb(d offset/d b = 2 b t/(1 - b**2 t**2)).d(offset)/d bis 0 for the MEDIAN and on the linear scale, so this reduces to-|R|/b**2 + 1/bbyte-for-byte off that corner.
- d_residual_d_prediction(prediction, observation, noise, extra=None)[source]¶
Laplace has no least-squares residual Jacobian (the residual itself has an infinite-slope cusp at
z=0); it stays scalar-only – seeresidual()(#459).
- data_fit(prediction, observation, noise, extra=None)[source]¶
The parameter-dependent negative-log-likelihood term for one point.
noiseis the family’s primary scalar parameter;extrais a mapping of any secondary parameters ({'df': nu}for student_t),None/ empty for the single-parameter families, which ignore it (ADR-0058).
- log_normalizer(noise)[source]¶
The single-parameter likelihood normalizer – constant when
noiseis fixed. Zero unless the family has a separable normalizer (e.g. Gaussian’slog sigma); a self-normalizing PMF (NegBinomial) leaves it at 0. A multi-parameter family overridesparam_normalizers()instead.
- mean_offset(noise)[source]¶
The Laplace moment correction (distinct from Gaussian’s, #419). For
base**Laplace(mu, b)the mean isbase**mu / (1 - b**2 t**2)witht = ln(base)(the Laplace MGFE[e**(tL)] = e**(mu t)/(1 - b**2 t**2), which exists only for|b t| < 1), so recoveringmufrom a prediction taken to be that mean subtracts-ln(1 - b**2 t**2)/tin additive space. 0 on the linear scale (symmetric: mean = median).
- noise_params = ('scale',)¶
This family’s noise-parameter names in declaration order (ADR-0058). The first is the primary scalar passed as
noise; any others arrive in the trailingextramapping. Single-parameter families inherit('sigma',)or override it (Laplace’s('scale',), NegBinomial’s('dispersion',)); the one multi-parameter family is StudentT (('sigma', 'df')). This is the single source of truth for a family’s parameter names – the objective reads it to validate anoise_modelline’s fields.
- residual(prediction, observation, noise, extra=None)[source]¶
Laplace has no exact least-squares residual – it stays scalar-only (#459).
The sqrt-loss residual
sign(z) * sqrt(2 * data_fit) = sign(z) * sqrt(2|z|/b)(z = mu - forward(obs)) is~ sqrt|z|near the residual zero: a cusp with infinite slope at z=0 (and the IRLS weight1/|z| -> infthere too). L1 / least-absolute-deviation is inherently not cleanly least-squares, so – unlike Student-t, whose sqrt-loss residual is smooth through 0 (#459) – Laplace carries no residual a trust-region solver could minimize. Its gradient rides the scalard_data_fit_d_prediction()path instead (has_least_squares_residual()returns False for Laplace, so this is never reached on the normal path); a smoothed pseudo-Huber surrogate would be a separate, explicitly opt-in approximation of the loss, not exposed here.
- with_location(location)[source]¶
Return a copy of this family reinterpreting the prediction as a different distributional summary – the location axis (ADR-0011/0024/0031). Every noise family implements it (“every means every”): the location-scale families via the additive offset, the count family via a per-point CDF inversion. The base raises for a (hypothetical) family with no location axis.
Student-t observation noise (ADR-0058) – the robust-regression likelihood.
- class pybnf.noise.student_t.StudentT(additive_on=<pybnf.noise.scale._Linear object>, location=<pybnf.noise.location._Median object>)[source]¶
Student-t (heavy-tailed) observation noise – the outlier-robust likelihood, a Gaussian with a tail-heaviness knob. It is the first two-parameter noise family (ADR-0058): a location-scale family (like Gaussian/Laplace) carrying both a scale
sigmaand a shapedf(degrees of freedom, nu). Smalldfgives fat tails that downweight outliers (robust regression);df -> infrecovers the Gaussian. This is Stan’s/PyMC’sstudent_t(nu, mu, sigma), withmupinned to the prediction as for every family.Both noise parameters are independently sourced by the objective – each may be
fix_ata constant orfita free parameter – so a fit estimates 0, 1, or 2 noise parameters.dfis the one parameter with a default (DEFAULT_DF, a fixed 4): omit thedffield andnoise_param_defaultsfills it, giving the common fixed-nu robust recipe. Estimatingdfis statistically weakly identified (the likelihood is nearly flat in large nu), so pairdf = fit nu__FREEwith a positive prior (gamma / half_*, ADR-0057) – not enforced here.With
z = (mu - forward(obs)) / sigmathe per-point NLL splits (oracle:scipy.stats.t(df=nu, loc=mu, scale=sigma).logpdf):data_fit(always summed):(nu+1)/2 * log(1 + z**2/nu)– the parameter-dependent core (depends on sigma via z, and on nu).the
sigmanormalizerlog sigma– summed iff sigma is estimated.the
dfnormalizer-logGamma((nu+1)/2) + logGamma(nu/2) + 0.5*log(nu*pi)– summed iff df is estimated. When df is fixed this whole block is a constant the sampler drops; when df is free it is the term that keeps the fit honest. Either waylog_density(LOO/WAIC) includes it, so student_t needs no_density_constant– the “constant when fixed” the Gaussian carries as0.5*log(2*pi)is, for student_t, this estimated-gated normalizer (ADR-0058).
Configured by the same two axes as Gaussian – the scale its noise is additive on and the location interpretation – but exposed on the linear scale only (no
log_student_ttoken). On the linear scale t is symmetric, so mean = median = mu trivially. On a log scalebase**StudentThas no finite mean (its tails are too heavy for the MGF to exist, for any nu), solocation = meanon a log scale raises (only median centering is safe there) – the Laplace log-scale-mean guard (#419) taken to its limit.- d_data_fit_d_prediction(prediction, observation, noise, extra=None)[source]¶
d(data_fit)/d(prediction) = (nu+1) z/(nu + z**2) * forward'(pred)/sigma(#454), withz = (mu - forward(obs))/sigma. The factor(nu+1)/(nu + z**2)is the IRLS weightw(z)that downweights an outlier (largez);w(z)*zis the robustified residual. PyBNF emits only this scalar data-fit gradient for Student-t (no least-squares residual; the IRLS pseudo-residual is a possible later trust-region refinement). The location offset is prediction-independent, so MEAN and MEDIAN agree ond/d pred;forward'(pred) = 1on the linear scale.
- d_nll_d_noise_params(prediction, observation, noise, extra=None)[source]¶
The estimated-scale gradient columns
{'sigma': ..., 'df': ...}(#451/#454/#385) – the first multi-parameter estimated-noise gradient (ADR-0058). Each is the derivative ofdata_fitplus that parameter’s own normalizer (log sigmafor sigma, the df-block for df); the normalizers depend only on their own parameter, so the cross terms vanish. Withz = (mu - forward(obs))/sigma:d loss/d sigma = nu (1 - z**2) / (sigma (nu + z**2)) d loss/d nu = 1/2 log1p(z**2/nu) - (nu+1) z**2 / (2 nu (nu + z**2)) + 1/2 (digamma(nu/2) - digamma((nu+1)/2) + 1/nu)
The sigma column folds
d(log sigma)/d sigma = 1/sigmaintod(data_fit)/d sigmaand reduces to Gaussian’s(1 - z**2)/sigmaasnu -> inf; the df column folds the data fit’s nu-dependence with the df-block’s digamma derivative – the term that keeps a free df honest. The location offset is noise-independent wherever Student-t’s mean centering is defined – the linear scale, where the offset is 0; on a log scale Student-t has no finite mean, so mean-centering raises inmean_offsetregardless (nod_mean_offset_d_noisecoupling term is needed, unlike Gaussian/Laplace on a log scale, #385).
- d_residual_d_prediction(prediction, observation, noise, extra=None)[source]¶
d(r)/d(prediction) = (d r/d z) * forward'(pred)/sigma(#459), the residual-Jacobian seam paired withresidual(). Withu = z**2/nu:d r/d z = sqrt((nu+1)/nu) * sqrt(u / log1p(u)) / (1 + u)
the closed form of
d_data_fit_d_prediction / rthat stays finite at the residual zero: asz -> 0the factorsqrt(u/log1p(u)) -> 1, sod r/d z -> sqrt((nu+1)/nu)(the slope of the linearr ~ sqrt((nu+1)/nu) zcore) – the smoothz->0limit, where the naive(d data_fit/d pred)/ris0/0. The tiny-ucusp ofu/log1p(u)is series-guarded (1 + u/2).forward'(pred) = 1on the linear scale, the scale’s chain factor on a log scale; the offset is prediction-independent, so MEAN and MEDIAN agree.
- data_fit(prediction, observation, noise, extra=None)[source]¶
The parameter-dependent negative-log-likelihood term for one point.
noiseis the family’s primary scalar parameter;extrais a mapping of any secondary parameters ({'df': nu}for student_t),None/ empty for the single-parameter families, which ignore it (ADR-0058).
- mean_offset(noise)[source]¶
0 on the linear scale (t is symmetric: mean = median). On any log scale the original-space mean does not exist (the t-distribution’s tails are heavier than any exponential, so
E[base**T]diverges for every nu), so mean-centering is undefined – raise, directing the user tolocation = median(ADR-0058).
- noise_param_defaults = {'df': 4.0}¶
Default fixed values for parameters that may be omitted from a
noise_modelline (ADR-0058):{param_name: value}. The objective fills an omitted such parameter with aConstantSigmaof this value (student_t’s{'df': 4.0}). A parameter named here is optional; one absent here (e.g.sigma) is required. The value lives with the family (a distributional fact); the engine builds the source, keeping source construction out of the pure kernel (ADR-0011).
- noise_params = ('sigma', 'df')¶
This family’s noise-parameter names in declaration order (ADR-0058). The first is the primary scalar passed as
noise; any others arrive in the trailingextramapping. Single-parameter families inherit('sigma',)or override it (Laplace’s('scale',), NegBinomial’s('dispersion',)); the one multi-parameter family is StudentT (('sigma', 'df')). This is the single source of truth for a family’s parameter names – the objective reads it to validate anoise_modelline’s fields.
- param_normalizers(noise, extra=None)[source]¶
{param_name: separable normalizer}– the normalizer each noise parameter contributes, which the objective adds iff that parameter’s source is estimated (ADR-0058). The default attributes the family’s wholelog_normalizer()to its single primary parameter, so a one-parameter family needs no override; StudentT splitslog sigma('sigma') from the df-block ('df').
- residual(prediction, observation, noise, extra=None)[source]¶
The exact square-root-loss residual
r = sign(z) * sqrt(2 * data_fit) = sign(z) * sqrt((nu+1) * log1p(z**2/nu))withz = (mu - forward(obs))/sigma(#459) – the least-squares residual #386’s LM/TRF solver minimizes.Unlike the IRLS pseudo-residual
sqrt(w(z)) z, this satisfies both invariants:1/2 r**2 == data_fit(soscipy.least_squaresminimizes the true Student-t loss, not a frozen-weight reweighted surrogate) andr * d_residual_d_prediction == d_data_fit_d_prediction(so its residual-Jacobian reproduces the objective gradient). It is smooth through z=0:r ~ sqrt((nu+1)/nu) znear the origin (an odd, C-infinity function ofz), behaving like a Gaussian residual at the center and downweighting the tails aszgrows – which is why a fixed-scale Student-t fit isleast_squares_exact, the Gaussian’s exact-least-squares status recovered for the robust family (#459). (Laplace has no such clean form –sqrt(2*data_fit) ~ sqrt|z|is a cusp – so it stays scalar-only.)sign(0) == 0makes the residual exactly 0 atz=0.
- with_location(location)[source]¶
Return a copy of this family reinterpreting the prediction as a different distributional summary – the location axis (ADR-0011/0024/0031). Every noise family implements it (“every means every”): the location-scale families via the additive offset, the count family via a per-point CDF inversion. The base raises for a (hypothetical) family with no location axis.
Negative-binomial observation noise (ADR-0011, ADR-0031).
- class pybnf.noise.negative_binomial.NegBinomial(location=<pybnf.noise.location._Median object>)[source]¶
Negative-binomial observation noise for count data. The dispersion
ris the noise parameter (neg_binreads it from the config constantneg_bin_r;neg_bin_dynamicfrom ther__FREEfree parameter).The location axis (ADR-0011/0031) sets which distributional summary the prediction is taken to be. The default is
MEDIAN– median is the universal prediction-centering default for every noise family (ADR-0031, “every means every”), true in code at the constructor like Gaussian/Laplace.MEDIANinterprets the prediction as the 0.5-quantile and solves for the mean placing the continuous median there (issue #419).MEANis the native parameterization (the prediction is the mean) – the legacyneg_binobjfuncs pin it explicitly to stay frozen-mean. Unlike Gaussian/Laplace, the count family is not additive on a scale, so it owns this realization directly rather than going throughlocation.py’s additive-offset abstraction – it reuses theMEAN/MEDIANmarkers, not theoffsetmath.A negative observed count contributes nothing (the count-domain guard). A PMF is self-normalizing, so there is no separable normalizer (
log_normalizerstays 0) and the full-logpmflives indata_fit.- d_data_fit_d_prediction(prediction, observation, noise, extra=None)[source]¶
d(data_fit)/d(prediction)for one count point (#458, the deferred layer-G follow-up of #385). The count family carries no least-squares residual (its data fit is a-logpmf, not a sum of squares), so PyBNF emits only this scalar data-fit gradient.The chain
d(data_fit)/d(mean) * d(mean)/d(prediction). The mean-slope is the closed-form negative-binomial scorer (mean - obs) / (mean (r + mean))(r = noisethe dispersion); the mean-factor depends on the location interpretation:MEAN: the prediction is the mean, so
d mean/d pred = 1and the slope is that closed form directly – the clean case (neg_bin/neg_bin_dynamic).MEDIAN: the mean is the CDF inversion
_mean_for_medianplacing the continuous median at the prediction, sod mean/d predis the implicit derivative of that root-find,-(dG/d pred) / (dG/d mean)withG(mean, pred) = betainc(r, target+1, p) - 0.5andp = r/(r+mean).dG/d mean = -beta_pdf(p; r, target+1) * r/(r+mean)**2is the smooth beta-density chain rule (Fstrictly decreasing in the mean, so this is negative);dG/d predputs the prediction in the beta’s second parameter, so it is the non-elementaryd betainc/d b(_d_betainc_d_b()), timesd target/d pred = 1. The result is positive (a larger predicted median needs a larger mean).
A negative observation contributes nothing (the count-domain guard, mirroring
data_fit()). A prediction clamped to the count floor (pred <= 0, wheretargetfloors at 0 and the median stops moving) has slope 0 – a kink atpred == 0where PyBNF takes the floor-side subgradient, like the Laplace kink (#454). The gradient uses the un-clipped analytic form (data_fitclipsprobonly at pathological extremes).
- d_nll_d_noise_params(prediction, observation, noise, extra=None)[source]¶
{'dispersion': d(data_fit)/d r}– the estimated-dispersion gradient column for a freer(neg_bin_dynamic’sr__FREE; #458, generalizing layer D/G of #385).The negative-binomial PMF is self-normalizing (
log_normalizer == 0), so – unlike a Gaussian’s+log sigmaor a Laplace’slog(2 b)– there is no separable normalizer: the whole dispersion gradient lives in the data fit. Withmeanthe distribution mean andprob = r/(r+mean)the partial holding the mean fixed is the negative-binomial dispersion score:d(data_fit)/d r |_mean = psi(r) - psi(obs + r) - log(prob) - 1 + (r + obs)/(r + mean)
(
psithe digamma; the-logpmf’sr-dependence through its gamma terms andprob).MEAN: the prediction is the mean, r-independent, so this partial is the whole derivative.
MEDIAN: the mean is solved from
r(the CDF inversion_mean_for_median), so add the couplingd(data_fit)/d mean * d mean/d r.d mean/d ris the implicit derivative ofG(mean, r) = betainc(r, target+1, p) - 0.5 = 0(p = r/(r+mean)) holding the prediction fixed,-(dG/d r)/(dG/d mean);dG/d rbrings in the betainc first- parameter derivative (a = r,_d_betainc_d_a()) plusp’s ownr-dependence, anddG/d meanis the same beta-density chain rule as the prediction gradient. The count floor (target = max(pred, 0)) does not zero this – the mean still moves withreven at a clamped prediction. (Validated FD-first: the partial-only form is off 5-15%, sometimes far more, on the median.)
A negative observation contributes nothing (the count-domain guard).
- data_fit(prediction, observation, noise, extra=None)[source]¶
The parameter-dependent negative-log-likelihood term for one point.
noiseis the family’s primary scalar parameter;extrais a mapping of any secondary parameters ({'df': nu}for student_t),None/ empty for the single-parameter families, which ignore it (ADR-0058).
- noise_params = ('dispersion',)¶
This family’s noise-parameter names in declaration order (ADR-0058). The first is the primary scalar passed as
noise; any others arrive in the trailingextramapping. Single-parameter families inherit('sigma',)or override it (Laplace’s('scale',), NegBinomial’s('dispersion',)); the one multi-parameter family is StudentT (('sigma', 'df')). This is the single source of truth for a family’s parameter names – the objective reads it to validate anoise_modelline’s fields.
- with_location(location)[source]¶
Return a copy of this family reinterpreting the prediction as a different distributional summary – the location axis (ADR-0011/0024/0031). Every noise family implements it (“every means every”): the location-scale families via the additive offset, the count family via a per-point CDF inversion. The base raises for a (hypothetical) family with no location axis.
Noise axes¶
The additive-noise-scale axis (ADR-0011, ADR-0022): the scale a noise model’s noise is additive on.
This is distinct from a free parameter’s priors.Scale (the Parameter
Scale – the space a parameter is sampled in). They are different domain concepts
(see the CONTEXT.md glossary), so they are deliberately separate code – but they
share one log-base convention (ADR-0022): a bare “log” means log10 everywhere in
PyBNF, matching logvar / loguniform_var / lognormal_var and the
proposal arithmetic. Natural log is never implied; it exists only as the explicit
LN. So this axis has three named members: LINEAR, LOG10, and LN –
there is no ambiguous bare LOG. Gaussian noise additive on LINEAR is
ordinary additive error; additive on LOG10 is (log10) lognormal error.
A scale exposes ln_base – the natural log of its base (the t in
X = base**L = e**(t*L)): 0 on the linear scale, ln 10 on log10, 1 on
natural log. That is all a family’s moment-generating function needs to convert an
additive-space location into the original-space mean. The moment correction itself
is family-specific – Gaussian’s t*sigma**2/2 differs from Laplace’s
-ln(1 - b**2 t**2)/t (#419) – so it lives on each NoiseModel family (their
mean_offset), not here. The scale owns only the transform (forward) and its
base (ln_base).
- class pybnf.noise.scale.AdditiveNoiseScale[source]¶
Maps a value into the space a noise model’s noise is additive on.
ln_baseis the natural log of the scale’s base – the only thing a family’s moment correction needs from the scale (the family owns the correction itself).- dforward(x)[source]¶
d forward(x)/dx– the plain first derivative of the scale transform, the per-point seam a gradient needs (#452): the standardized residual on a log scale isrho = (forward(pred) - forward(obs))/sigma, sod rho/d pred = forward'(pred)/sigma.1on the linear scale (identity),1/(x ln 10)on log10,1/xon natural log – so the linear short-circuits to the historical1/sigmabyte-for-byte.forward’s sibling for the optimizer, aslog_abs_dforward()is for a normalized density: this is the signed derivative the chain rule multiplies, that one the log-absolute change-of- variables term a density carries; deliberately the Additive Noise Scale axis, not a parameter’spriors.Scale(the CONTEXT.md glossary keeps them apart).
- log_abs_dforward(x)[source]¶
log|d forward(x)/dx|– the change-of-variables Jacobian term that turns an additive-space log-density into the original-space (data-space) log-density,log p_X(x) = log p_L(forward(x)) + log|d forward/dx|(ADR-0056). It is 0 on the linear scale (identity transform) and the only thing besides a family’s own normalizer that a normalized per-point likelihood (LOO/WAIC) needs that the optimizer/sampler never did – the Jacobian is constant in the parameters, so it cancels in every accept ratio and PyBNF’snllomits it.forward’s sibling:forwardmoves the value, this accounts for the density’s stretch under that move.
The location-interpretation axis (ADR-0011): which summary of a noise model’s distribution the deterministic prediction is taken to be.
PEtab v2 hardcodes the median; PyBNF makes it an explicit, overridable choice. It
only bites when the noise is asymmetric on the prediction’s scale: on a symmetric
(e.g. linear-additive Gaussian) noise model mean = median = mode, so every
location coincides. A LocationInterpretation returns the additive-space offset
to subtract from scale.forward(prediction) to obtain the family’s location
parameter.
The median commutes with the monotone scale transform, so its offset is 0 on
any scale – which is why it is the clean, scale-agnostic default. The mean
picks up the family’s moment correction in the transformed space – which is
family-specific (Gaussian’s differs from Laplace’s, #419), so it is delegated
to the noise model’s own mean_offset rather than computed here. MODE is not
modeled until a use exercises it.
Noise parameter sources¶
The SigmaSource abstraction (ADR-0021): where a noise model’s noise
parameter comes from.
ADR-0011 made the NoiseModel a pure per-point kernel and named the noise
parameter source as the objective wrapper’s job, but left it hard-coded – the
_SD data column, the magic free parameters sigma__FREE / r__FREE, and
the neg_bin_r constant each lived in a different objfunc subclass. This module
lifts those three into one first-class abstraction so they can be selected per
observable (the #410 engine) and named freely (dissolving the magic strings, as
M2.3 did for priors).
A SigmaSource answers two questions the per-point engine needs: its
value(...) for one observation, and whether it is estimated. The
estimated flag is load-bearing – it is what decides whether the family’s
likelihood normalizer is summed: a fixed source (a data column, a constant)
contributes only the family’s data_fit, while an estimated source (a free
parameter) contributes the full nll including the normalizer. This is exactly
ADR-0011’s “normalizer retained iff the noise parameter is estimated”, now keyed
off the source rather than hard-coded per objfunc.
- class pybnf.noise.source.ColumnMeanSigma[source]¶
The noise parameter set to the observable’s experimental column mean – one scalar shared by every point of the column (the native
column_meansource, ADR-0031). This is the heteroscedastic-across-observables model the legacyave_norm_sosobjfunc fits – each variable’s residuals are normalized by that variable’s own scale (its mean), so a large-magnitude observable does not dominate a small one (sigma = ybarreproduces((sim - exp) / ybar)**2up to the proper1/2). Fixed (it is data, not estimated), so the caller drops the likelihood normalizer.
- class pybnf.noise.source.ConstantSigma(value)[source]¶
The noise parameter held at a fixed configuration constant (
neg_bin’sneg_bin_r; the nativefix_atsource). Fixed, so the caller drops the likelihood normalizer.
- class pybnf.noise.source.DataColumnSigma(suffix='_SD')[source]¶
The noise parameter read per point from an experimental-data column named
<observable><suffix>(chi_sq/lognormal: the_SDcolumn). It is a fixed source – the value is data, not estimated – so the caller drops the likelihood normalizer. The suffix is explicit (default_SD) so a non-Gaussian family can read a differently-named column without the “standard deviation” misnomer (ADR-0021).
- class pybnf.noise.source.FormulaSigma(formula, names=None)[source]¶
The noise parameter given by an expression over free parameters (+ constants), evaluated against the current PSet at each point (the native
formulasource, ADR-0044).PEtab lets the
noiseFormulabe an arithmetic expression – e.g.0.1 + 0.05*scalingafter a per-observableobservableParameterplaceholder is substituted in – a per-observable sigma that is neither a data column (DataColumnSigma) nor a single free parameter (FreeParameterSigma). This source closes that gap: it holds a PEtab-math expression string and, lazily on first use, compiles it to a vectorizednumpycallable over its free symbols (the same compile-once-per-worker pattern asMeasurementModel, ADR-0036 – alambdifyd callable is not picklable, so it is excluded from__getstate__and rebuilt worker-side).It is estimated (it reads estimated parameters), so the caller keeps the family’s likelihood normalizer (ADR-0011). Its free symbols are the nuisance free parameters the fit must declare (
required_free_params()); they resolve fromowner._pset_valuesexactly asFreeParameterSigma’s single name does.- formula¶
The PEtab-math expression (over free-parameter ids + numeric constants).
- names¶
The ordered free-symbol names (PSet keys at eval time);
Noneuntil first resolved (a parse), then the compiler’s canonical sorted order. Picklable.
- required_free_params()[source]¶
The set of free-parameter names this source requires the fit to declare. Defaults to the single
required_free_param()(or empty);FormulaSigmaoverrides it because an expression sigma references several (ADR-0044). The objective unions these across its sources (required_free_noise_params).
- class pybnf.noise.source.FreeParameterSigma(name)[source]¶
The noise parameter estimated as a free parameter, resolved by name from the pset (
chi_sq_dynamic’ssigma__FREE,neg_bin_dynamic’sr__FREE, or a per-observable__FREEparameter). It is estimated, so the caller keeps the likelihood normalizer.
- class pybnf.noise.source.PerMeasurementFormulaSigma(formula, names=None)[source]¶
The noise parameter given by an expression that references a row-varying PEtab per-measurement placeholder, bound per data point from the experimental data’s binding table (the native
formulasource over a placeholder, ADR-0045).ADR-0044’s
FormulaSigmacovers a noiseFormula whose placeholder is constant across an observable’s rows (substituted away to a per-observable σ). When the placeholder’snoiseParameterstoken instead differs row to row – a per-condition or per-timepoint estimated σ – it cannot be substituted to one symbol; it must be bound at scoring time from the row’s token. This source keeps the placeholder as a free symbol of the (cached, lambdified) expression and, atvalue(owner, exp_data, exp_row, col_name), readsexp_data.measurement_params[col_name][placeholder][exp_row]for the row’s token – a number binds as itself, a parameter id resolves fromowner._pset_values(a per-row estimated nuisance, ADR-0034). Any non-placeholder free symbol resolves from the PSet exactly asFormulaSigma’s do.It is estimated (a per-row token id is an estimated parameter), lazy-compiled, and not pickled (the same compile-once-per-worker pattern as
FormulaSigma). The binding table lives on the experimentalData(carried there byconfig.pyfrom the importer’s sidecar), not on the source, so the source survives dask scatter independently of the data.- formula¶
The PEtab-math expression, with its per-measurement placeholder(s) kept as symbols.
- names¶
The ordered free-symbol names (placeholders + fixed PSet names);
Noneuntil a parse resolves them, then the compiler’s canonical sorted order. Picklable.
- required_free_params()[source]¶
The set of free-parameter names this source requires the fit to declare. Defaults to the single
required_free_param()(or empty);FormulaSigmaoverrides it because an expression sigma references several (ADR-0044). The objective unions these across its sources (required_free_noise_params).
- class pybnf.noise.source.RelativeSigma(cv=1.0)[source]¶
The noise parameter proportional to the measurement: constant-coefficient-of- variation noise,
sigma = cv * |observation|(the nativerelativesource, ADR-0031). This is the honest heteroscedastic noise model the legacynorm_sosobjfunc fits – a Gaussian whose standard deviation scales with the data, so the squared residual is normalized per point by the measurement (cv = 1reproduces((sim - exp) / exp)**2up to the proper1/2). Thecvis a fixed constant, so this source is fixed and the caller drops the likelihood normalizer.
- class pybnf.noise.source.SigmaSource[source]¶
Where a noise model reads its noise parameter for one observation.
estimateddecides normalizer inclusion (see the module docstring); it isFalseby default and overridden toTrueby the free-parameter source.- exp_column(col_name)[source]¶
The experimental-data column this source consumes, or
Noneif it reads no data column – so_check_columnscan exempt it from the unused-column error.
- required_free_param()[source]¶
The
__FREEparameter name this source requires the fit to declare, orNoneif it sources no free parameter (used by_load_variablesvalidation, ADR-0021).
- required_free_params()[source]¶
The set of free-parameter names this source requires the fit to declare. Defaults to the single
required_free_param()(or empty);FormulaSigmaoverrides it because an expression sigma references several (ADR-0044). The objective unions these across its sources (required_free_noise_params).