clophfit.fitting.noise_calibration#

Noise-model calibration from fit residuals.

Estimators that turn a canonical residual table into a PlateNoiseModel: per-label floor, photon gain, and proportional error, plus the plate slope helpers used to propagate x-axis noise.

Functions#

calibrate_noise_robust(residuals, sigma_floor, *[, ...])

Calibrate a per-label noise model from outlier-screened residuals.

fit_rel_error_from_residuals(df, sigma_floor)

Estimate proportional error (alpha) per label via moment estimator.

fit_gain_from_residuals(df, sigma_floor)

Estimate Poisson gain per label via moment estimator.

fit_noise_model_nnls(df[, sigma_floor_fixed, ...])

Fit heteroscedastic noise model via non-negative least squares.

compute_binding_slope(ph, pka, s0, s1)

Compute |dS/dpH| for the Henderson-Hasselbalch equation.

compute_plate_slopes(results)

Compute per-well per-label ∂S/∂pH from pass-1 fit results.

fit_ph_slope_noise(df, noise_model, plate_slopes)

Fit global sigma_ph from excess variance after per-label model.

Module Contents#

clophfit.fitting.noise_calibration.calibrate_noise_robust(residuals, sigma_floor, *, p_threshold=0.9, min_keep=3)#

Calibrate a per-label noise model from outlier-screened residuals.

Drops points whose posterior outlier probability exceeds p_threshold (from a PyMC mixture fit) and then estimates gain and alpha per label with the single-term moment estimators (fit_gain_from_residuals(), fit_rel_error_from_residuals()) on the retained points. Screening with the mixture’s p_outlier keeps outliers from inflating the estimate, while the two single-term estimators avoid the gain/alpha collinearity of the joint NNLS over narrow titration ranges.

Parameters:
  • residuals (pd.DataFrame) – Canonical residual table (e.g. MultiFitResult.residuals from a mixture fit). Must have label, raw_res, yhat columns; a p_outlier column enables screening (otherwise all points are used).

  • sigma_floor (dict[str, float]) – Known read-noise floor per label, e.g. tit.bg_noise. Used as the fixed floor and copied into the returned model.

  • p_threshold (float, optional) – Posterior outlier probability above which a point is dropped.

  • min_keep (int, optional) – Per label, if screening would retain fewer than this many points the full (unscreened) set is used instead.

Returns:

Per-label model with sigma_floor from sigma_floor and calibrated gain/alpha.

Return type:

PlateNoiseModel

clophfit.fitting.noise_calibration.fit_rel_error_from_residuals(df, sigma_floor)#

Estimate proportional error (alpha) per label via moment estimator.

Assumes the simplified noise model sigma^2 = floor^2 + alpha^2 * yhat^2 (no Poisson gain term). With floor known from buffer measurements and using model-predicted values yhat in the denominator to avoid noise-in-variables bias, the closed-form moment estimator is:

\[\hat{\alpha}^2 = \frac{\overline{r^2} - \sigma_{\text{floor}}^2}{\overline{\hat{y}^2}}\]
Parameters:
  • df (pd.DataFrame) – DataFrame with columns label (str), raw_res (float), and yhat (float – the model-predicted signal at each point). Typically from clophfit.fitting.model_validation.residuals_from_fit_results().

  • sigma_floor (dict[str, float]) – Known read-noise floor per label, e.g. from tit.bg_noise.

Returns:

Per-label proportional error estimate alpha (non-negative).

Return type:

dict[str, float]

Examples

>>> import numpy as np, pandas as pd
>>> rng = np.random.default_rng(0)
>>> y_pred = np.linspace(50, 500, 200)
>>> floor, true_alpha = 5.0, 0.02
>>> sigma = np.sqrt(floor**2 + (true_alpha * y_pred) ** 2)
>>> resid = sigma * rng.standard_normal(200)
>>> df = pd.DataFrame({"label": "1", "raw_res": resid, "yhat": y_pred})
>>> alpha = fit_rel_error_from_residuals(df, sigma_floor={"1": floor})
>>> round(alpha["1"], 2)  # should be close to true_alpha=0.02
0.02
clophfit.fitting.noise_calibration.fit_gain_from_residuals(df, sigma_floor)#

