Source code for ewoksxas.fit.data

"""Shared 1D x/y data primitives and goodness-of-fit metrics for the backends.

Both fitting backends (:mod:`ewoksxas.fit.lca_fit` and
:mod:`ewoksxas.fit.peak_fit`) use these: the ``ArrayF64``/``IterableF64``/
``DataDict`` aliases, the :func:`as_1d_array` helper, the immutable
:class:`XYData` container for observations and fitted curves, and the
:func:`r2`/:func:`rmse` goodness-of-fit metrics (the latter also used by the
interactive Orange fit panel to report the quality of a fitted model).
"""

from __future__ import annotations

from collections.abc import Iterable
from dataclasses import asdict, dataclass

import numpy as np
import numpy.typing as npt

ArrayF64 = npt.NDArray[np.float64]
IterableF64 = Iterable[np.float64]
DataDict = dict[str, npt.NDArray[np.float64]]


[docs] def as_1d_array(iterable: IterableF64) -> ArrayF64: """Converts a 1D numeric iterable into a flattened 1D numpy array. Args: iterable: One-dimensional sequence of floats. Raises: TypeError: If iterable is not type Iterable or is type string or bytes. ValueError: If iterable is scalar or has two or more dimensions. """ if not isinstance(iterable, Iterable) or isinstance(iterable, (str, bytes)): raise TypeError(f"Expected an array, got {type(iterable)}.") array = np.asarray(iterable, dtype=np.float64) if not array.size or array.ndim == 1: return array if array.size in array.shape: return array.flatten() raise ValueError(f"Array is scalar or has 2+ dimensions: shape={array.shape}.")
[docs] @dataclass(kw_only=True, frozen=True, slots=True) class XYData: """An immutable container for 1D X and Y data. Used to hold the observations to be fitted as well as the fitted peak, baseline and total-model curves returned by a fit. Attributes: x: 1D array of x-values. y: 1D array of y-values. """ x: ArrayF64 y: ArrayF64 def __post_init__(self) -> None: """Converts inputs to 1D numpy arrays and checks shape matching. Raises: ValueError: If self.x and self.y do not have matching shapes. """ object.__setattr__(self, "x", as_1d_array(self.x)) object.__setattr__(self, "y", as_1d_array(self.y)) if self.x.size != self.y.size: raise ValueError(f"Arrays don't match: x={self.x.shape}, y={self.y.shape}.")
[docs] def as_dict(self) -> DataDict: """Returns the dataclass attributes as a dictionary.""" return asdict(self)
[docs] def rmse(observed: ArrayF64, model: ArrayF64) -> float: """Root-mean-square error of a model against the observed data. Args: observed: 1D array of observed y-values. model: 1D array of model y-values on the same grid. """ residual = observed - model return float(np.sqrt(np.mean(np.square(residual))))
[docs] def r2(observed: ArrayF64, model: ArrayF64) -> float: """Coefficient of determination of a model against the observed data. Args: observed: 1D array of observed y-values. model: 1D array of model y-values on the same grid. Returns: The R² value, or NaN when the observed data has zero variance. """ residual = observed - model ss_res = float(np.sum(np.square(residual))) ss_tot = float(np.sum(np.square(observed - observed.mean()))) return 1.0 - ss_res / ss_tot if ss_tot > 0 else float("nan")