clophfit.fitting.noise_calibration ================================== .. py:module:: clophfit.fitting.noise_calibration .. autoapi-nested-parse:: Noise-model calibration from fit residuals. Estimators that turn a canonical residual table into a :class:`~clophfit.fitting.data_structures.PlateNoiseModel`: per-label floor, photon gain, and proportional error, plus the plate slope helpers used to propagate x-axis noise. Functions --------- .. autoapisummary:: clophfit.fitting.noise_calibration.calibrate_noise_robust clophfit.fitting.noise_calibration.fit_rel_error_from_residuals clophfit.fitting.noise_calibration.fit_gain_from_residuals clophfit.fitting.noise_calibration.fit_noise_model_nnls clophfit.fitting.noise_calibration.compute_binding_slope clophfit.fitting.noise_calibration.compute_plate_slopes clophfit.fitting.noise_calibration.fit_ph_slope_noise Module Contents --------------- .. py:function:: 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 (:func:`~clophfit.fitting.noise_calibration.fit_gain_from_residuals`, :func:`~clophfit.fitting.noise_calibration.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. :param residuals: 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). :type residuals: pd.DataFrame :param sigma_floor: Known read-noise floor per label, e.g. ``tit.bg_noise``. Used as the fixed floor and copied into the returned model. :type sigma_floor: dict[str, float] :param p_threshold: Posterior outlier probability above which a point is dropped. :type p_threshold: float, optional :param min_keep: Per label, if screening would retain fewer than this many points the full (unscreened) set is used instead. :type min_keep: int, optional :returns: Per-label model with ``sigma_floor`` from *sigma_floor* and calibrated ``gain``/``alpha``. :rtype: PlateNoiseModel .. py:function:: 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: .. math:: \hat{\alpha}^2 = \frac{\overline{r^2} - \sigma_{\text{floor}}^2}{\overline{\hat{y}^2}} :param df: DataFrame with columns ``label`` (str), ``raw_res`` (float), and ``yhat`` (float -- the model-predicted signal at each point). Typically from :func:`clophfit.fitting.model_validation.residuals_from_fit_results`. :type df: pd.DataFrame :param sigma_floor: Known read-noise floor per label, e.g. from ``tit.bg_noise``. :type sigma_floor: dict[str, float] :returns: Per-label proportional error estimate ``alpha`` (non-negative). :rtype: dict[str, float] .. rubric:: 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 .. py:function:: fit_gain_from_residuals(df, sigma_floor) Estimate Poisson gain per label via moment estimator. Symmetric counterpart to :func:`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: .. math:: \hat{\text{gain}} = \frac{\overline{r^2} - \sigma_{\text{floor}}^2}{\overline{\hat{y}}} :param df: DataFrame with columns ``label`` (str), ``raw_res`` (float), and ``yhat`` (float -- the model-predicted signal at each point). Typically from :func:`clophfit.fitting.model_validation.residuals_from_fit_results`. :type df: pd.DataFrame :param sigma_floor: Known read-noise floor per label, e.g. from ``tit.bg_noise``. :type sigma_floor: dict[str, float] :returns: Per-label Poisson gain estimate (non-negative). :rtype: dict[str, float] .. rubric:: 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 .. py:function:: fit_noise_model_nnls(df, sigma_floor_fixed = None, rel_error_fixed = None) Fit heteroscedastic noise model via non-negative least squares. Model: :math:`\sigma^2 = \sigma_\text{floor}^2 + \text{gain} \cdot y + \alpha^2 \cdot y^2` Uses :func:`scipy.optimize.nnls` to enforce non-negativity on all parameters, which stabilises estimates when :math:`y` and :math:`y^2` are highly collinear (typical for narrow-range titrations). :param df: Residual DataFrame with columns ``label``, ``raw_res``, ``yhat``. :type df: pd.DataFrame :param sigma_floor_fixed: If given, fix floor per label and only fit gain and alpha. :type sigma_floor_fixed: dict[str, float] | None :param rel_error_fixed: If given, fix alpha per label and only fit floor and gain. :type rel_error_fixed: dict[str, float] | None :returns: ``(sigma_floor, gain, alpha)`` per label — all non-negative. :rtype: tuple[dict[str, float], dict[str, float], dict[str, float]] :raises ValueError: If both *sigma_floor_fixed* and *rel_error_fixed* are provided. .. py:function:: 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). .. py:function:: compute_plate_slopes(results) Compute per-well per-label ``∂S/∂pH`` from pass-1 fit results. :param results: Fit results keyed by well (must have ``.result`` and ``.dataset``). :type results: dict[str, typing.Any] :returns: ``{well: {label: slope_array}}``. :rtype: dict[str, dict[str, np.ndarray]] .. py:function:: 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. :param df: Residual DataFrame with columns ``label``, ``well``, ``raw_res``, ``yhat``, and ``raw_i``. :type df: pd.DataFrame :param noise_model: Per-label noise model (floor, gain, alpha) fitted in the same pass. :type noise_model: PlateNoiseModel :param plate_slopes: Per-well per-label derivative ``|dS/dpH|`` arrays. :type plate_slopes: dict[str, dict[str, np.ndarray]] :returns: Global ``sigma_ph`` estimate (>= 0). :rtype: float