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, override log_normalizer (one parameter) or param_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 through d(prediction)/d(theta) (layer G, #454/#385).

This is the universal scalar-gradient seam: an asymmetric family (Laplace, Student-t) whose data_fit is not a sum of squares carries no least-squares residual, so its objective gradient is assembled as sum_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’s ln(base)*sigma, Laplace’s 2 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 like mean_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_point adds each parameter’s normalizer iff estimated – so the returned derivative folds in the parameter’s own normalizer (Gaussian’s log 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 with residual() (#459). The assembly multiplies it by the forward sensitivity d pred/d theta to build the residual-Jacobian column, exactly as for a Gaussian.

Equal to d_data_fit_d_prediction / residual away from z=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 override residual() (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. noise is the family’s primary scalar parameter; extra is 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.

nll is 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 matches scipy.stats.<dist>.logpdf / .logpmf (the oracle each family documents). The count family carries no additive_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’s eval_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 noise is fixed. Zero unless the family has a separable normalizer (e.g. Gaussian’s log sigma); a self-normalizing PMF (NegBinomial) leaves it at 0. A multi-parameter family overrides param_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, via location.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_model line (ADR-0058): {param_name: value}. The objective fills an omitted such parameter with a ConstantSigma of 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 trailing extra mapping. 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 a noise_model line’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 whole log_normalizer() to its single primary parameter, so a one-parameter family needs no override; StudentT splits log sigma ('sigma') from the df-block ('df').

residual(prediction, observation, noise, extra=None)[source]

The signed square-root-loss residual r for 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_fit and r * d_residual_d_prediction == d_data_fit_d_prediction – i.e. r reproduces both this point’s loss and its prediction gradient, so scipy.least_squares minimizes the true objective (not a frozen-weight IRLS surrogate). The canonical form is r = sign(z) * sqrt(2 * data_fit) with z = (mu - forward(obs))/sigma the additive-space residual: for a Gaussian this is exactly the standardized residual rho (its data fit is 1/2 rho**2); for Student-t the sqrt-loss residual is smooth through z=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|/b has a cusp at z=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 scalar d_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 (the lognormal objfunc); 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). (LN gives the natural-log lognormal density; ADR-0022.)

data_fit is the squared residual in the additive space, (mu - forward(obs))^2 / (2 sigma^2), where mu is the additive-space location parameter; log_normalizer is log sigma. With a fixed sigma the caller drops the normalizer (and, on the log scale, the parameter-independent Jacobian) – so chi_sq/lognormal sum only data_fit – while a free sigma keeps the full nll (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 scale forward'(pred) = 1. Equal to rho * d rho/d pred (the residual_point pair) – the Gaussian is the one family whose data fit is exactly 1/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, where ln(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 sigma has the symmetric -rho**2/sigma part plus d(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 offset mu = forward(pred) - offset itself depends on sigma there (offset = ln(base)*sigma**2/2), so rho carries an extra -rho * (d offset/d sigma)/sigma with d offset/d sigma = ln(base)*sigma, giving -ln(base)*rho (#385). d(offset)/d sigma is 0 for the MEDIAN and on the linear scale, so this reduces to (1 - rho**2)/sigma byte-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): 1 on 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. noise is the family’s primary scalar parameter; extra is 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 noise is fixed. Zero unless the family has a separable normalizer (e.g. Gaussian’s log sigma); a self-normalizing PMF (NegBinomial) leaves it at 0. A multi-parameter family overrides param_normalizers() instead.

mean_offset(noise)[source]

The Gaussian moment correction ln(base)*sigma**2/2 (0 on the linear scale): the mean of base**N(mu, sigma) is base**(mu + ln(base)*sigma**2/2), so recovering mu from 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 trailing extra mapping. 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 a noise_model line’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 whose data_fit is exactly 1/2 rho**2, so rho is already the signed square-root-loss residual (sign(z) sqrt(2 data_fit) == rho identically); 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’s noiseDistribution = 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 default LINEAR, MEDIAN); on a log scale (log-laplace) the distribution is asymmetric in the original space, so location = mean picks up the Laplace moment correction (mean_offset), which is not Gaussian’s (#419). data_fit is |mu - forward(obs)| / b with the scale b as the noise parameter; log_normalizer is log(2 b). With a fixed b the caller drops the normalizer; with a free b (the laplace objfunc’s b__FREE) it keeps the full nll – the log(2 b) term is exactly what prevents the fit from driving b -> 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)|/b is non-smooth where mu == 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) = 1 on 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 scale b, folded into the estimated-scale gradient column when a free Laplace scale centers a MEAN on a log scale (#385). Shares mean_offset’s b*ln(base) < 1 domain (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} with R = mu - forward(obs) – the estimated-scale gradient column (#451/#454/#385). The -|R|/b**2 is d(data_fit)/d b holding the offset fixed and 1/b is d(log(2 b))/d b (the normalizer that keeps a free Laplace scale from running to infinity); the -sign(R) * (d offset/d b)/b is the coupling that appears when a MEAN is centered on a log scale, where mu = forward(pred) - offset and the offset depends on b (d offset/d b = 2 b t/(1 - b**2 t**2)). d(offset)/d b is 0 for the MEDIAN and on the linear scale, so this reduces to -|R|/b**2 + 1/b byte-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 – see residual() (#459).

data_fit(prediction, observation, noise, extra=None)[source]

The parameter-dependent negative-log-likelihood term for one point. noise is the family’s primary scalar parameter; extra is 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 noise is fixed. Zero unless the family has a separable normalizer (e.g. Gaussian’s log sigma); a self-normalizing PMF (NegBinomial) leaves it at 0. A multi-parameter family overrides param_normalizers() instead.

mean_offset(noise)[source]

The Laplace moment correction (distinct from Gaussian’s, #419). For base**Laplace(mu, b) the mean is base**mu / (1 - b**2 t**2) with t = ln(base) (the Laplace MGF E[e**(tL)] = e**(mu t)/(1 - b**2 t**2), which exists only for |b t| < 1), so recovering mu from a prediction taken to be that mean subtracts -ln(1 - b**2 t**2)/t in 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 trailing extra mapping. 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 a noise_model line’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 weight 1/|z| -> inf there 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 scalar d_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 sigma and a shape df (degrees of freedom, nu). Small df gives fat tails that downweight outliers (robust regression); df -> inf recovers the Gaussian. This is Stan’s/PyMC’s student_t(nu, mu, sigma), with mu pinned to the prediction as for every family.

Both noise parameters are independently sourced by the objective – each may be fix_at a constant or fit a free parameter – so a fit estimates 0, 1, or 2 noise parameters. df is the one parameter with a default (DEFAULT_DF, a fixed 4): omit the df field and noise_param_defaults fills it, giving the common fixed-nu robust recipe. Estimating df is statistically weakly identified (the likelihood is nearly flat in large nu), so pair df = fit nu__FREE with a positive prior (gamma / half_*, ADR-0057) – not enforced here.

With z = (mu - forward(obs)) / sigma the 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 sigma normalizer log sigma – summed iff sigma is estimated.

  • the df normalizer -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 way log_density (LOO/WAIC) includes it, so student_t needs no _density_constant – the “constant when fixed” the Gaussian carries as 0.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_t token). On the linear scale t is symmetric, so mean = median = mu trivially. On a log scale base**StudentT has no finite mean (its tails are too heavy for the MGF to exist, for any nu), so location = mean on 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), with z = (mu - forward(obs))/sigma. The factor (nu+1)/(nu + z**2) is the IRLS weight w(z) that downweights an outlier (large z); w(z)*z is 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 on d/d pred; forward'(pred) = 1 on 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 of data_fit plus that parameter’s own normalizer (log sigma for sigma, the df-block for df); the normalizers depend only on their own parameter, so the cross terms vanish. With z = (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/sigma into d(data_fit)/d sigma and reduces to Gaussian’s (1 - z**2)/sigma as nu -> 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 in mean_offset regardless (no d_mean_offset_d_noise coupling 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 with residual(). With u = 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 / r that stays finite at the residual zero: as z -> 0 the factor sqrt(u/log1p(u)) -> 1, so d r/d z -> sqrt((nu+1)/nu) (the slope of the linear r ~ sqrt((nu+1)/nu) z core) – the smooth z->0 limit, where the naive (d data_fit/d pred)/r is 0/0. The tiny-u cusp of u/log1p(u) is series-guarded (1 + u/2). forward'(pred) = 1 on 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. noise is the family’s primary scalar parameter; extra is 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 to location = median (ADR-0058).

noise_param_defaults = {'df': 4.0}

Default fixed values for parameters that may be omitted from a noise_model line (ADR-0058): {param_name: value}. The objective fills an omitted such parameter with a ConstantSigma of 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 trailing extra mapping. 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 a noise_model line’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 whole log_normalizer() to its single primary parameter, so a one-parameter family needs no override; StudentT splits log 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)) with z = (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 (so scipy.least_squares minimizes the true Student-t loss, not a frozen-weight reweighted surrogate) and r * 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) z near the origin (an odd, C-infinity function of z), behaving like a Gaussian residual at the center and downweighting the tails as z grows – which is why a fixed-scale Student-t fit is least_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) == 0 makes the residual exactly 0 at z=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 r is the noise parameter (neg_bin reads it from the config constant neg_bin_r; neg_bin_dynamic from the r__FREE free 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. MEDIAN interprets the prediction as the 0.5-quantile and solves for the mean placing the continuous median there (issue #419). MEAN is the native parameterization (the prediction is the mean) – the legacy neg_bin objfuncs 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 through location.py’s additive-offset abstraction – it reuses the MEAN/MEDIAN markers, not the offset math.

A negative observed count contributes nothing (the count-domain guard). A PMF is self-normalizing, so there is no separable normalizer (log_normalizer stays 0) and the full -logpmf lives in data_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 score r (mean - obs) / (mean (r + mean)) (r = noise the dispersion); the mean-factor depends on the location interpretation:

  • MEAN: the prediction is the mean, so d mean/d pred = 1 and the slope is that closed form directly – the clean case (neg_bin / neg_bin_dynamic).

  • MEDIAN: the mean is the CDF inversion _mean_for_median placing the continuous median at the prediction, so d mean/d pred is the implicit derivative of that root-find, -(dG/d pred) / (dG/d mean) with G(mean, pred) = betainc(r, target+1, p) - 0.5 and p = r/(r+mean). dG/d mean = -beta_pdf(p; r, target+1) * r/(r+mean)**2 is the smooth beta-density chain rule (F strictly decreasing in the mean, so this is negative); dG/d pred puts the prediction in the beta’s second parameter, so it is the non-elementary d betainc/d b (_d_betainc_d_b()), times d 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, where target floors at 0 and the median stops moving) has slope 0 – a kink at pred == 0 where PyBNF takes the floor-side subgradient, like the Laplace kink (#454). The gradient uses the un-clipped analytic form (data_fit clips prob only 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 free r (neg_bin_dynamic’s r__FREE; #458, generalizing layer D/G of #385).

The negative-binomial PMF is self-normalizing (log_normalizer == 0), so – unlike a Gaussian’s +log sigma or a Laplace’s log(2 b) – there is no separable normalizer: the whole dispersion gradient lives in the data fit. With mean the distribution mean and prob = 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)

(psi the digamma; the -logpmf’s r-dependence through its gamma terms and prob).

  • 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 coupling d(data_fit)/d mean * d mean/d r. d mean/d r is the implicit derivative of G(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 r brings in the betainc first- parameter derivative (a = r, _d_betainc_d_a()) plus p’s own r-dependence, and dG/d mean is 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 with r even 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. noise is the family’s primary scalar parameter; extra is 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 trailing extra mapping. 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 a noise_model line’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_base is 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 is rho = (forward(pred) - forward(obs))/sigma, so d rho/d pred = forward'(pred)/sigma. 1 on the linear scale (identity), 1/(x ln 10) on log10, 1/x on natural log – so the linear short-circuits to the historical 1/sigma byte-for-byte. forward’s sibling for the optimizer, as log_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’s priors.Scale (the CONTEXT.md glossary keeps them apart).

forward(x)[source]

Transform an original-space value into the additive space.

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’s nll omits it. forward’s sibling: forward moves 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.

class pybnf.noise.location.LocationInterpretation[source]

The offset to subtract from scale.forward(prediction) to recover a family’s additive-space location parameter – 0 for the median, the family’s moment correction for the mean.

d_offset_d_noise(noise_model, noise)[source]

d(offset)/d(noise parameter) – the offset’s own dependence on the noise scale, which the estimated-scale gradient column needs (#385). 0 for the median (its offset is identically 0); the family’s moment-correction derivative for the mean.

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_mean source, ADR-0031). This is the heteroscedastic-across-observables model the legacy ave_norm_sos objfunc 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 = ybar reproduces ((sim - exp) / ybar)**2 up to the proper 1/2). Fixed (it is data, not estimated), so the caller drops the likelihood normalizer.

value(owner, exp_data, exp_row, col_name)[source]

The noise-parameter value for one observation. owner is the objective (used by the free-parameter source to read the resolved pset values); exp_data/exp_row/col_name locate the point (used by the data- column source).

class pybnf.noise.source.ConstantSigma(value)[source]

The noise parameter held at a fixed configuration constant (neg_bin’s neg_bin_r; the native fix_at source). Fixed, so the caller drops the likelihood normalizer.

value(owner, exp_data, exp_row, col_name)[source]

The noise-parameter value for one observation. owner is the objective (used by the free-parameter source to read the resolved pset values); exp_data/exp_row/col_name locate the point (used by the data- column source).

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 _SD column). 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).

