clophfit.fitting.plate_lm ========================= .. py:module:: clophfit.fitting.plate_lm .. autoapi-nested-parse:: Fit a whole plate at once, classically, with plate-wide noise scales. Every classical arm in this campaign fits one well at a time: `fit_binding_glob` takes a single well's `Dataset`, and the control groups are only pooled afterwards, by averaging per-well K estimates. The Bayesian multi-well model does something the classical side cannot answer to - it shares one K across a control group *during* the fit, and learns a per-label noise scale from the whole plate - so the two have never been compared like for like on the control holdout. This closes that gap. Residuals from every well are concatenated into one least-squares problem: K is shared within each control group and free elsewhere, S0 and S1 stay per well and label, and the per-label scale is *profiled* rather than fitted. Profiling is not an optimisation detail. Multiplying every weight by a constant leaves the argmin of a least-squares objective untouched - only chi-square and the covariance move - so a global noise scale is not identifiable that way at all. For fixed structural parameters the maximum-likelihood scale has a closed form, the root-mean-square of that label's standardised residuals, so the fit alternates: solve with current scales, update the scales, repeat. That is what lmfit's ``scale_covar`` does with one global factor, generalised to one per label, which is the structure section 1 found to be the right amount. Classes ------- .. autoapisummary:: clophfit.fitting.plate_lm.PlateLMResult Functions --------- .. autoapisummary:: clophfit.fitting.plate_lm.fit_plate_lm Module Contents --------------- .. py:class:: PlateLMResult Outcome of one plate-wide classical fit. :param k: Fitted K per well. Wells sharing a control group hold the identical value, not an average of separate fits. :type k: dict[str, float] :param k_stderr: Standard error on each K, from the Jacobian at the solution, scaled by the profiled noise. Conditional on the profiled scales, so mildly optimistic. :type k_stderr: dict[str, float] :param ye_mag: Profiled noise multiplier per label, relative to the supplied ``y_err``. :type ye_mag: dict[str, float] :param n_points: Unmasked observations entering the fit. :type n_points: int :param n_params: Free structural parameters. :type n_params: int :param success: Whether the final least-squares solve converged. :type success: bool :param residuals: One record per unmasked observation, on the library's canonical columns: ``well``, ``label``, ``step``, ``yhat``, ``raw_res``, ``sigma`` and ``std_res``. ``sigma`` includes the profiled scale, so ``std_res`` has per-label SD ~1 by construction and only its shape - tails, step dependence - carries information; ``raw_res`` and ``yhat`` are the signal-scale pair the noise-calibration estimators read. :type residuals: list[dict[str, Any]] .. py:function:: fit_plate_lm(datasets, groups, *, max_iter = 6, tol = 0.001) Fit every well of a plate jointly, profiling one noise scale per label. :param datasets: Well identifier to `Dataset`. :type datasets: Mapping[str, Any] :param groups: Control group name to member wells; those wells share one K. :type groups: Mapping[str, Sequence[str]] :param max_iter: Maximum alternations between solving and rescaling. :type max_iter: int :param tol: Stop when every scale moves by less than this, relatively. :type tol: float :returns: Fitted K per well, its standard error, and the profiled scales. :rtype: PlateLMResult