SpatialQuery 0.0.4__tar.gz → 0.0.6__tar.gz

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.
Files changed (35) hide show
  1. {spatialquery-0.0.4/SpatialQuery.egg-info → spatialquery-0.0.6}/PKG-INFO +23 -1
  2. {spatialquery-0.0.4 → spatialquery-0.0.6}/README.md +22 -0
  3. {spatialquery-0.0.4 → spatialquery-0.0.6}/SpatialQuery/__init__.py +3 -1
  4. {spatialquery-0.0.4 → spatialquery-0.0.6}/SpatialQuery/plotting.py +82 -1
  5. {spatialquery-0.0.4 → spatialquery-0.0.6}/SpatialQuery/spatial_gene_covarying.py +1 -0
  6. {spatialquery-0.0.4 → spatialquery-0.0.6}/SpatialQuery/spatial_query.py +226 -9
  7. {spatialquery-0.0.4 → spatialquery-0.0.6}/SpatialQuery/spatial_query_multiple_fov.py +452 -159
  8. {spatialquery-0.0.4 → spatialquery-0.0.6}/SpatialQuery/spatial_utils.py +301 -3
  9. {spatialquery-0.0.4 → spatialquery-0.0.6/SpatialQuery.egg-info}/PKG-INFO +23 -1
  10. {spatialquery-0.0.4 → spatialquery-0.0.6}/pyproject.toml +1 -1
  11. {spatialquery-0.0.4 → spatialquery-0.0.6}/LICENSE +0 -0
  12. {spatialquery-0.0.4 → spatialquery-0.0.6}/MANIFEST.in +0 -0
  13. {spatialquery-0.0.4 → spatialquery-0.0.6}/SpatialQuery/scfind4sp/__init__.py +0 -0
  14. {spatialquery-0.0.4 → spatialquery-0.0.6}/SpatialQuery/scfind4sp/core_methods.py +0 -0
  15. {spatialquery-0.0.4 → spatialquery-0.0.6}/SpatialQuery/scfind4sp/cpp_src/EliasFano.h +0 -0
  16. {spatialquery-0.0.4 → spatialquery-0.0.6}/SpatialQuery/scfind4sp/cpp_src/QueryScore.cpp +0 -0
  17. {spatialquery-0.0.4 → spatialquery-0.0.6}/SpatialQuery/scfind4sp/cpp_src/QueryScore.h +0 -0
  18. {spatialquery-0.0.4 → spatialquery-0.0.6}/SpatialQuery/scfind4sp/cpp_src/Serialization.h +0 -0
  19. {spatialquery-0.0.4 → spatialquery-0.0.6}/SpatialQuery/scfind4sp/cpp_src/__init__.py +0 -0
  20. {spatialquery-0.0.4 → spatialquery-0.0.6}/SpatialQuery/scfind4sp/cpp_src/const.h +0 -0
  21. {spatialquery-0.0.4 → spatialquery-0.0.6}/SpatialQuery/scfind4sp/cpp_src/eliasFano.cpp +0 -0
  22. {spatialquery-0.0.4 → spatialquery-0.0.6}/SpatialQuery/scfind4sp/cpp_src/fp_growth.cpp +0 -0
  23. {spatialquery-0.0.4 → spatialquery-0.0.6}/SpatialQuery/scfind4sp/cpp_src/fp_growth.h +0 -0
  24. {spatialquery-0.0.4 → spatialquery-0.0.6}/SpatialQuery/scfind4sp/cpp_src/serialization.cpp +0 -0
  25. {spatialquery-0.0.4 → spatialquery-0.0.6}/SpatialQuery/scfind4sp/cpp_src/typedef.h +0 -0
  26. {spatialquery-0.0.4 → spatialquery-0.0.6}/SpatialQuery/scfind4sp/cpp_src/utils.cpp +0 -0
  27. {spatialquery-0.0.4 → spatialquery-0.0.6}/SpatialQuery/scfind4sp/cpp_src/utils.h +0 -0
  28. {spatialquery-0.0.4 → spatialquery-0.0.6}/SpatialQuery/spatial_differential_pattern.py +0 -0
  29. {spatialquery-0.0.4 → spatialquery-0.0.6}/SpatialQuery/utils.py +0 -0
  30. {spatialquery-0.0.4 → spatialquery-0.0.6}/SpatialQuery.egg-info/SOURCES.txt +0 -0
  31. {spatialquery-0.0.4 → spatialquery-0.0.6}/SpatialQuery.egg-info/dependency_links.txt +0 -0
  32. {spatialquery-0.0.4 → spatialquery-0.0.6}/SpatialQuery.egg-info/requires.txt +0 -0
  33. {spatialquery-0.0.4 → spatialquery-0.0.6}/SpatialQuery.egg-info/top_level.txt +0 -0
  34. {spatialquery-0.0.4 → spatialquery-0.0.6}/setup.cfg +0 -0
  35. {spatialquery-0.0.4 → spatialquery-0.0.6}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SpatialQuery