exp_column(col_name)[source]

The experimental-data column this source consumes, or None if it reads no data column – so _check_columns can exempt it from the unused-column error.

value(owner, exp_data, exp_row, col_name)[source]

The noise-parameter value for one observation. owner is the objective (used by the free-parameter source to read the resolved pset values); exp_data/exp_row/col_name locate the point (used by the data- column source).

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 formula source, ADR-0044).

PEtab lets the noiseFormula be an arithmetic expression – e.g. 0.1 + 0.05*scaling after a per-observable observableParameter placeholder 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 vectorized numpy callable over its free symbols (the same compile-once-per-worker pattern as MeasurementModel, ADR-0036 – a lambdifyd 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 from owner._pset_values exactly as FreeParameterSigma’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); None until 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); FormulaSigma overrides it because an expression sigma references several (ADR-0044). The objective unions these across its sources (required_free_noise_params).

value(owner, exp_data, exp_row, col_name)[source]

The noise-parameter value for one observation. owner is the objective (used by the free-parameter source to read the resolved pset values); exp_data/exp_row/col_name locate the point (used by the data- column source).

class pybnf.noise.source.FreeParameterSigma(name)[source]

The noise parameter estimated as a free parameter, resolved by name from the pset (chi_sq_dynamic’s sigma__FREE, neg_bin_dynamic’s r__FREE, or a per-observable __FREE parameter). It is estimated, so the caller keeps the likelihood normalizer.

