clophfit.fitting.plate_odr ========================== .. py:module:: clophfit.fitting.plate_odr .. autoapi-nested-parse:: Plate-wide fit that lets the pH axis move, not only the signal. `plate_lm` takes x at face value. That is the wrong assumption here twice over: the recorded pH of each titration step carries its own measured uncertainty - `list.pH.csv` has it in the third column, 0.006 to 0.086 on L6a - and section 2 found that modelling x as uncertain is the single largest improvement anywhere in this campaign, with every latent-x arm beating every fixed-x one. This is the classical counterpart: an errors-in-variables fit, which is what orthogonal distance regression is. Each titration step gets one shift, penalised by its recorded uncertainty, and the shift is *shared across wells* because the pH of a step is a property of the plate rather than of a well. Seven extra parameters for a seven-step titration, not one per point. The scale behaves differently here than in `plate_lm`, and that is the reason to have both. A least-squares objective is invariant to scaling every weight, so the noise scale is not identifiable and has to be profiled. Once x errors enter, the *ratio* between the y and x penalties is part of the objective, so the balance between them is doing real work rather than cancelling. Classes ------- .. autoapisummary:: clophfit.fitting.plate_odr.PlateODRResult Functions --------- .. autoapisummary:: clophfit.fitting.plate_odr.fit_plate_odr clophfit.fitting.plate_odr.ctr_holdout_odr Module Contents --------------- .. py:class:: PlateODRResult Outcome of one plate-wide errors-in-variables fit. :param k: Fitted K per well; control-group members share one value. :type k: dict[str, float] :param k_stderr: Standard error per well from the Jacobian at the solution. :type k_stderr: dict[str, float] :param ye_mag: Profiled noise multiplier per label. :type ye_mag: dict[str, float] :param dx: Fitted shift of each titration step, in pH, shared across wells. :type dx: np.ndarray :param n_points: Unmasked observations entering the fit. :type n_points: int :param n_params: Free parameters, structural shifts included. :type n_params: int :param success: Whether the final 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``, ``std_res``), taken on the shifted pH grid, so it reflects the model the fit actually settled on rather than the recorded axis. :type residuals: list[dict[str, Any]] .. py:function:: fit_plate_odr(datasets, groups, *, x_err = None, max_iter = 6, tol = 0.001) Fit a plate with the pH of each step free to move within its uncertainty. :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 x_err: Recorded uncertainty per titration step. ``None`` uses 0.015 pH, the pipetting scale section 2 settled on. :type x_err: np.ndarray | None :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, the per-step pH shifts, and the profiled scales. :rtype: PlateODRResult :raises ValueError: If *x_err* does not carry one entry per titration step. .. py:function:: ctr_holdout_odr(datasets, groups, *, x_err = None, rope = 0.1, plate = '') Leave one control out at a time, with the pH axis free to move. The same experiment as :func:`clophfit.fitting.plate_lm.ctr_holdout`, fitted with errors in variables so a wrongly recorded step is corrected rather than pushed into K. Rows share the schema, so the two can be pooled together. :param datasets: Well identifier to `Dataset`. :type datasets: Mapping[str, Any] :param groups: Control group name to member wells. :type groups: Mapping[str, Sequence[str]] :param x_err: Recorded uncertainty per titration step, from the third column of ``list.pH.csv``. ``None`` falls back to 0.015 pH. :type x_err: np.ndarray | None :param rope: Half-width of the region of practical equivalence, in pH. :type rope: float :param plate: Recorded in each row, for pooling across plates. :type plate: str :returns: One row per held-out control. :rtype: list[dict[str, Any]]