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

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

The expected per-point Fisher information of the location kappa – the Gauss-Newton curvature the EFIM trust-region path (fit_type = gntr, #481) weights d(prediction)/d(theta) by to build its Hessian H = sum_i kappa_i s_i s_i^T. kappa is the expected E[-d^2 log p / d mu^2] carried through the prediction: for a location-scale family additive on a scale it is (forward'(pred))**2 * I_additive (the unit location Fisher in the additive space times the scale’s chain factor squared).

Only a non-residual family in scope overrides it – Laplace ((forward'(pred))**2 / b**2) and the count family (its mean’s Fisher, through the location realization). A residual-bearing family (Gaussian, Student-t) is never asked: the assembly reads its location curvature straight off the residual Jacobian (d_residual_d_prediction**2, which is the Fisher there), so it takes that path. The base raises, so an unsupported family/config refuses the EFIM step with a pointer back to the scalar-gradient (L-BFGS-B) path.

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_param_fisher(prediction, observation, noise, extra=None)[source]

{param_name: I_scale} – the expected Fisher information of each estimated noise parameter, the diagonal noise block of the EFIM Hessian (fit_type = gntr, #481). The assembly reads it only for a parameter whose source is estimated (a free parameter), exactly as noise_grad_point() emits a column only for an estimated parameter, and accumulates w_i * I_scale * outer(e_param, e_param) (the free parameter is the noise parameter, so the curvature direction is that parameter’s own coordinate). The location-scale cross Fisher is 0 for the symmetric families on linear/MEDIAN, so the block is diagonal there – a family whose estimated scale couples to the location (a MEAN on a log scale) refuses instead.

Overridden by the families whose estimated-scale block this cut assembles – Gaussian ({'sigma': 2/sigma**2}) and Laplace ({'scale': 1/b**2}). The base raises, which is exactly the refusal for an out-of-scope estimated scale (the count family’s free dispersion, the Student-t 2-parameter block): the fit falls back to the scalar-gradient (L-BFGS-B) path.

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_param_fisher(prediction, observation, noise, extra=None)[source]

{'sigma': 2/sigma**2} – the expected Fisher information of an estimated Gaussian scale, the noise block of the EFIM Hessian (fit_type = gntr, #481). With the per-point loss R**2/(2 sigma**2) + log sigma (R the additive-space residual), d^2/d sigma^2 = 3 R**2/sigma^4 - 1/sigma^2 and E[R**2] = sigma**2, so E[d^2] = 3/sigma^2 - 1/sigma^2 = 2/sigma^2. Using the expected value (not the data-dependent observed second derivative, which can go negative) is what keeps the block positive-definite. The location-scale cross Fisher is 0 by symmetry (E[d^2/d mu d sigma] = -2 E[R]/sigma^3 = 0), so the block is diagonal – except a MEAN centered on a log scale, where the moment offset mu = forward(pred) - offset(sigma) couples location and scale; that corner is refused (its scalar gradient still fits under fit_type = lbfgs).

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

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

kappa = (forward'(pred))**2 / b**2 – the Laplace location Fisher, the Gauss-Newton curvature the EFIM trust-region path (fit_type = gntr, #481) weights d(prediction)/d(theta) by. The unit Fisher of a Laplace location is 1/b**2 (E[(sign(R)/b)**2] = 1/b**2); the scale’s chain factor forward'(pred) carries it to the prediction (1 on the linear scale). Laplace is not residual-bearing (its L1 data fit is the cusp sqrt|z|, #459), so the assembly reads its location curvature here rather than off a residual Jacobian. Holds for MEDIAN and MEAN alike – the offset is prediction-independent, so d mu/d pred = forward'(pred) either way. Note the observed second derivative of |R|/b is 0 almost everywhere; the expected Fisher 1/b**2 is the well-posed curvature (why an EFIM step, not a plain Newton step, is what makes L1 tractable).

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_param_fisher(prediction, observation, noise, extra=None)[source]

{'scale': 1/b**2} – the expected Fisher of an estimated Laplace scale, the noise block of the EFIM Hessian (fit_type = gntr, #481). With the per-point loss |R|/b + log(2 b), d^2/d b^2 = 2|R|/b^3 - 1/b^2 and E[|R|] = b, so E[d^2] = 2/b^2 - 1/b^2 = 1/b^2. Diagonal (cross-Fisher 0 by symmetry), except a MEAN on a log scale, refused as for Gaussian (its scalar gradient still fits under fit_type = lbfgs).

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 mean-slope divides by the mean, so it reads it through _mean_for_slope() – the value path’s prob clip expressed on the mean – which keeps a MEAN-centered zero prediction finite instead of nan; away from that floor the form is the un-clipped analytic one.

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). The mean is read through _mean_for_slope() for consistency with d_data_fit_d_prediction() – it matters only for the MEDIAN coupling term, which divides by the mean; the MEAN partial above is finite at a zero prediction either way, and the floor shifts it by O(_PROB_CLIP). MEDIAN centering never reaches the floor.

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

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

kappa = I_mean = r / (mean (r + mean)) – the negative-binomial mean’s Fisher information, the Gauss-Newton curvature the EFIM trust-region path (fit_type = gntr, #481) weights d(prediction)/d(theta) by. It is the variance of the mean score d(-logpmf)/d mean = r (mean - obs) / (mean (r + mean)) (Var(obs) = mean (r + mean)/r, so I_mean = [r/(mean(r+mean))]**2 * Var(obs) = r/(mean(r+mean))).

  • MEAN: the prediction is the mean (d mean/d pred = 1), so kappa is this directly – the case this cut supports (neg_bin / neg_bin_dynamic pinned to MEAN).

  • MEDIAN: the mean sits behind the betainc CDF inversion (_mean_for_median), whose d mean/d pred is the non-elementary implicit derivative; its location Fisher is out of scope for this cut – refused, pointing at fit_type = lbfgs (which fits it via the scalar data-fit gradient). A negative observation contributes no curvature (the count-domain guard, mirroring data_fit()).

A zero mean deliberately reports no curvature rather than the _mean_for_slope() floor the gradient uses. The two want opposite things: the true I_mean diverges as 1/mean, so flooring it would put a ~1/(r _PROB_CLIP) weight on every gated-off row (an epidemic model’s whole pre-t0 stretch) and let those rows dominate the Fisher matrix, crushing the trust-region step. Zero says “this row carries no information about the mean”, which is the stable reading and leaves the gradient – which does floor – to supply the descent direction.

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, sim_data, sim_row, 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); sim_data/sim_row locate the point on the simulated trajectory (read only by PredictionFormulaSigma, whose σ depends on the model prediction – ADR-0075); exp_data/exp_row/col_name locate the point on the experimental data (used by the data-column / relative / column-mean sources). Every source that does not read the simulation ignores sim_data/sim_row.

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, sim_data, sim_row, 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); sim_data/sim_row locate the point on the simulated trajectory (read only by PredictionFormulaSigma, whose σ depends on the model prediction – ADR-0075); exp_data/exp_row/col_name locate the point on the experimental data (used by the data-column / relative / column-mean sources). Every source that does not read the simulation ignores sim_data/sim_row.

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, sim_data, sim_row, 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); sim_data/sim_row locate the point on the simulated trajectory (read only by PredictionFormulaSigma, whose σ depends on the model prediction – ADR-0075); exp_data/exp_row/col_name locate the point on the experimental data (used by the data-column / relative / column-mean sources). Every source that does not read the simulation ignores sim_data/sim_row.

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.

Gradient (#505). As a PSet-only composite scale it is the easy half of the estimated-scale gradient frontier: every symbol resolves from owner._pset_values (a coefficient, never a prediction column), so sigma_sensitivity() is PredictionFormulaSigma’s σ-formula chain rule minus the prediction-coupling term – ∂σ/∂θ = Σ_coeff (∂σ/∂coeff)·e_coeff, purely on the coefficient columns. It strictly generalizes FreeParameterSigma (a single symbol, ∂σ/∂name = 1) to several coefficients that may couple off-diagonal in the noise block, and its unit-vector column feeds both the scalar (L-BFGS) gradient and the EFIM Fisher block unchanged (ADR-0079/0080).

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

sigma_sensitivity(owner, sim_data, sim_row, exp_data, exp_row, col_name, raw_sens, index)[source]

∂σ/∂θ for a PSet-only composite scale (#505) – a (len(index),) native-space vector, PredictionFormulaSigma.sigma_sensitivity minus the prediction-coupling branch. Every symbol of the formula resolves from owner._pset_values (a coefficient – there is no sim column and hence no term C), so the chain rule ∂σ/∂θ = Σ_coeff (∂σ/∂coeff)·(∂coeff/∂θ) lands each partial straight on that coefficient’s own column (∂coeff/∂θ = e_coeff). Reduces to FreeParameterSigma’s unit vector for a single-symbol formula (∂σ/∂name = 1); several coefficients simply populate several columns, coupling off-diagonal in the outer-product noise block (still PSD). sim_data/sim_row/exp_data/exp_row/raw_sens are unused (the σ reads only the PSet).

value(owner, sim_data, sim_row, 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); sim_data/sim_row locate the point on the simulated trajectory (read only by PredictionFormulaSigma, whose σ depends on the model prediction – ADR-0075); exp_data/exp_row/col_name locate the point on the experimental data (used by the data-column / relative / column-mean sources). Every source that does not read the simulation ignores sim_data/sim_row.

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

sigma_sensitivity(owner, sim_data, sim_row, exp_data, exp_row, col_name, raw_sens, index)[source]

∂σ/∂θ for a free-parameter scale (ADR-0079): the unit vector on this parameter’s own column – σ is the parameter (factor 1), with no prediction coupling. Weighted by ∂(loss)/∂σ in the objective’s noise-gradient seam, this reproduces the historical scalar noise column byte-for-byte (the column stays a single NONE-routed nuisance).

value(owner, sim_data, sim_row, 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); sim_data/sim_row locate the point on the simulated trajectory (read only by PredictionFormulaSigma, whose σ depends on the model prediction – ADR-0075); exp_data/exp_row/col_name locate the point on the experimental data (used by the data-column / relative / column-mean sources). Every source that does not read the simulation ignores sim_data/sim_row.

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.

Gradient (#505). The per-measurement half of the PSet-only composite-scale gradient. Its chain rule is FormulaSigma’s (no prediction coupling), with one wrinkle: a placeholder’s ∂σ/∂θ depends on what the row binds. So sigma_sensitivity() reads the same exp_data/exp_row binding value() does – a numeric token perturbs nothing (∂/∂θ = 0), a parameter id token lands ∂σ/∂placeholder on that parameter’s column – which is why the sigma_sensitivity seam threads exp_data/exp_row (ADR-0080’s follow-up: the seam previously carried only the sim row).

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

sigma_sensitivity(owner, sim_data, sim_row, exp_data, exp_row, col_name, raw_sens, index)[source]

∂σ/∂θ for a row-varying PSet-only composite scale (#505) – FormulaSigma’s coefficient-only chain rule with the placeholder(s) bound per row from exp_data/ exp_row exactly as value() binds them. For each symbol of the compiled expression the argument is resolved as in value() (a fixed symbol from the PSet, a placeholder from the row token), and its partial ∂σ/∂symbol lands on the column of whatever free parameter that symbol is:

  • a fixed coefficient -> its own column (like FormulaSigma);

  • a placeholder bound to a numeric token -> no column (∂token/∂θ = 0);

  • a placeholder bound to a parameter id -> ∂σ/∂placeholder on that parameter’s column (∂placeholder/∂that_param = 1) – a per-row estimated nuisance, so a row binding a constant and a row binding a free id differentiate differently on the same source (ADR-0045).

There is no prediction coupling (no term C), so sim_data/sim_row/raw_sens are unused; a parameter-id token not among the gradient free parameters contributes nothing (its ∂/∂θ is zero for every θ).

value(owner, sim_data, sim_row, 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); sim_data/sim_row locate the point on the simulated trajectory (read only by PredictionFormulaSigma, whose σ depends on the model prediction – ADR-0075); exp_data/exp_row/col_name locate the point on the experimental data (used by the data-column / relative / column-mean sources). Every source that does not read the simulation ignores sim_data/sim_row.

class pybnf.noise.source.PredictionFormulaSigma(formula, names=None, param_names=None)[source]

The noise parameter given by an expression that references the simulated prediction (model species / observables / functions) in addition to free parameters – the native prediction_formula source (ADR-0075).

ADR-0044’s FormulaSigma covers a noiseFormula that is an expression over free parameters + constants alone (0.1 + 0.05*scaling), evaluated against the PSet. A real PEtab combined-error model instead makes σ depend on the model output – the classic additive-plus-proportional noise σ = σ_abs + σ_rel * y where y is the observable’s simulated value (Raia_CancerResearch2011’s noiseParameter1_X + noiseParameter2_X * (IL13_Rec + Rec + p_IL13_Rec), the noise scaling with the predicted trajectory). Such a σ is neither a PSet-only expression (FormulaSigma) nor a data column: it must be evaluated against the current simulation at the scored point. This source closes that gap – it is the noise-side peer of the measurement-model observation layer (ADR-0036), a PEtab-math expression compiled to a numpy callable whose symbols resolve either from owner._pset_values (a free parameter – the σ_abs / σ_rel coefficients) or from the simulated Data column of that name (a model entity – the trajectory the σ scales with), read at (sim_data, sim_row).

It is estimated (it references the estimated noise coefficients), so the caller keeps the family’s likelihood normalizer (ADR-0011). Its free-parameter symbols (not the model-entity ones) are the nuisances the fit must declare; the split is model-namespace dependent, so config.py classifies it at load (_load_prediction_noise) and calls set_param_names() – until then required_free_params() is empty (the declared check runs after classification). Lazy-compiled and not pickled (the same compile-once-per-worker pattern as FormulaSigma).

Gradient (ADR-0079, lifting the ADR-0075 deferral). A prediction-dependent σ makes the per-point loss depend on the prediction through the scale as well as the residual, so ∂(loss)/∂θ gains a ∂(loss)/∂σ · ∂σ/∂θ term riding the same ∂prediction/∂θ forward sensitivity as the residual. sigma_sensitivity() supplies ∂σ/∂θ (the σ formula’s chain rule) and the #385 assembly threads it into the scalar gradient column, so an L-BFGS / trust-region fit differentiates the combined error model (the fit is not least_squares_exact – the σ-through-prediction term is not a square).

EFIM Fisher (ADR-0080, lifting the ADR-0079 deferral). The expected-Fisher block (fit_type = gntr) is now assembled too. In the natural (μ, σ) coordinates the Gaussian Fisher is block-diagonal (E[∂²L/∂μ²] = 1/σ², E[∂²L/∂σ²] = 2/σ², cross term 0), so mapping to θ through the Jacobian rows ∂μ/∂θ = s_i and ∂σ/∂θ = g_i gives the per-point H_i = (1/σ²)outer(s_i, s_i) + (2/σ²)outer(g_i, g_i). The same sigma_sensitivity() g_i the scalar gradient rides feeds the noise block’s outer product (noise_fisher_point); since g_i couples the scale to the model parameters through the prediction, that outer product is the genuine off-diagonal location↔scale block – a strict superset of a bare free σ’s diagonal entry (whose g_i = e_p recovers it byte-for-byte). Only the MEAN-on-log corner (the moment offset’s own σ-dependence, a further location↔scale coupling) stays refused, by noise_param_fisher().

formula

The PEtab-math expression (over free parameters + model-entity symbols).

names

The ordered free-symbol names (PSet keys u sim columns); None until a parse resolves them, then the compiler’s canonical sorted order. Picklable.

param_names

The subset of names that are declared free parameters (the estimated nuisances), set by config.py once the model namespace is known; the rest are simulated-trajectory columns. None until classified.

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

set_param_names(param_names)[source]

Record which of the formula’s symbols are declared free parameters (the estimated nuisances) – the model-namespace classification config.py performs at load.

sigma_sensitivity(owner, sim_data, sim_row, exp_data, exp_row, col_name, raw_sens, index)[source]

∂σ/∂θ for a prediction-dependent σ (ADR-0079) – a (len(index),) native-space vector, the noise-side peer of prediction_sensitivity() (ADR-0036). By the chain rule ∂σ/∂θ = Σ_symbol (∂σ/∂symbol)·(∂symbol/∂θ), resolving each symbol exactly as value() does (a symbol in owner._pset_values is a coefficient, else a simulated column):

  • a coefficient (a declared free parameter – the σ_abs / σ_rel the σ is affine in): ∂σ/∂coeff lands straight on that parameter’s own column – the term the existing free-scale noise column generalizes (∂σ/∂coeff = 1 for a bare free sigma);

  • a simulated-column symbol (a model entity the σ scales with – the predicted trajectory): ∂σ/∂col chains through that column’s forward sensitivity raw_sens(col, sim_row) – the same ∂prediction/∂θ the residual rides, so the σ-through-prediction coupling perturbs the model-parameter columns (and forces the fit off least_squares_exact).

The objective’s noise-gradient seam weights this vector by ∂(loss)/∂σ (family.d_nll_d_noise_params['sigma'] = (1-rho²)/σ); the per-point weight is applied by the assembly, mirroring residual_point(). A σ that scales only with the scored observable collapses the column term to (∂σ/∂obs)·raw_sens(obs), but the general raw_sens form differentiates a σ scaling with any emitted trajectory (a species/observable/function with a forward-sensitivity column).

value(owner, sim_data, sim_row, 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); sim_data/sim_row locate the point on the simulated trajectory (read only by PredictionFormulaSigma, whose σ depends on the model prediction – ADR-0075); exp_data/exp_row/col_name locate the point on the experimental data (used by the data-column / relative / column-mean sources). Every source that does not read the simulation ignores sim_data/sim_row.

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, sim_data, sim_row, 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); sim_data/sim_row locate the point on the simulated trajectory (read only by PredictionFormulaSigma, whose σ depends on the model prediction – ADR-0075); exp_data/exp_row/col_name locate the point on the experimental data (used by the data-column / relative / column-mean sources). Every source that does not read the simulation ignores sim_data/sim_row.

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

sigma_sensitivity(owner, sim_data, sim_row, exp_data, exp_row, col_name, raw_sens, index)[source]

The native-space ∂σ/∂θ (a (len(index),) vector) of this source’s noise parameter at one scored point – what the objective’s noise-gradient seam weights by ∂(loss)/∂σ to build the estimated-scale gradient column (ADR-0079/0080/#505). Every estimated source overrides it: FreeParameterSigma (a unit vector), PredictionFormulaSigma (the σ-formula chain rule, coupling the scale to the prediction), and the PSet-only composite scales FormulaSigma (the coefficient-only chain rule) / PerMeasurementFormulaSigma (that chain rule plus a per-row placeholder binding). Every fixed source contributes no gradient column, so the base default refuses – unreachable once the capability gate (_require_gradient_supported) has run, but a pointed guard if it is ever bypassed.

The argument list mirrors value() (owner, the (sim_data, sim_row) point on the simulated trajectory, the (exp_data, exp_row, col_name) point on the experimental data) plus the gradient seam’s raw_sens forward-sensitivity accessor and index free-parameter column map. exp_data/exp_row are read only by PerMeasurementFormulaSigma, whose per-row placeholder token binds from the experiment’s table (ADR-0045); every other source ignores them, exactly as they ignore sim_data/sim_row unless the σ scales with the prediction.

abstractmethod value(owner, sim_data, sim_row, 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); sim_data/sim_row locate the point on the simulated trajectory (read only by PredictionFormulaSigma, whose σ depends on the model prediction – ADR-0075); exp_data/exp_row/col_name locate the point on the experimental data (used by the data-column / relative / column-mean sources). Every source that does not read the simulation ignores sim_data/sim_row.