required_free_param()[source]

The __FREE parameter name this source requires the fit to declare, or None if it sources no free parameter (used by _load_variables validation, ADR-0021).

value(owner, exp_data, exp_row, col_name)[source]

The noise-parameter value for one observation. owner is the objective (used by the free-parameter source to read the resolved pset values); exp_data/exp_row/col_name locate the point (used by the data- column source).

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 formula source over a placeholder, ADR-0045).

ADR-0044’s FormulaSigma covers a noiseFormula whose placeholder is constant across an observable’s rows (substituted away to a per-observable σ). When the placeholder’s noiseParameters token 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, at value(owner, exp_data, exp_row, col_name), reads exp_data.measurement_params[col_name][placeholder][exp_row] for the row’s token – a number binds as itself, a parameter id resolves from owner._pset_values (a per-row estimated nuisance, ADR-0034). Any non-placeholder free symbol resolves from the PSet exactly as FormulaSigma’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 experimental Data (carried there by config.py from 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); None until 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); FormulaSigma overrides it because an expression sigma references several (ADR-0044). The objective unions these across its sources (required_free_noise_params).

value(owner, exp_data, exp_row, col_name)[source]

The noise-parameter value for one observation. owner is the objective (used by the free-parameter source to read the resolved pset values); exp_data/exp_row/col_name locate the point (used by the data- column source).

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 native relative source, ADR-0031). This is the honest heteroscedastic noise model the legacy norm_sos objfunc fits – a Gaussian whose standard deviation scales with the data, so the squared residual is normalized per point by the measurement (cv = 1 reproduces ((sim - exp) / exp)**2 up to the proper 1/2). The cv is a fixed constant, so this source is fixed and the caller drops the likelihood normalizer.

value(owner, exp_data, exp_row, col_name)[source]

The noise-parameter value for one observation. owner is the objective (used by the free-parameter source to read the resolved pset values); exp_data/exp_row/col_name locate the point (used by the data- column source).

class pybnf.noise.source.SigmaSource[source]

Where a noise model reads its noise parameter for one observation.

estimated decides normalizer inclusion (see the module docstring); it is False by default and overridden to True by the free-parameter source.

exp_column(col_name)[source]

The experimental-data column this source consumes, or None if it reads no data column – so _check_columns can exempt it from the unused-column error.

required_free_param()[source]

The __FREE parameter name this source requires the fit to declare, or None if it sources no free parameter (used by _load_variables validation, 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); FormulaSigma overrides it because an expression sigma references several (ADR-0044). The objective unions these across its sources (required_free_noise_params).

abstractmethod value(owner, exp_data, exp_row, col_name)[source]

The noise-parameter value for one observation. owner is the objective (used by the free-parameter source to read the resolved pset values); exp_data/exp_row/col_name locate the point (used by the data- column source).