Source code for quadsv.api
"""
Top-level factory entry points: :func:`Detector` and :func:`Comparator`.
Thin one-liner discovery face on top of the four explicit classes
(:class:`DetectorIrregular`, :class:`DetectorGrid`,
:class:`ComparatorIrregular`, :class:`ComparatorGrid`). Dispatches on
the runtime type of the input data so users don't have to know the
``Irregular`` / ``Grid`` split:
>>> det = Detector(adata) # → DetectorIrregular
>>> det = Detector(sdata) # → DetectorGrid
>>> cmp = Comparator([adata, ...]) # → ComparatorIrregular
>>> cmp = Comparator([sdata, ...]) # → ComparatorGrid
The factories only check ``isinstance`` to pick the right class, then
forward kwargs verbatim. Asymmetry between the two:
- :func:`Detector` does **not** pass the data argument to the
constructor — the caller chains ``.setup_data(data)`` afterwards
(matching the explicit-class flow).
- :func:`Comparator` **does** pass the sample list as the first
positional argument, since both comparator constructors take
``samples`` there. Cross-sample contrasts (``design``) are
supplied later, at test time, on
:meth:`~quadsv.ComparatorIrregular.test_diff_freq` /
:meth:`~quadsv.ComparatorIrregular.test_diff_expr`.
For advanced use (custom kernel selection, sample-list inputs that
mix two backends intentionally) prefer the explicit class names —
the factories deliberately reject mixed-type lists with a
:class:`TypeError`.
"""
from __future__ import annotations
from collections.abc import Sequence
from typing import Any
from quadsv.comparators import ComparatorGrid, ComparatorIrregular
from quadsv.detectors.grid import DetectorGrid
from quadsv.detectors.irregular import DetectorIrregular
__all__ = ["Detector", "Comparator"]
def _is_anndata(obj: Any) -> bool:
"""Return True if ``obj`` is an :class:`anndata.AnnData`. Lazy
import so the factories work even when ``anndata`` isn't
available — though in practice ``anndata`` is a hard dependency
of :mod:`quadsv`.
"""
try:
from anndata import AnnData
except ImportError: # pragma: no cover — anndata is a hard dep
return False
return isinstance(obj, AnnData)
def _is_spatialdata(obj: Any) -> bool:
"""Return True if ``obj`` is a :class:`spatialdata.SpatialData`.
Lazy import so the factories raise a clear :class:`TypeError`
rather than :class:`ImportError` when ``spatialdata`` isn't
installed.
"""
try:
from spatialdata import SpatialData
except ImportError:
return False
return isinstance(obj, SpatialData)
def _supported_types_msg() -> str:
return (
"supported types: anndata.AnnData (→ DetectorIrregular / "
"ComparatorIrregular) or spatialdata.SpatialData (→ "
"DetectorGrid / ComparatorGrid)"
)
[docs]
def Detector(data: Any, **kwargs: Any) -> Any: # noqa: N802 - factory mimics class names
"""Construct the right :class:`~quadsv.Detector` for ``data``.
Dispatches on ``type(data)``:
- :class:`anndata.AnnData` → :class:`~quadsv.DetectorIrregular`.
- :class:`spatialdata.SpatialData` → :class:`~quadsv.DetectorGrid`.
The data itself is **not** passed to the constructor — the caller
is expected to chain ``.setup_data(data, ...)`` afterwards (the
factory only uses ``data``'s type to pick a class).
Parameters
----------
data : anndata.AnnData or spatialdata.SpatialData
The dataset whose type drives the dispatch.
**kwargs
Forwarded to the chosen class's ``__init__`` verbatim.
Returns
-------
DetectorIrregular or DetectorGrid
Constructed (but not yet set up) detector instance.
Raises
------
TypeError
If ``data`` is neither an ``AnnData`` nor a ``SpatialData``.
Examples
--------
>>> from quadsv import Detector
>>> det = Detector(adata, kernel_method="gaussian", backend="matrix")
>>> det = det.setup_data(adata)
>>> df = det.compute_qstat()
"""
if _is_anndata(data):
return DetectorIrregular(**kwargs)
if _is_spatialdata(data):
return DetectorGrid(**kwargs)
raise TypeError(
f"Detector cannot dispatch on type {type(data).__name__!r}; " f"{_supported_types_msg()}."
)
[docs]
def Comparator( # noqa: N802 - factory mimics class names
data_list: Sequence[Any], **kwargs: Any
) -> Any:
"""Construct the right :class:`~quadsv.Comparator` for ``data_list``.
Dispatches on the homogeneous element type:
- all :class:`anndata.AnnData` → :class:`~quadsv.ComparatorIrregular`.
- all :class:`spatialdata.SpatialData` → :class:`~quadsv.ComparatorGrid`.
- mixed types → :class:`TypeError`.
Unlike :func:`Detector`, the data list **is** forwarded as the
first positional arg to the chosen class (both comparator
constructors take ``samples`` as their first positional
parameter).
Parameters
----------
data_list : sequence of anndata.AnnData or sequence of spatialdata.SpatialData
Per-sample inputs. Must all be of the same type.
**kwargs
Forwarded to the chosen class's ``__init__`` verbatim
(e.g. ``gene_names=...``, ``feature_mode=...``, etc.). The
cross-sample contrast (``design``) is supplied later on
:meth:`test_diff_freq` / :meth:`test_diff_expr`, not here.
Returns
-------
ComparatorIrregular or ComparatorGrid
Constructed comparator instance.
Raises
------
TypeError
If ``data_list`` is empty or its elements aren't all the
same supported type.
Examples
--------
>>> from quadsv import Comparator
>>> cmp = Comparator([a1, a2, a3]).compute_spectra()
>>> df = cmp.test_diff_freq(group_labels)
"""
items = list(data_list)
if len(items) == 0:
raise TypeError(
f"Comparator requires a non-empty list of samples; " f"{_supported_types_msg()}."
)
all_anndata = all(_is_anndata(x) for x in items)
all_spatialdata = all(_is_spatialdata(x) for x in items)
if all_anndata:
return ComparatorIrregular(items, **kwargs)
if all_spatialdata:
return ComparatorGrid(items, **kwargs)
raise TypeError(
f"Comparator received a list of mixed / unsupported types "
f"{[type(x).__name__ for x in items]}; {_supported_types_msg()}."
)