Source code for quadsv.detectors.base

"""
Shared base class for single-sample spatial pattern detectors.

Concrete detectors follow a three-step workflow:

1. **Construction** — :meth:`Detector.__init__` takes kernel method + backend
   configs + kernel hyperparameters. No data is attached.
2. **Data setup** — :meth:`Detector.setup_data` takes the input container
   (:class:`anndata.AnnData` for :class:`DetectorIrregular`,
   :class:`spatialdata.SpatialData` for :class:`DetectorGrid`), performs
   preprocessing (feature filtering, coordinate / obsp extraction, or
   rasterization), and builds the kernel.
3. **Computation** — :meth:`Detector.compute_qstat` and
   :meth:`Detector.compute_rstat` take feature selections + compute-time knobs
   (``n_jobs``, ``chunk_size``, etc.) and return per-feature results.

The base class owns the attribute contract (``kernel_method_``,
``kernel_params_``, ``kernel_``, ``n``, ``_data_ready``) and enforces the
workflow via :meth:`_require_setup`. Concrete subclasses implement
:meth:`_merge_kernel_defaults`, :meth:`setup_data`, :meth:`compute_qstat`,
and :meth:`compute_rstat`.
"""

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import Any

import pandas as pd

from quadsv.kernels import Kernel

__all__ = ["Detector"]


[docs] class Detector(ABC): r""" Abstract base for single-sample pattern detectors. Attributes ---------- kernel_method\_ : str Kernel method name (e.g. ``'matern'``, ``'car'``). Set at construction. kernel_params\_ : dict Resolved kernel parameters after backend-specific defaults are merged with user overrides. Set at construction. kernel\_ : :class:`~quadsv.kernels.Kernel` or None Kernel object built in :meth:`setup_data`. ``None`` before data setup. n : int or None Effective number of observations after preprocessing. ``None`` before data setup. """ _available_kernels: tuple[str, ...] = ( "gaussian", "matern", "moran", "graph_laplacian", "car", ) def __init__(self, kernel_method: str, **kernel_params: Any) -> None: if kernel_method not in self._available_kernels: raise ValueError( f"kernel_method must be one of {self._available_kernels}, " f"got {kernel_method!r}." )
[docs] self.kernel_method_: str = kernel_method
[docs] self.kernel_params_: dict = self._merge_kernel_defaults(kernel_method, dict(kernel_params))
[docs] self.kernel_: Kernel | None = None
[docs] self.n: int | None = None
self._data_ready: bool = False # ------------------------------------------------------------------ # Subclass hooks # ------------------------------------------------------------------ @abstractmethod def _merge_kernel_defaults(self, method: str, user_params: dict) -> dict: """Return ``{default: value}`` after merging user overrides. Subclasses must validate unknown keys and fill in backend-specific defaults (e.g. ``fft_solver`` / ``spacing`` / ``topology`` for :class:`DetectorGrid`; ``k_neighbors`` vs ``neighbor_degree`` for the matrix vs NUFFT backends of :class:`DetectorIrregular`). """ @abstractmethod
[docs] def setup_data(self, data: Any, **kwargs: Any) -> Detector: """Attach ``data``, preprocess features, and build :attr:`kernel_`. Must set :attr:`kernel_`, :attr:`n`, and ``self._data_ready = True`` before returning ``self``. """
@abstractmethod
[docs] def compute_qstat(self, features: list[str] | None = None, **kwargs: Any) -> pd.DataFrame: """Univariate Q-test across ``features``."""
@abstractmethod
[docs] def compute_rstat(self, **kwargs: Any) -> pd.DataFrame: """Bivariate R-test. Signature / feature selection are subclass-specific."""
# ------------------------------------------------------------------ # Shared helpers # ------------------------------------------------------------------ def _require_setup(self) -> None: """Raise if :meth:`setup_data` has not been called.""" if not self._data_ready or self.kernel_ is None: raise RuntimeError(f"{type(self).__name__}: call setup_data(...) before compute_*().") def __repr__(self) -> str: state = "ready" if self._data_ready else "not set up" return ( f"<{type(self).__name__} kernel_method={self.kernel_method_!r} " f"state={state} n={self.n}>" )