Estimate Poisson gain per label via moment estimator.

Symmetric counterpart to fit_rel_error_from_residuals(). Assumes the Poisson-only noise model sigma^2 = floor^2 + gain * yhat (no proportional term), which sidesteps the gain/alpha collinearity of the joint fit. With floor known from buffer measurements and using model-predicted values yhat, the closed-form moment estimator is:

\[\hat{\text{gain}} = \frac{\overline{r^2} - \sigma_{\text{floor}}^2}{\overline{\hat{y}}}\]
Parameters:
  • df (pd.DataFrame) – DataFrame with columns label (str), raw_res (float), and yhat (float – the model-predicted signal at each point). Typically from clophfit.fitting.model_validation.residuals_from_fit_results().

  • sigma_floor (dict[str, float]) – Known read-noise floor per label, e.g. from tit.bg_noise.

Returns:

Per-label Poisson gain estimate (non-negative).

Return type:

dict[str, float]

Examples

>>> import numpy as np, pandas as pd
>>> rng = np.random.default_rng(0)
>>> y_pred = np.linspace(50, 500, 400)
>>> floor, true_gain = 5.0, 0.8
>>> sigma = np.sqrt(floor**2 + true_gain * y_pred)
>>> resid = sigma * rng.standard_normal(400)
>>> df = pd.DataFrame({"label": "1", "raw_res": resid, "yhat": y_pred})
>>> gain = fit_gain_from_residuals(df, sigma_floor={"1": floor})
>>> round(gain["1"], 1)  # should be close to true_gain=0.8
0.8
clophfit.fitting.noise_calibration.fit_noise_model_nnls(df, sigma_floor_fixed=None, rel_error_fixed=None)#

Fit heteroscedastic noise model via non-negative least squares.

Model: \(\sigma^2 = \sigma_\text{floor}^2 + \text{gain} \cdot y + \alpha^2 \cdot y^2\)

Uses scipy.optimize.nnls() to enforce non-negativity on all parameters, which stabilises estimates when \(y\) and \(y^2\) are highly collinear (typical for narrow-range titrations).

Parameters:
  • df (pd.DataFrame) – Residual DataFrame with columns label, raw_res, yhat.

  • sigma_floor_fixed (dict[str, float] | None) – If given, fix floor per label and only fit gain and alpha.

  • rel_error_fixed (dict[str, float] | None) – If given, fix alpha per label and only fit floor and gain.

Returns:

(sigma_floor, gain, alpha) per label — all non-negative.

Return type:

tuple[dict[str, float], dict[str, float], dict[str, float]]

Raises:

ValueError – If both sigma_floor_fixed and rel_error_fixed are provided.

clophfit.fitting.noise_calibration.compute_binding_slope(ph, pka, s0, s1)#

Compute |dS/dpH| for the Henderson-Hasselbalch equation.

dS/dpH = (s1 - s0) * ln(10) * t / (1 + t)^2 where t = 10^(pka - ph). Returns the absolute value (sign irrelevant for variance).

Parameters:
  • ph (numpy.ndarray)

  • pka (float)

  • s0 (float)

  • s1 (float)

Return type:

numpy.ndarray

clophfit.fitting.noise_calibration.compute_plate_slopes(results)#

Compute per-well per-label ∂S/∂pH from pass-1 fit results.

Parameters:

results (dict[str, Any]) – Fit results keyed by well (must have .result and .dataset).

Returns:

{well: {label: slope_array}}.

Return type:

dict[str, dict[str, np.ndarray]]

clophfit.fitting.noise_calibration.fit_ph_slope_noise(df, noise_model, plate_slopes)#

Fit global sigma_ph from excess variance after per-label model.

After subtracting the per-label noise model variance, the leftover r^2 - var_model is regressed against (dS/dpH)^2 via NNLS.

Parameters:
  • df (pd.DataFrame) – Residual DataFrame with columns label, well, raw_res, yhat, and raw_i.

  • noise_model (PlateNoiseModel) – Per-label noise model (floor, gain, alpha) fitted in the same pass.

  • plate_slopes (dict[str, dict[str, np.ndarray]]) – Per-well per-label derivative |dS/dpH| arrays.

Returns:

Global sigma_ph estimate (>= 0).

Return type:

float