3
- Version: 0.0.4
3
+ Version: 0.0.6
4
4
  Summary: Spatial query tools for analyzing spatial transcriptomics data
5
5
  Author-email: Shaokun An <shan12@bwh.harvard.edu>
6
6
  License-Expression: MIT
@@ -35,6 +35,10 @@ Requires-Dist: sphinx-copybutton; extra == "docs"
35
35
  Requires-Dist: myst-parser; extra == "docs"
36
36
  Dynamic: license-file
37
37
 
38
+ <p align="center">
39
+ <img src="https://raw.githubusercontent.com/ShaokunAn/Spatial-Query/main/docs/_static/img/logo.png" alt="SpatialQuery" width="480">
40
+ </p>
41
+
38
42
  # SpatialQuery
39
43
 
40
44
  SpatialQuery is a Python package for systematic spatial analysis of single-cell resolution spatial omics data. It provides a unified framework to discover, quantify, and compare recurring spatial cell type patterns — termed *motifs* — across single or multiple fields of view (FOVs).
@@ -47,6 +51,7 @@ Key capabilities include:
47
51
 
48
52
  - **Spatial motif discovery**: Identify frequent cell type patterns in local neighborhoods
49
53
  - **Motif enrichment analysis**: Statistically test whether motifs occur more than expected
54
+ - **Motif composition**: Quantify the relative abundance of each motif cell type within motif-positive neighborhoods
50
55
  - **Differential motif analysis**: Compare spatial compositions across conditions
51
56
  - **Motif-associated differential expression**: Find DE genes linked to specific motifs
52
57
  - **Cross-cell gene-gene covariation**: Detect spatially dependent intercellular gene correlations
@@ -58,10 +63,21 @@ Key capabilities include:
58
63
  pip install SpatialQuery
59
64
  ```
60
65
 
66
+ > **Note:** Installation typically takes ~5 minutes depending on your environment.
67
+
61
68
  ## Documentation
62
69
 
63
70
  Full documentation, tutorials, and API reference: **[https://spatialquery.readthedocs.io/en/latest/](https://spatialquery.readthedocs.io/en/latest/)**
64
71
 
72
+ ## Reproducibility
73
+
74
+ The following tutorials reproduce the main analyses presented in our manuscript. Each tutorial takes approximately 5–20 minutes to run depending on dataset size and hardware.
75
+
76
+ - [Mouse organogenesis — single-FOV analysis (seqFISH)](https://spatialquery.readthedocs.io/en/latest/tutorials/tutorial_1.html)
77
+ - [Kidney disease atlas — multi-condition analysis (MERFISH)](https://spatialquery.readthedocs.io/en/latest/tutorials/tutorial_2.html)
78
+ - [Colorectal cancer microenvironment — spatial proteomics (CODEX)](https://spatialquery.readthedocs.io/en/latest/tutorials/tutorial_3.html)
79
+ - [Whole-brain atlas — large-scale analysis (MERFISH)](https://spatialquery.readthedocs.io/en/latest/tutorials/tutorial_4.html)
80
+
65
81
  ## Quick Start
66
82
 
67
83
  ```python
