sonic-spatial 1.0.0rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sonic/__init__.py ADDED
@@ -0,0 +1,73 @@
1
+ """
2
+ sonic: kernel-based spatial pattern detection and comparison for spatial omics.
3
+
4
+ The public top-level API is organised in four layers:
5
+
6
+ 1. **Kernels** — :class:`MatrixKernel` (dense / sparse), :class:`FFTKernel`
7
+ (regular grid), :class:`NUFFTKernel` (irregular 2D coordinates). The
8
+ :class:`~sonic.kernels.Kernel` and
9
+ :class:`~sonic.kernels.MatrixKernelBase` ABCs live in
10
+ :mod:`sonic.kernels` and are intended for backend authors.
11
+ 2. **Statistical tests** — :func:`spatial_q_test` and :func:`spatial_r_test`.
12
+ A single entry point per test dispatches on the kernel type (matrix, FFT,
13
+ or NUFFT). Signature: ``(x, kernel, null_params=None, return_pval=True,
14
+ is_standardized=False)``.
15
+ 3. **Detectors** — :class:`DetectorIrregular` consumes :class:`anndata.AnnData`
16
+ (irregular grids, matrix/NUFFT backends); :class:`DetectorGrid` consumes
17
+ :class:`spatialdata.SpatialData` (regular grids, FFT backend).
18
+ 4. **Comparators** — cross-sample pattern comparison:
19
+ :class:`ComparatorIrregular` on a list of AnnData (NUFFT backend);
20
+ :class:`ComparatorGrid` on a list of SpatialData (FFT backend).
21
+ """
22
+
23
+ import logging
24
+
25
+ logging.getLogger(__name__).addHandler(logging.NullHandler())
26
+
27
+ # Version resolution order: prefer the file written by ``setuptools-scm`` at
28
+ # build time (``src/sonic/_version.py`` — see ``[tool.setuptools_scm]`` in
29
+ # ``pyproject.toml``), fall back to installed-package metadata, then to a
30
+ # last-known release string for unbuilt / shallow-clone checkouts.
31
+ try:
32
+ from sonic._version import version as __version__ # type: ignore[assignment]
33
+ except ImportError: # _version.py absent — source checkout without a build step
34
+ try:
35
+ from importlib.metadata import PackageNotFoundError, version
36
+
37
+ __version__ = version("sonic-spatial")
38
+ except (ImportError, PackageNotFoundError):
39
+ __version__ = "0.0.0+unknown"
40
+
41
+ from sonic.api import Comparator, Detector
42
+ from sonic.comparators import ComparatorGrid, ComparatorIrregular
43
+ from sonic.detectors.grid import DetectorGrid
44
+ from sonic.detectors.irregular import DetectorIrregular
45
+ from sonic.kernels import MatrixKernel
46
+ from sonic.kernels.fft import FFTKernel
47
+ from sonic.kernels.nufft import NUFFTKernel
48
+ from sonic.statistics import spatial_q_test, spatial_r_test
49
+
50
+ # The :class:`~sonic.kernels.Kernel` and
51
+ # :class:`~sonic.kernels.MatrixKernelBase` ABCs are intentionally not
52
+ # re-exported here. They are extension points for backend authors and
53
+ # live at ``sonic.kernels`` (the canonical path). Importing them through
54
+ # ``sonic`` directly is unsupported.
55
+
56
+ __all__ = [
57
+ # Kernels
58
+ "MatrixKernel",
59
+ "FFTKernel",
60
+ "NUFFTKernel",
61
+ # Statistical tests
62
+ "spatial_q_test",
63
+ "spatial_r_test",
64
+ # Detectors
65
+ "DetectorIrregular",
66
+ "DetectorGrid",
67
+ # Cross-sample
68
+ "ComparatorIrregular",
69
+ "ComparatorGrid",
70
+ # Factories — type-dispatched discovery face on the four classes above
71
+ "Detector",
72
+ "Comparator",
73
+ ]
sonic/_rasterize.py ADDED
@@ -0,0 +1,95 @@
1
+ """
2
+ Shared :func:`spatialdata.rasterize_bins` wrappers used by
3
+ :class:`sonic.DetectorGrid` and :class:`sonic.ComparatorGrid`.
4
+
5
+ Both consumers need the same boilerplate:
6
+
7
+ 1. Coerce the table's X matrix to CSC sparse (required by ``rasterize_bins``).
8
+ 2. Forward to ``spatialdata.rasterize_bins`` with the user-supplied keys.
9
+ 3. Restore structurally absent grid positions as ``NaN``; SpatialData emits
10
+ zeros for both absent bins and observed zero-valued bins.
11
+
12
+ Callers differ only in what they do with the rasterized image afterwards —
13
+ ``DetectorGrid`` stashes it back into ``sdata.images`` under a derived key,
14
+ ``ComparatorGrid`` extracts the array and reindexes the gene axis — so the
15
+ shared helper stops after restoring the raster's structural-missing mask.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from typing import Any
21
+
22
+ import numpy as np
23
+ import scipy.sparse as sp
24
+ import spatialdata as sd
25
+
26
+ __all__ = ["ensure_csc_table", "rasterize_table"]
27
+
28
+
29
+ def _mean_fill_missing(values: np.ndarray, axis: tuple[int, ...]) -> np.ndarray:
30
+ """Mean-fill ``NaN`` bins in place so centering makes their residuals zero."""
31
+ missing = np.isnan(values)
32
+ if not missing.any():
33
+ return values
34
+ n_observed = np.prod([values.shape[i] for i in axis]) - missing.sum(axis=axis, keepdims=True)
35
+ np.copyto(values, 0.0, where=missing)
36
+ sums = values.sum(axis=axis, keepdims=True)
37
+ means = np.full_like(sums, np.nan)
38
+ np.divide(sums, n_observed, out=means, where=n_observed > 0)
39
+ np.copyto(values, means, where=missing)
40
+ return values
41
+
42
+
43
+ def ensure_csc_table(sdata: Any, table_name: str) -> None:
44
+ """Coerce ``sdata.tables[table_name].X`` to CSC sparse in-place, if sparse.
45
+
46
+ ``spatialdata.rasterize_bins`` performs column-wise slicing and requires CSC.
47
+ Dense arrays are left untouched.
48
+ """
49
+ if table_name not in sdata.tables:
50
+ raise ValueError(f"Table {table_name!r} not found in sdata.")
51
+ table = sdata.tables[table_name]
52
+ X = getattr(table, "X", None)
53
+ if X is None or isinstance(X, np.ndarray):
54
+ return
55
+ if sp.issparse(X) and X.format != "csc":
56
+ table.X = X.tocsc()
57
+
58
+
59
+ def rasterize_table(
60
+ sdata: Any,
61
+ *,
62
+ bins: str,
63
+ table_name: str,
64
+ col_key: str,
65
+ row_key: str,
66
+ value_key: str | list[str] | None = None,
67
+ return_region_as_labels: bool = False,
68
+ ):
69
+ """Rasterize a table while preserving structural missingness as ``NaN``.
70
+
71
+ ``spatialdata.rasterize_bins`` initializes its bounding rectangle with
72
+ zeros, making an absent bin indistinguishable from an observed zero. This
73
+ wrapper reconstructs occupancy from ``row_key`` / ``col_key`` and masks
74
+ only absent positions. The mask is applied lazily to image outputs; label
75
+ outputs are returned unchanged.
76
+ """
77
+ ensure_csc_table(sdata, table_name)
78
+ rasterized = sd.rasterize_bins(
79
+ sdata,
80
+ bins=bins,
81
+ table_name=table_name,
82
+ col_key=col_key,
83
+ row_key=row_key,
84
+ value_key=value_key,
85
+ return_region_as_labels=return_region_as_labels,
86
+ )
87
+ if return_region_as_labels:
88
+ return rasterized
89
+
90
+ table = sdata.tables[table_name]
91
+ rows = np.asarray(table.obs[row_key])
92
+ cols = np.asarray(table.obs[col_key])
93
+ occupied = np.zeros(rasterized.shape[-2:], dtype=bool)
94
+ occupied[(rows - rows.min()).astype(int), (cols - cols.min()).astype(int)] = True
95
+ return rasterized if occupied.all() else rasterized.where(occupied)
sonic/_version.py ADDED
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '1.0.0rc1'
22
+ __version_tuple__ = version_tuple = (1, 0, 0, 'rc1')
23
+
24
+ __commit_id__ = commit_id = None
sonic/api.py ADDED
@@ -0,0 +1,182 @@
1
+ """
2
+ Top-level factory entry points: :func:`Detector` and :func:`Comparator`.
3
+
4
+ Thin one-liner discovery face on top of the four explicit classes
5
+ (:class:`DetectorIrregular`, :class:`DetectorGrid`,
6
+ :class:`ComparatorIrregular`, :class:`ComparatorGrid`). Dispatches on
7
+ the runtime type of the input data so users don't have to know the
8
+ ``Irregular`` / ``Grid`` split:
9
+
10
+ >>> det = Detector(adata) # → DetectorIrregular
11
+ >>> det = Detector(sdata) # → DetectorGrid
12
+ >>> cmp = Comparator([adata, ...]) # → ComparatorIrregular
13
+ >>> cmp = Comparator([sdata, ...]) # → ComparatorGrid
14
+
15
+ The factories only check ``isinstance`` to pick the right class, then
16
+ forward kwargs verbatim. Asymmetry between the two:
17
+
18
+ - :func:`Detector` does **not** pass the data argument to the
19
+ constructor — the caller chains ``.setup_data(data)`` afterwards
20
+ (matching the explicit-class flow).
21
+ - :func:`Comparator` **does** pass the sample list as the first
22
+ positional argument, since both comparator constructors take
23
+ ``samples`` there. Cross-sample contrasts (``design``) are
24
+ supplied later, at test time, on
25
+ :meth:`~sonic.ComparatorIrregular.test_diff_freq` /
26
+ :meth:`~sonic.ComparatorIrregular.test_diff_expr`.
27
+
28
+ For advanced use (custom kernel selection, sample-list inputs that
29
+ mix two backends intentionally) prefer the explicit class names —
30
+ the factories deliberately reject mixed-type lists with a
31
+ :class:`TypeError`.
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ from collections.abc import Sequence
37
+ from typing import Any
38
+
39
+ from sonic.comparators import ComparatorGrid, ComparatorIrregular
40
+ from sonic.detectors.grid import DetectorGrid
41
+ from sonic.detectors.irregular import DetectorIrregular
42
+
43
+ __all__ = ["Detector", "Comparator"]
44
+
45
+
46
+ def _is_anndata(obj: Any) -> bool:
47
+ """Return True if ``obj`` is an :class:`anndata.AnnData`. Lazy
48
+ import so the factories work even when ``anndata`` isn't
49
+ available — though in practice ``anndata`` is a hard dependency
50
+ of :mod:`sonic`.
51
+ """
52
+ try:
53
+ from anndata import AnnData
54
+ except ImportError: # pragma: no cover — anndata is a hard dep
55
+ return False
56
+ return isinstance(obj, AnnData)
57
+
58
+
59
+ def _is_spatialdata(obj: Any) -> bool:
60
+ """Return True if ``obj`` is a :class:`spatialdata.SpatialData`.
61
+ Lazy import so the factories raise a clear :class:`TypeError`
62
+ rather than :class:`ImportError` when ``spatialdata`` isn't
63
+ installed.
64
+ """
65
+ try:
66
+ from spatialdata import SpatialData
67
+ except ImportError:
68
+ return False
69
+ return isinstance(obj, SpatialData)
70
+
71
+
72
+ def _supported_types_msg() -> str:
73
+ return (
74
+ "supported types: anndata.AnnData (→ DetectorIrregular / "
75
+ "ComparatorIrregular) or spatialdata.SpatialData (→ "
76
+ "DetectorGrid / ComparatorGrid)"
77
+ )
78
+
79
+
80
+ def Detector(data: Any, **kwargs: Any) -> Any: # noqa: N802 - factory mimics class names
81
+ """Construct the right :class:`~sonic.Detector` for ``data``.
82
+
83
+ Dispatches on ``type(data)``:
84
+
85
+ - :class:`anndata.AnnData` → :class:`~sonic.DetectorIrregular`.
86
+ - :class:`spatialdata.SpatialData` → :class:`~sonic.DetectorGrid`.
87
+
88
+ The data itself is **not** passed to the constructor — the caller
89
+ is expected to chain ``.setup_data(data, ...)`` afterwards (the
90
+ factory only uses ``data``'s type to pick a class).
91
+
92
+ Parameters
93
+ ----------
94
+ data : anndata.AnnData or spatialdata.SpatialData
95
+ The dataset whose type drives the dispatch.
96
+ **kwargs
97
+ Forwarded to the chosen class's ``__init__`` verbatim.
98
+
99
+ Returns
100
+ -------
101
+ DetectorIrregular or DetectorGrid
102
+ Constructed (but not yet set up) detector instance.
103
+
104
+ Raises
105
+ ------
106
+ TypeError
107
+ If ``data`` is neither an ``AnnData`` nor a ``SpatialData``.
108
+
109
+ Examples
110
+ --------
111
+ >>> from sonic import Detector
112
+ >>> det = Detector(adata, kernel_method="gaussian", backend="matrix")
113
+ >>> det = det.setup_data(adata)
114
+ >>> df = det.compute_qstat()
115
+ """
116
+ if _is_anndata(data):
117
+ return DetectorIrregular(**kwargs)
118
+ if _is_spatialdata(data):
119
+ return DetectorGrid(**kwargs)
120
+ raise TypeError(
121
+ f"Detector cannot dispatch on type {type(data).__name__!r}; " f"{_supported_types_msg()}."
122
+ )
123
+
124
+
125
+ def Comparator( # noqa: N802 - factory mimics class names
126
+ data_list: Sequence[Any], **kwargs: Any
127
+ ) -> Any:
128
+ """Construct the right :class:`~sonic.Comparator` for ``data_list``.
129
+
130
+ Dispatches on the homogeneous element type:
131
+
132
+ - all :class:`anndata.AnnData` → :class:`~sonic.ComparatorIrregular`.
133
+ - all :class:`spatialdata.SpatialData` → :class:`~sonic.ComparatorGrid`.
134
+ - mixed types → :class:`TypeError`.
135
+
136
+ Unlike :func:`Detector`, the data list **is** forwarded as the
137
+ first positional arg to the chosen class (both comparator
138
+ constructors take ``samples`` as their first positional
139
+ parameter).
140
+
141
+ Parameters
142
+ ----------
143
+ data_list : sequence of anndata.AnnData or sequence of spatialdata.SpatialData
144
+ Per-sample inputs. Must all be of the same type.
145
+ **kwargs
146
+ Forwarded to the chosen class's ``__init__`` verbatim
147
+ (e.g. ``gene_names=...``, ``feature_mode=...``, etc.). The
148
+ cross-sample contrast (``design``) is supplied later on
149
+ :meth:`test_diff_freq` / :meth:`test_diff_expr`, not here.
150
+
151
+ Returns
152
+ -------
153
+ ComparatorIrregular or ComparatorGrid
154
+ Constructed comparator instance.
155
+
156
+ Raises
157
+ ------
158
+ TypeError
159
+ If ``data_list`` is empty or its elements aren't all the
160
+ same supported type.
161
+
162
+ Examples
163
+ --------
164
+ >>> from sonic import Comparator
165
+ >>> cmp = Comparator([a1, a2, a3]).compute_spectra()
166
+ >>> df = cmp.test_diff_freq(group_labels)
167
+ """
168
+ items = list(data_list)
169
+ if len(items) == 0:
170
+ raise TypeError(
171
+ f"Comparator requires a non-empty list of samples; " f"{_supported_types_msg()}."
172
+ )
173
+ all_anndata = all(_is_anndata(x) for x in items)
174
+ all_spatialdata = all(_is_spatialdata(x) for x in items)
175
+ if all_anndata:
176
+ return ComparatorIrregular(items, **kwargs)
177
+ if all_spatialdata:
178
+ return ComparatorGrid(items, **kwargs)
179
+ raise TypeError(
180
+ f"Comparator received a list of mixed / unsupported types "
181
+ f"{[type(x).__name__ for x in items]}; {_supported_types_msg()}."
182
+ )
@@ -0,0 +1,33 @@
1
+ """
2
+ ``sonic.comparators`` — cross-sample spatial-pattern comparison.
3
+
4
+ Subpackage grouping the layer-4 public classes:
5
+
6
+ - :class:`ComparatorIrregular` — wraps a sequence of
7
+ :class:`anndata.AnnData` (irregular spots, NUFFT backend).
8
+ - :class:`ComparatorGrid` — wraps a sequence of
9
+ :class:`spatialdata.SpatialData` (regular rasterized bins, FFT
10
+ backend).
11
+
12
+ Both classes share the same post-``compute_spectra`` surface
13
+ (``normalize_background``, ``normalize_covariates``,
14
+ ``test_diff_freq``, ``test_diff_expr``) through
15
+ the private :class:`~sonic.comparators.base._ComparatorBase` mixin.
16
+ Cross-sample contrasts are supplied at test time via the ``design``
17
+ argument on the test methods — the comparator itself is
18
+ design-agnostic, so one fitted comparator can serve any number of
19
+ unrelated contrasts on the same spectra. The shape-only frequency
20
+ test is available via a ``normalize_shape: bool = False`` keyword on
21
+ :meth:`test_diff_freq` (forwarded to the standalone ``compare_*``
22
+ function).
23
+
24
+ The array-level spectral feature helpers live in
25
+ :mod:`sonic.comparators.features`; normalization primitives live in
26
+ :mod:`sonic.comparators.normalization`; statistical comparison
27
+ primitives live in :mod:`sonic.comparators.multisample`.
28
+ """
29
+
30
+ from sonic.comparators.grid import ComparatorGrid
31
+ from sonic.comparators.irregular import ComparatorIrregular
32
+
33
+ __all__ = ["ComparatorIrregular", "ComparatorGrid"]