@@ -81,6 +97,12 @@ spm = spatial_query_multi(adatas=adatas, datasets=datasets,
81
97
  enrich_motif = spm.motif_enrichment_dist(ct="T_cell", dataset="healthy", max_dist=10, min_support=0.5)
82
98
  ```
83
99
 
100
+ ## Citation
101
+
102
+ If you use SpatialQuery in your research, please cite:
103
+
104
+ > An, S. et al. [SpatialQuery: scalable discovery and molecular characterization of multicellular motifs from spatial omics data](https://www.biorxiv.org/content/10.64898/2026.04.22.720136v1). *bioRxiv* (2026).
105
+
84
106
  ## License
85
107
 
86
108
  MIT
@@ -1,3 +1,7 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/ShaokunAn/Spatial-Query/main/docs/_static/img/logo.png" alt="SpatialQuery" width="480">
3
+ </p>
4
+
1
5
  # SpatialQuery
2
6
 
3
7
  SpatialQuery is a Python package for systematic spatial analysis of single-cell resolution spatial omics data. It provides a unified framework to discover, quantify, and compare recurring spatial cell type patterns — termed *motifs* — across single or multiple fields of view (FOVs).
@@ -10,6 +14,7 @@ Key capabilities include:
10
14
 
11
15
  - **Spatial motif discovery**: Identify frequent cell type patterns in local neighborhoods
12
16
  - **Motif enrichment analysis**: Statistically test whether motifs occur more than expected
17
+ - **Motif composition**: Quantify the relative abundance of each motif cell type within motif-positive neighborhoods
13
18
  - **Differential motif analysis**: Compare spatial compositions across conditions
14
19
  - **Motif-associated differential expression**: Find DE genes linked to specific motifs
15
20
  - **Cross-cell gene-gene covariation**: Detect spatially dependent intercellular gene correlations
@@ -21,10 +26,21 @@ Key capabilities include:
21
26
  pip install SpatialQuery
22
27
  ```
23
28
 
29
+ > **Note:** Installation typically takes ~5 minutes depending on your environment.
30
+
24
31
  ## Documentation
25
32
 
26
33
  Full documentation, tutorials, and API reference: **[https://spatialquery.readthedocs.io/en/latest/](https://spatialquery.readthedocs.io/en/latest/)**
27
34
 
35
+ ## Reproducibility
36
+
37
+ The following tutorials reproduce the main analyses presented in our manuscript. Each tutorial takes approximately 5–20 minutes to run depending on dataset size and hardware.
38
+
39
+ - [Mouse organogenesis — single-FOV analysis (seqFISH)](https://spatialquery.readthedocs.io/en/latest/tutorials/tutorial_1.html)
40
+ - [Kidney disease atlas — multi-condition analysis (MERFISH)](https://spatialquery.readthedocs.io/en/latest/tutorials/tutorial_2.html)
41
+ - [Colorectal cancer microenvironment — spatial proteomics (CODEX)](https://spatialquery.readthedocs.io/en/latest/tutorials/tutorial_3.html)
42
+ - [Whole-brain atlas — large-scale analysis (MERFISH)](https://spatialquery.readthedocs.io/en/latest/tutorials/tutorial_4.html)
43
+
28
44
  ## Quick Start
29
45
 
30
46
  ```python
@@ -44,6 +60,12 @@ spm = spatial_query_multi(adatas=adatas, datasets=datasets,
44
60
  enrich_motif = spm.motif_enrichment_dist(ct="T_cell", dataset="healthy", max_dist=10, min_support=0.5)
45
61
  ```
46
62
 
63
+ ## Citation
64
+
65
+ If you use SpatialQuery in your research, please cite:
66
+
67
+ > An, S. et al. [SpatialQuery: scalable discovery and molecular characterization of multicellular motifs from spatial omics data](https://www.biorxiv.org/content/10.64898/2026.04.22.720136v1). *bioRxiv* (2026).
68
+
47
69
  ## License
48
70
 
49
71
  MIT
@@ -7,7 +7,8 @@ from .plotting import (
7
7
  plot_motif_grid,
8
8
  plot_motif_rand,
9
9
  plot_motif_celltype,
10
- plot_all_center_motif
10
+ plot_all_center_motif,
11
+ plot_motif_composition
11
12
  )
12
13
 
13
14
  __all__ = [
@@ -22,5 +23,6 @@ __all__ = [
22
23
  'plot_motif_rand',
23
24
  'plot_motif_celltype',
24
25
  'plot_all_center_motif',
26
+ 'plot_motif_composition',
25
27
  'interactive_motif'
26
28
  ]
@@ -670,7 +670,88 @@ def plot_motif_enrichment_heatmap(enrich_df: pd.DataFrame,
670
670
  plt.savefig(save_path, dpi=300, bbox_inches='tight')
671
671
 
672
672
  plt.show()
673
-
673
+
674
+
675
+ def plot_motif_composition(comp_df: pd.DataFrame,
676
+ motif: Optional[List[str]] = None,
677
+ bins: int = 25,
678
+ colors: Optional[List] = None,
679
+ figsize: tuple = (6, 5),
680
+ title: Optional[str] = None,
681
+ save_path: Optional[str] = None):
682
+ """
683
+ Plot the distribution of each motif cell type's proportion across motif-positive center cells.
684
+
685
+ One semi-transparent histogram per motif cell type, on fixed bins over [0, 1]; the legend
686
+ reports the median and interquartile range (IQR) of each type's proportion.
687
+
688
+ Parameters
689
+ ----------
690
+ comp_df : pd.DataFrame
691
+ Output from motif_composition_dist or motif_composition_knn.
692
+ motif : List[str], optional
693
+ Motif to plot. May be omitted when comp_df holds a single motif.
694
+ bins : int, default=25
695
+ Number of equal-width bins on [0, 1].
696
+ colors : List, optional
697
+ One color per motif cell type, in sorted cell type order. Defaults to the tab10 palette.
698
+ figsize : tuple, default=(6, 5)
699
+ Figure size.
700
+ title : str, optional
701
+ Figure title. Defaults to the center cell type and the motif.
702
+ save_path : str, optional
703
+ Path to save the figure. If None, the figure will not be saved.
704
+
705
+ Returns
706
+ -------
707
+ None
708
+ Displays a histogram of motif cell type proportions.
709
+ """
710
+ if len(comp_df) == 0:
711
+ print("No motif-positive center cells to plot.")
712
+ return
713
+
714
+ keys = comp_df['motifs'].map(spatial_utils.canonical_motif)
715
+ if motif is None:
716
+ if keys.nunique() > 1:
717
+ raise ValueError("comp_df holds several motifs; specify which one to plot with motif.")
718
+ motif_key = keys.iloc[0]
719
+ else:
720
+ motif_key = spatial_utils.canonical_motif([motif] if isinstance(motif, str) else motif)
721
+ df = comp_df[keys == motif_key]
722
+ if len(df) == 0:
723
+ raise ValueError(f"Motif {list(motif_key)} is not in comp_df.")
724
+
725
+ cell_types = list(motif_key)
726
+ if colors is None:
727
+ colors = [plt.cm.tab10(i % 10) for i in range(len(cell_types))]
728
+ bin_edges = np.linspace(0, 1, bins + 1)
729
+
730
+ fig, ax = plt.subplots(figsize=figsize)
731
+ for cell_type, color in zip(cell_types, colors):
732
+ values = df.loc[df['cell_type'] == cell_type, 'proportion'].to_numpy(dtype=float)
733
+ q25, median, q75 = np.percentile(values, [25, 50, 75])
734
+ ax.hist(values, bins=bin_edges, color=color, alpha=0.5,
735
+ label=f'{cell_type}\n(median={median:.2f}, IQR={q75 - q25:.2f})')
736
+
737
+ ax.set_xlim(-0.03, 1.03)
738
+ ax.set_xlabel('Relative abundance among motif-associated cells', fontsize=12)
739
+ ax.set_ylabel('Number of motif-positive anchors', fontsize=12)
740
+ ax.spines[['top', 'right']].set_visible(False)
741
+ ax.legend(frameon=False, fontsize=10, loc='best')
742
+
743
+ if title is None:
744
+ centers = df['center'].unique()
745
+ center = f'{centers[0]} | ' if len(centers) == 1 else ''
746
+ title = f'{center}{" + ".join(cell_types)}'
747
+ ax.set_title(title, fontsize=13)
748
+ plt.tight_layout()
749
+
750
+ if save_path is not None:
751
+ plt.savefig(save_path, dpi=300, bbox_inches='tight')
752
+
753
+ plt.show()
754
+
674
755
 
675
756
  def plot_differential_pattern_heatmap(
676
757
  diff_motif: dict,
@@ -4395,6 +4395,7 @@ def compute_gene_gene_correlation_by_type_binary_multi_fov(
4395
4395
  if len(non_center_types) == 1:
4396
4396
  print(f"Only one non-center cell type in motif: {non_center_types}. Using compute_gene_gene_correlation_binary method.")
4397
4397
  result = compute_gene_gene_correlation_binary_multi_fov(
4398
+ sq_objs=sq_objs,
4398
4399
  ct=ct,
4399
4400
  motif=motif,
4400
4401
  dataset=dataset,
@@ -157,6 +157,7 @@ class spatial_query:
157
157
  k: int = 30,
158
158
  min_support: float = 0.5,
159
159
  max_dist: float = 20,
160
+ if_max: bool = True,
160
161
  ) -> pd.DataFrame:
161
162
  """
162
163
  Find frequent patterns within the KNNs of certain cell type.
@@ -171,6 +172,9 @@ class spatial_query:
171
172
  Threshold of frequency to consider a pattern as a frequent pattern.
172
173
  max_dist : float, default=20
173
174
  Maximum distance for considering a cell as a neighbor.
175
+ if_max : bool, default=True
176
+ Return only maximal patterns. Set to False to return every frequent pattern,
177
+ which nested motif enrichment needs to look up sub-pattern supports.
174
178
 
175
179
  Returns
176
180
  -------
@@ -190,7 +194,7 @@ class spatial_query:
190
194
  k=k,
191
195
  min_support=min_support,
192
196
  max_dist=max_dist,
193
- if_max=True
197
+ if_max=if_max
194
198
  )
195
199
 
196
200
  return fp
@@ -200,6 +204,7 @@ class spatial_query:
200
204
  max_dist: float = 20,
201
205
  min_size: int = 0,
202
206
  min_support: float = 0.5,
207
+ if_max: bool = True,
203
208
  ):
204
209
  """
205
210
  Find frequent patterns within the radius of certain cell type.
@@ -214,6 +219,9 @@ class spatial_query:
214
219
  Minimum neighborhood size for each point to consider.
215
220
  min_support : float, default=0.5
216
221
  Threshold of frequency to consider a pattern as a frequent pattern.
222
+ if_max : bool, default=True
223
+ Return only maximal patterns. Set to False to return every frequent pattern,
224
+ which nested motif enrichment needs to look up sub-pattern supports.
217
225
 
218
226
  Returns
219
227
  -------
@@ -233,7 +241,8 @@ class spatial_query:
233
241
  max_dist=max_dist,
234
242
  min_size=min_size,
235
243
  min_support=min_support,
236
- cinds=cinds
244
+ cinds=cinds,
245
+ if_max=if_max
237
246
  )
238
247
 
239
248
  return fp
@@ -244,7 +253,8 @@ class spatial_query:
244
253
  k: int = 30,
245
254
  min_support: float = 0.5,
246
255
  max_dist: float = 20,
247
- return_cellID: bool = False
256
+ return_cellID: bool = False,
257
+ mode: str = 'maximal'
248
258
  ) -> pd.DataFrame:
249
259
  """
250
260
  Perform motif enrichment analysis using k-nearest neighbors (KNN).
@@ -265,6 +275,16 @@ class spatial_query:
265
275
  return_cellID : bool, default=False
266
276
  Indicate whether return cell IDs for each frequent pattern within the neighborhood of grid points.
267
277
  By defaults do not return cell ID.
278
+ mode : {'maximal', 'nested'}, default='maximal'
279
+ How the motifs to test are chosen when motifs=None. Ignored when motifs is given.
280
+
281
+ 'maximal'
282
+ Test the maximal patterns only.
283
+ 'nested'
284
+ Test the maximal patterns, then descend into the sub-patterns of any that
285
+ are not significant, stopping a branch as soon as it becomes significant or
286
+ reaches length 1. Finds cores that a maximal pattern hides when one abundant
287
+ cell type dilutes it. Multiple-testing correction covers everything tested.
268
288
 
269
289
  Returns
270
290
  -------
@@ -285,6 +305,27 @@ class spatial_query:
285
305
  if ct not in self.labels.unique():
286
306
  raise ValueError(f"Found no {ct} in {self.label_key}!")
287
307
 
308
+ if mode not in ('nested', 'maximal'):
309
+ raise ValueError(f"mode must be 'nested' or 'maximal', got {mode!r}")
310
+
311
+ # Nested descent only decides WHICH motifs get tested; each batch is scored by this
312
+ # same method, so the statistics are identical to the maximal mode's.
313
+ if mode == 'nested' and motifs is None:
314
+ fp_all = self.find_fp_knn(ct=ct, k=k, min_support=min_support,
315
+ max_dist=max_dist, if_max=False)
316
+ if len(fp_all) == 0:
317
+ return spatial_utils.apply_fdr_correction(pd.DataFrame())
318
+ # Cell IDs are DataFrame columns here, so they travel with the rows and need
319
+ # no separate merging.
320
+ return spatial_utils.nested_motif_descent(
321
+ test_motifs=lambda ms: self.motif_enrichment_knn(
322
+ ct=ct, motifs=ms, k=k, min_support=min_support, max_dist=max_dist,
323
+ return_cellID=return_cellID, mode='maximal'),
324
+ maximal_motifs=spatial_utils.find_maximal_patterns(fp_all)['itemsets'],
325
+ support_of={spatial_utils.canonical_motif(i): s
326
+ for i, s in zip(fp_all['itemsets'], fp_all['support'])},
327
+ )
328
+
288
329
  dists, idxs = self.kd_tree.query(self.spatial_pos,
289
330
  k=k + 1, workers=-1
290
331
  ) # use k+1 to find the knn except for the points themselves
@@ -418,6 +459,7 @@ class spatial_query:
418
459
  min_size: int = 0,
419
460
  min_support: float = 0.5,
420
461
  return_cellID: bool = False,
462
+ mode: str = 'maximal',
421
463
  ) -> DataFrame:
422
464
  """
423
465
  Perform motif enrichment analysis within a specified radius-based neighborhood.
@@ -438,6 +480,16 @@ class spatial_query:
438
480
  return_cellID : bool, default=False
439
481
  Indicate whether return cell IDs for each motif within the neighborhood of central cell type.
440
482
  By defaults do not return cell ID.
483
+ mode : {'maximal', 'nested'}, default='maximal'
484
+ How the motifs to test are chosen when motifs=None. Ignored when motifs is given.
485
+
486
+ 'maximal'
487
+ Test the maximal patterns only.
488
+ 'nested'
489
+ Test the maximal patterns, then descend into the sub-patterns of any that
490
+ are not significant, stopping a branch as soon as it becomes significant or
491
+ reaches length 1. Finds cores that a maximal pattern hides when one abundant
492
+ cell type dilutes it. Multiple-testing correction covers everything tested.
441
493
 
442
494
  Returns
443
495
  -------
@@ -458,6 +510,28 @@ class spatial_query:
458
510
  if ct not in self.labels.unique():
459
511
  raise ValueError(f"Found no {ct} in {self.label_key}!")
460
512
 
513
+ if mode not in ('nested', 'maximal'):
514
+ raise ValueError(f"mode must be 'nested' or 'maximal', got {mode!r}")
515
+
516
+ # Nested descent only decides WHICH motifs get tested; each batch is scored by this
517
+ # same method, so the statistics are identical to the maximal mode's.
518
+ if mode == 'nested' and motifs is None:
519
+ fp_all = self.find_fp_dist(ct=ct, max_dist=max_dist, min_size=min_size,
520
+ min_support=min_support, if_max=False)
521
+ if len(fp_all) == 0:
522
+ return spatial_utils.apply_fdr_correction(pd.DataFrame())
523
+ # Cell IDs are DataFrame columns here, so they travel with the rows and need
524
+ # no separate merging.
525
+ return spatial_utils.nested_motif_descent(
526
+ test_motifs=lambda ms: self.motif_enrichment_dist(
527
+ ct=ct, motifs=ms, max_dist=max_dist, min_size=min_size,
528
+ min_support=min_support, return_cellID=return_cellID,
529
+ mode='maximal'),
530
+ maximal_motifs=spatial_utils.find_maximal_patterns(fp_all)['itemsets'],
531
+ support_of={spatial_utils.canonical_motif(i): s
532
+ for i, s in zip(fp_all['itemsets'], fp_all['support'])},
533
+ )
534
+
461
535
  out = []
462
536
  if motifs is None:
463
537
  fp = self.find_fp_dist(ct=ct,
@@ -576,6 +650,109 @@ class spatial_query:
576
650
  out_pd = out_pd.sort_values(by='adj-pval', ignore_index=True)
577
651
  return out_pd
578
652
 
653
+ def motif_composition_dist(self,
654
+ ct: str,
655
+ motifs: Union[str, List[str], List[List[str]]],
656
+ max_dist: float = 20,
657
+ ) -> pd.DataFrame:
658
+ """
659
+ Compute the composition of motif cell types in the radius-based neighborhoods of center cells.
660
+
661
+ Uses the same neighborhoods as motif_enrichment_dist, but performs no enrichment test.
662
+ Only center cells whose neighborhood contains every cell type of the motif (motif-positive
663
+ centers) are kept. For each of them, the proportion of a motif cell type is its number of
664
+ cells divided by the total number of motif-type cells in that neighborhood.
665
+
666
+ Parameters
667
+ ----------
668
+ ct : str
669
+ Cell type as the center cells.
670
+ motifs : str or List[str] or List[List[str]]
671
+ Motifs to analyze, e.g. the 'motifs' column of motif_enrichment_dist output.
672
+ max_dist : float, default=20
673
+ Maximum distance for considering a cell as a neighbor.
674
+
675
+ Returns
676
+ -------
677
+ pd.DataFrame
678
+ Long-format table with one row per (motif-positive center cell, motif cell type):
679
+ - center: center cell type name
680
+ - motifs: sorted list of cell types in the motif
681
+ - cell_type: motif cell type this row describes
682
+ - n_cells: number of cells of cell_type in the neighborhood
683
+ - n_motif_cells: number of motif-type cells in the neighborhood
684
+ - proportion: n_cells / n_motif_cells
685
+ """
686
+ if ct not in self.labels.unique():
687
+ raise ValueError(f"Found no {ct} in {self.label_key}!")
688
+ motifs = spatial_utils.normalize_motif_list(motifs, self.labels.unique(), self.label_key)
689
+
690
+ label_encoder = LabelEncoder()
691
+ int_labels = label_encoder.fit_transform(np.array(self.labels))
692
+ cinds = np.where(self.labels == ct)[0]
693
+ counts = spatial_utils.anchor_neighbor_counts_dist(
694
+ self.kd_tree, self.spatial_pos, cinds, int_labels,
695
+ len(label_encoder.classes_), max_dist)
696
+
697
+ out = [spatial_utils.motif_composition_table(
698
+ ct, motif, counts[:, label_encoder.transform(np.array(motif))]) for motif in motifs]
699
+ if len(out) == 0:
700
+ return pd.DataFrame(columns=spatial_utils.COMPOSITION_COLUMNS)
701
+ return pd.concat(out, ignore_index=True)
702
+
703
+ def motif_composition_knn(self,
704
+ ct: str,
705
+ motifs: Union[str, List[str], List[List[str]]],
706
+ k: int = 30,
707
+ max_dist: float = 20,
708
+ ) -> pd.DataFrame:
709
+ """
710
+ Compute the composition of motif cell types in the k-nearest-neighbor neighborhoods of center cells.
711
+
712
+ Uses the same neighborhoods as motif_enrichment_knn, but performs no enrichment test.
713
+ Only center cells whose neighborhood contains every cell type of the motif (motif-positive
714
+ centers) are kept. For each of them, the proportion of a motif cell type is its number of
715
+ cells divided by the total number of motif-type cells in that neighborhood.
716
+
717
+ Parameters
718
+ ----------
719
+ ct : str
720
+ Cell type of the center cell.
721
+ motifs : str or List[str] or List[List[str]]
722
+ Motifs to analyze, e.g. the 'motifs' column of motif_enrichment_knn output.
723
+ k : int, default=30
724
+ Number of nearest neighbors to consider.
725
+ max_dist : float, default=20
726
+ Maximum distance for neighbors.
727
+
728
+ Returns
729
+ -------
730
+ pd.DataFrame
731
+ Long-format table with one row per (motif-positive center cell, motif cell type):
732
+ - center: center cell type name
733
+ - motifs: sorted list of cell types in the motif
734
+ - cell_type: motif cell type this row describes
735
+ - n_cells: number of cells of cell_type in the neighborhood
736
+ - n_motif_cells: number of motif-type cells in the neighborhood
737
+ - proportion: n_cells / n_motif_cells
738
+ """
739
+ if ct not in self.labels.unique():
740
+ raise ValueError(f"Found no {ct} in {self.label_key}!")
741
+ motifs = spatial_utils.normalize_motif_list(motifs, self.labels.unique(), self.label_key)
742
+
743
+ label_encoder = LabelEncoder()
744
+ int_labels = label_encoder.fit_transform(np.array(self.labels))
745
+ cinds = np.where(self.labels == ct)[0]
746
+ counts = spatial_utils.anchor_neighbor_counts_knn(
747
+ self.kd_tree, self.spatial_pos, cinds, int_labels,
748
+ len(label_encoder.classes_), k, max_dist)
749
+
750
+ out = [spatial_utils.motif_composition_table(
751
+ ct, motif, counts[:, label_encoder.transform(np.array(motif))]) for motif in motifs]
752
+ if len(out) == 0:
753
+ return pd.DataFrame(columns=spatial_utils.COMPOSITION_COLUMNS)
754
+ return pd.concat(out, ignore_index=True)
755
+
579
756
 
580
757
  def find_patterns_grid(self,
581
758
  max_dist: float = 20,
@@ -1404,6 +1581,42 @@ class spatial_query:
1404
1581
  """
1405
1582
  return plotting.plot_motif_enrichment_heatmap(enrich_df=enrich_df, figsize=figsize,
1406
1583
  save_path=save_path, title=title, cmap=cmap)
1584
+
1585
+ def plot_motif_composition(self,
1586
+ comp_df: pd.DataFrame,
1587
+ motif: Optional[List[str]] = None,
1588
+ bins: int = 25,
1589
+ colors: Optional[List] = None,
1590
+ figsize: tuple = (6, 5),
1591
+ title: Optional[str] = None,
1592
+ save_path: Optional[str] = None):
1593
+ """
1594
+ Plot the distribution of each motif cell type's proportion across motif-positive center cells.
1595
+
1596
+ Parameters
1597
+ ----------
1598
+ comp_df : pd.DataFrame
1599
+ Output DataFrame from motif_composition_dist or motif_composition_knn.
1600
+ motif : List[str], optional
1601
+ Motif to plot. May be omitted when comp_df holds a single motif.
1602
+ bins : int, default=25
1603
+ Number of equal-width bins on [0, 1].
1604
+ colors : List, optional
1605
+ One color per motif cell type, in sorted cell type order. Defaults to the tab10 palette.
1606
+ figsize : tuple, default=(6, 5)
1607
+ Figure size.
1608
+ title : str, optional
1609
+ Figure title. Defaults to the center cell type and the motif.
1610
+ save_path : str, optional
1611
+ Path to save the figure. If None, the figure will not be saved.
1612
+
1613
+ Returns
1614
+ -------
1615
+ None
1616
+ Displays a histogram of motif cell type proportions, with median and IQR in the legend.
1617
+ """
1618
+ return plotting.plot_motif_composition(comp_df=comp_df, motif=motif, bins=bins, colors=colors,
1619
+ figsize=figsize, title=title, save_path=save_path)
1407
1620
 
1408
1621
  def plot_gene_pair_heatmap(self,
1409
1622
  gene_pair_df: pd.DataFrame,
@@ -1536,6 +1749,7 @@ def interactive_motif(
1536
1749
  sp,
1537
1750
  zarr_path: str,
1538
1751
  spatialSpotRadius: float = 1,
1752
+ use_adata_store: bool = False,
1539
1753
  ):
1540
1754
  """
1541
1755
  Create an interactive Vitessce widget to explore spatial motifs.
@@ -1558,6 +1772,9 @@ def interactive_motif(
1558
1772
  spatialSpotRadius : float, default=1
1559
1773
  Radius for spatial spots in the Vitessce visualization. Adjust based
1560
1774
  on data density and spatial coordinate scale.
1775
+ use_adata_store : bool, default=False
1776
+ If True, pass the zarr path via ``adata_store`` (for HubMAP workspace).
1777
+ If False, pass via ``adata_path`` (for local usage).
1561
1778
 
1562
1779
  Returns
1563
1780
  -------
@@ -1638,8 +1855,9 @@ def interactive_motif(
1638
1855
 
1639
1856
  vc = VitessceConfig(schema_version="1.0.16", name="SpatialQuery")
1640
1857
 
1858
+ _adata_key = "adata_store" if use_adata_store else "adata_path"
1641
1859
  wrapper_kwargs = dict(
1642
- adata_path=zarr_path,
1860
+ **{_adata_key: zarr_path},
1643
1861
  obs_set_paths=[f"obs/{label_key}"],
1644
1862
  obs_set_names=["Cell Type"],
1645
1863
  obs_spots_path=f"obsm/{spatial_key}",
@@ -1652,7 +1870,6 @@ def interactive_motif(
1652
1870
  dataset = vc.add_dataset("Query results").add_object(AnnDataWrapper(**wrapper_kwargs))
1653
1871
 
1654
1872
  spatial_view = vc.add_view("spatialBeta", dataset=dataset)
1655
- lc_view = vc.add_view("layerControllerBeta", dataset=dataset)
1656
1873
  sets_view = vc.add_view("obsSets", dataset=dataset)
1657
1874
  sq_view = vc.add_view("spatialQuery", dataset=dataset)
1658
1875
  sq_heatmap_view = vc.add_view("spatialQueryHeatmap", dataset=dataset)
@@ -1661,7 +1878,7 @@ def interactive_motif(
1661
1878
  ["Cell Type", ct] for ct in sp.labels.unique().tolist()
1662
1879
  ]
1663
1880
 
1664
- linked_views = [spatial_view, lc_view, sets_view, sq_view]
1881
+ linked_views = [spatial_view, sets_view, sq_view]
1665
1882
  if has_expression:
1666
1883
  features_view = vc.add_view("featureList", dataset=dataset)
1667
1884
  linked_views.append(features_view)
@@ -1672,21 +1889,21 @@ def interactive_motif(
1672
1889
  [plugin.additional_obs_sets, plugin.obs_set_color, initial_obs_set_selection, "cellSetSelection"],
1673
1890
  )
1674
1891
 
1675
- vc.link_views_by_dict([spatial_view, lc_view], {
1892
+ vc.link_views_by_dict([spatial_view], {
1676
1893
  "spotLayer": CL([{"obsType": "cell", "spatialSpotRadius": spatialSpotRadius}]),
1677
1894
  })
1678
1895
 
1679
1896
  if has_expression:
1680
1897
  vc.layout(
1681
1898
  vconcat(
1682
- hconcat(spatial_view, vconcat(lc_view, features_view)),
1899
+ hconcat(spatial_view, features_view, split=[2, 1]),
1683
1900
  hconcat(sets_view, sq_heatmap_view, sq_view),
1684
1901
  )
1685
1902
  )
1686
1903
  else:
1687
1904
  vc.layout(
1688
1905
  vconcat(
1689
- hconcat(spatial_view, lc_view),
1906
+ spatial_view,
1690
1907
  hconcat(sets_view, sq_heatmap_view, sq_view),
1691
1908
  )
1692
1909
  )