m3-sc 0.3.0__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.
- m3_sc-0.3.0/.gitignore +32 -0
- m3_sc-0.3.0/LICENSE +21 -0
- m3_sc-0.3.0/PKG-INFO +98 -0
- m3_sc-0.3.0/README.md +66 -0
- m3_sc-0.3.0/pyproject.toml +63 -0
- m3_sc-0.3.0/src/m3/__init__.py +29 -0
- m3_sc-0.3.0/src/m3/_bridge.py +77 -0
- m3_sc-0.3.0/src/m3/_concat.py +72 -0
- m3_sc-0.3.0/src/m3/_dataset.py +63 -0
- m3_sc-0.3.0/src/m3/_engine/__init__.py +4 -0
- m3_sc-0.3.0/src/m3/_engine/feature_selection.py +300 -0
- m3_sc-0.3.0/src/m3/_engine/model.py +221 -0
- m3_sc-0.3.0/src/m3/_engine/nn.py +115 -0
- m3_sc-0.3.0/src/m3/_engine/patient_prediction.py +344 -0
- m3_sc-0.3.0/src/m3/_engine/reclassify.py +233 -0
- m3_sc-0.3.0/src/m3/_engine/run_M3.py +1518 -0
- m3_sc-0.3.0/src/m3/_engine/simulation.py +178 -0
- m3_sc-0.3.0/src/m3/_engine/train.py +368 -0
- m3_sc-0.3.0/src/m3/_engine/util.py +507 -0
- m3_sc-0.3.0/src/m3/_model.py +915 -0
- m3_sc-0.3.0/src/m3/datasets/__init__.py +13 -0
- m3_sc-0.3.0/src/m3/datasets/_data/liu_demo.h5ad +0 -0
- m3_sc-0.3.0/src/m3/datasets/_loaders.py +108 -0
- m3_sc-0.3.0/src/m3/io.py +173 -0
- m3_sc-0.3.0/tests/test_anndata.py +56 -0
- m3_sc-0.3.0/tests/test_augment_batch.py +11 -0
- m3_sc-0.3.0/tests/test_concat.py +59 -0
- m3_sc-0.3.0/tests/test_construction_contract.py +77 -0
- m3_sc-0.3.0/tests/test_dataset.py +60 -0
- m3_sc-0.3.0/tests/test_marshal_roundtrip.py +60 -0
- m3_sc-0.3.0/tests/test_package.py +33 -0
- m3_sc-0.3.0/tests/test_pipeline_smoke.py +53 -0
- m3_sc-0.3.0/tests/test_read_h5.py +27 -0
- m3_sc-0.3.0/tests/test_read_matrix.py +28 -0
- m3_sc-0.3.0/tests/test_top_genes.py +74 -0
- m3_sc-0.3.0/tests/test_train_defaults.py +8 -0
- m3_sc-0.3.0/tests/test_validation.py +51 -0
m3_sc-0.3.0/.gitignore
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
|
|
6
|
+
# torch checkpoints (nothing in the package writes one, but a stray checkpoint
|
|
7
|
+
# from an ad-hoc script should never be committed)
|
|
8
|
+
*.pth
|
|
9
|
+
build/
|
|
10
|
+
dist/
|
|
11
|
+
.pytest_cache/
|
|
12
|
+
.ipynb_checkpoints/
|
|
13
|
+
|
|
14
|
+
# R
|
|
15
|
+
.Rproj.user/
|
|
16
|
+
.Rhistory
|
|
17
|
+
.RData
|
|
18
|
+
*.tar.gz
|
|
19
|
+
|
|
20
|
+
# MkDocs
|
|
21
|
+
website/site/
|
|
22
|
+
website/.cache/
|
|
23
|
+
|
|
24
|
+
# OS
|
|
25
|
+
.DS_Store
|
|
26
|
+
|
|
27
|
+
# NOTE: rendered tutorials (website/docs/notebooks/*.ipynb, r_*_media/) and the
|
|
28
|
+
# packaged demo data (*.h5ad under datasets/_data) are TRACKED on purpose —
|
|
29
|
+
# CI builds the docs from these without re-running the models.
|
|
30
|
+
|
|
31
|
+
# build cache (mkdocs-jupyter, etc.)
|
|
32
|
+
.cache/
|
m3_sc-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CSB Yang Laboratory
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
m3_sc-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: m3-sc
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Multimodal, multi-batch, condition-aware single-cell model with donor-level disease prediction
|
|
5
|
+
Project-URL: Homepage, https://github.com/PYangLab/M3
|
|
6
|
+
Project-URL: Documentation, https://pyanglab.github.io/M3/
|
|
7
|
+
Project-URL: Repository, https://github.com/PYangLab/M3
|
|
8
|
+
Author: CSB Yang Laboratory
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: CITE-seq,batch-integration,deep-learning,multiomics,patient-prediction,single-cell,variational-autoencoder
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Requires-Dist: anndata
|
|
20
|
+
Requires-Dist: captum
|
|
21
|
+
Requires-Dist: h5py
|
|
22
|
+
Requires-Dist: numpy
|
|
23
|
+
Requires-Dist: pandas
|
|
24
|
+
Requires-Dist: scanpy
|
|
25
|
+
Requires-Dist: scikit-learn
|
|
26
|
+
Requires-Dist: scipy
|
|
27
|
+
Requires-Dist: tqdm
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
30
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# M3
|
|
34
|
+
|
|
35
|
+
**M3** is a deep generative framework for **condition-aware integration**,
|
|
36
|
+
**patient-level inference**, and **multi-resolution interpretation** of multimodal
|
|
37
|
+
single-cell omics data across many biological conditions and samples.
|
|
38
|
+
|
|
39
|
+
From one shared, condition-aware representation, M3 delivers six downstream tasks:
|
|
40
|
+
factorised dimension reduction, condition-aware batch correction, mosaic
|
|
41
|
+
integration + imputation, patient-level condition inference, patient/sample
|
|
42
|
+
generation, and multi-resolution attribution.
|
|
43
|
+
|
|
44
|
+
📖 **Documentation & tutorials:** https://pyanglab.github.io/M3/
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
### Python
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install m3-sc # imported as `import m3`
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
PyTorch comes along automatically. If you need a build matched to a specific CUDA
|
|
55
|
+
version, install it first and pip will keep it:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install torch --index-url https://download.pytorch.org/whl/cu121
|
|
59
|
+
pip install m3-sc
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
For the development version: `pip install "git+https://github.com/PYangLab/M3.git"`.
|
|
63
|
+
|
|
64
|
+
### R
|
|
65
|
+
|
|
66
|
+
```r
|
|
67
|
+
install.packages("remotes")
|
|
68
|
+
remotes::install_github("PYangLab/M3", subdir = "m3-r")
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The R package provisions its own Python engine through
|
|
72
|
+
[basilisk](https://bioconductor.org/packages/basilisk/) on first use — no manual
|
|
73
|
+
Python / PyTorch setup, and R and Python produce identical results.
|
|
74
|
+
|
|
75
|
+
## Quickstart (Python)
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
import m3
|
|
79
|
+
|
|
80
|
+
data = m3.datasets.liu_demo() # built-in demo
|
|
81
|
+
model = m3.M3(data, condition_keys=["cond_group"],
|
|
82
|
+
celltype_key="mergedcelltype").train()
|
|
83
|
+
emb = model.embedding(part="bio") # integrated embedding
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Repository layout
|
|
87
|
+
|
|
88
|
+
| Path | What |
|
|
89
|
+
|---|---|
|
|
90
|
+
| `src/m3/` | the `m3-sc` Python package (PyTorch engine vendored under `_engine/`) |
|
|
91
|
+
| `m3-r/` | the R package — `library(m3)`, basilisk-wrapped, same engine |
|
|
92
|
+
| `website/` | documentation site (MkDocs Material); `mkdocs serve -f website/mkdocs.yml` |
|
|
93
|
+
| `.github/workflows/deploy.yml` | builds + publishes the docs to GitHub Pages |
|
|
94
|
+
| `tests/` | Python test suite |
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
See [LICENSE](LICENSE).
|
m3_sc-0.3.0/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# M3
|
|
2
|
+
|
|
3
|
+
**M3** is a deep generative framework for **condition-aware integration**,
|
|
4
|
+
**patient-level inference**, and **multi-resolution interpretation** of multimodal
|
|
5
|
+
single-cell omics data across many biological conditions and samples.
|
|
6
|
+
|
|
7
|
+
From one shared, condition-aware representation, M3 delivers six downstream tasks:
|
|
8
|
+
factorised dimension reduction, condition-aware batch correction, mosaic
|
|
9
|
+
integration + imputation, patient-level condition inference, patient/sample
|
|
10
|
+
generation, and multi-resolution attribution.
|
|
11
|
+
|
|
12
|
+
📖 **Documentation & tutorials:** https://pyanglab.github.io/M3/
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
### Python
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install m3-sc # imported as `import m3`
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
PyTorch comes along automatically. If you need a build matched to a specific CUDA
|
|
23
|
+
version, install it first and pip will keep it:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install torch --index-url https://download.pytorch.org/whl/cu121
|
|
27
|
+
pip install m3-sc
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
For the development version: `pip install "git+https://github.com/PYangLab/M3.git"`.
|
|
31
|
+
|
|
32
|
+
### R
|
|
33
|
+
|
|
34
|
+
```r
|
|
35
|
+
install.packages("remotes")
|
|
36
|
+
remotes::install_github("PYangLab/M3", subdir = "m3-r")
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The R package provisions its own Python engine through
|
|
40
|
+
[basilisk](https://bioconductor.org/packages/basilisk/) on first use — no manual
|
|
41
|
+
Python / PyTorch setup, and R and Python produce identical results.
|
|
42
|
+
|
|
43
|
+
## Quickstart (Python)
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import m3
|
|
47
|
+
|
|
48
|
+
data = m3.datasets.liu_demo() # built-in demo
|
|
49
|
+
model = m3.M3(data, condition_keys=["cond_group"],
|
|
50
|
+
celltype_key="mergedcelltype").train()
|
|
51
|
+
emb = model.embedding(part="bio") # integrated embedding
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Repository layout
|
|
55
|
+
|
|
56
|
+
| Path | What |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `src/m3/` | the `m3-sc` Python package (PyTorch engine vendored under `_engine/`) |
|
|
59
|
+
| `m3-r/` | the R package — `library(m3)`, basilisk-wrapped, same engine |
|
|
60
|
+
| `website/` | documentation site (MkDocs Material); `mkdocs serve -f website/mkdocs.yml` |
|
|
61
|
+
| `.github/workflows/deploy.yml` | builds + publishes the docs to GitHub Pages |
|
|
62
|
+
| `tests/` | Python test suite |
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "m3-sc"
|
|
7
|
+
# Single source of truth: src/m3/__init__.py (see [tool.hatch.version] below)
|
|
8
|
+
dynamic = ["version"]
|
|
9
|
+
description = "Multimodal, multi-batch, condition-aware single-cell model with donor-level disease prediction"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
license = { text = "MIT" }
|
|
13
|
+
authors = [{ name = "CSB Yang Laboratory" }]
|
|
14
|
+
keywords = [
|
|
15
|
+
"single-cell", "multiomics", "CITE-seq", "batch-integration",
|
|
16
|
+
"patient-prediction", "variational-autoencoder", "deep-learning",
|
|
17
|
+
]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 3 - Alpha",
|
|
20
|
+
"Intended Audience :: Science/Research",
|
|
21
|
+
"License :: OSI Approved :: MIT License",
|
|
22
|
+
"Operating System :: OS Independent",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Topic :: Scientific/Engineering :: Bio-Informatics",
|
|
25
|
+
]
|
|
26
|
+
# torch arrives transitively via captum, so `pip install m3-sc` works unaided.
|
|
27
|
+
# Only users who need a specific CUDA build install torch themselves first.
|
|
28
|
+
dependencies = [
|
|
29
|
+
"numpy",
|
|
30
|
+
"scipy",
|
|
31
|
+
"pandas",
|
|
32
|
+
"anndata",
|
|
33
|
+
"h5py",
|
|
34
|
+
"scikit-learn",
|
|
35
|
+
"scanpy",
|
|
36
|
+
"tqdm",
|
|
37
|
+
"captum",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[project.optional-dependencies]
|
|
41
|
+
dev = ["pytest", "ruff"]
|
|
42
|
+
|
|
43
|
+
[project.urls]
|
|
44
|
+
Homepage = "https://github.com/PYangLab/M3"
|
|
45
|
+
Documentation = "https://pyanglab.github.io/M3/"
|
|
46
|
+
Repository = "https://github.com/PYangLab/M3"
|
|
47
|
+
|
|
48
|
+
[tool.hatch.version]
|
|
49
|
+
path = "src/m3/__init__.py"
|
|
50
|
+
|
|
51
|
+
[tool.hatch.build.targets.wheel]
|
|
52
|
+
packages = ["src/m3"]
|
|
53
|
+
|
|
54
|
+
[tool.hatch.build.targets.sdist]
|
|
55
|
+
include = ["/src/m3", "/tests", "/README.md", "/LICENSE", "/pyproject.toml"]
|
|
56
|
+
|
|
57
|
+
[tool.pytest.ini_options]
|
|
58
|
+
addopts = "-q"
|
|
59
|
+
testpaths = ["tests"]
|
|
60
|
+
|
|
61
|
+
[tool.ruff]
|
|
62
|
+
line-length = 100
|
|
63
|
+
extend-exclude = ["src/m3/_engine"]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""m3 -- multimodal, multi-batch, condition-aware single-cell model."""
|
|
2
|
+
|
|
3
|
+
from m3.io import read_matrix, from_anndata, read_h5ad, read_h5
|
|
4
|
+
from m3._concat import concat
|
|
5
|
+
from m3 import datasets
|
|
6
|
+
|
|
7
|
+
__version__ = "0.3.0"
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"read_matrix", "from_anndata", "read_h5ad", "read_h5", "concat",
|
|
11
|
+
"M3", "Attribution", "M3CapabilityError",
|
|
12
|
+
"datasets",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
# The model and its readouts need torch; import them lazily so that `import m3`
|
|
16
|
+
# and the torch-free readers (io) and bundled datasets stay usable without a
|
|
17
|
+
# torch install. Accessing m3.M3 (etc.) triggers the torch-backed import.
|
|
18
|
+
_LAZY = {"M3", "Attribution", "M3CapabilityError"}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def __getattr__(name):
|
|
22
|
+
if name in _LAZY:
|
|
23
|
+
from m3 import _model
|
|
24
|
+
return getattr(_model, name)
|
|
25
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def __dir__():
|
|
29
|
+
return sorted(__all__)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Bridge: marshal a :class:`m3.Dataset` to the engine's file-based inputs.
|
|
2
|
+
|
|
3
|
+
The engine (``m3._engine.run_M3``) consumes per-modality lists of
|
|
4
|
+
paper-format HDF5 files (one element per batch, ``None`` where a batch lacks a
|
|
5
|
+
modality) plus a per-batch metadata CSV list. This module writes those temp
|
|
6
|
+
files from an in-memory ``Dataset`` so the proven engine path runs unchanged.
|
|
7
|
+
|
|
8
|
+
Paper HDF5 layout (what ``m3._engine.util.read_h5_data`` expects):
|
|
9
|
+
``matrix/data`` is genes x cells (transpose of our cells x genes), and
|
|
10
|
+
``matrix/features`` holds the feature names.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import tempfile
|
|
17
|
+
|
|
18
|
+
import h5py
|
|
19
|
+
import numpy as np
|
|
20
|
+
|
|
21
|
+
_MODALITY_ORDER = ("rna", "adt", "atac") # maps to engine modality1/2/3_path
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _write_paper_h5(path: str, counts_cells_by_features, feature_names) -> None:
|
|
25
|
+
dense = np.asarray(counts_cells_by_features.todense()
|
|
26
|
+
if hasattr(counts_cells_by_features, "todense")
|
|
27
|
+
else counts_cells_by_features, dtype=np.float32)
|
|
28
|
+
# Store cells x features (the Liu/Stephenson layout). The engine's
|
|
29
|
+
# read_h5_data + load_data_from_list double-transpose nets back to cells x features.
|
|
30
|
+
with h5py.File(path, "w") as f:
|
|
31
|
+
g = f.create_group("matrix")
|
|
32
|
+
g.create_dataset("data", data=dense)
|
|
33
|
+
g.create_dataset("features", data=np.array([str(x) for x in feature_names], dtype="S"))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def marshal(dataset, tmpdir: str | None = None) -> dict:
|
|
37
|
+
"""Write a Dataset to temp paper-format h5 + metadata CSVs.
|
|
38
|
+
|
|
39
|
+
Returns a dict with:
|
|
40
|
+
modality_paths: {"rna": [path|None per batch], "adt": [...], "atac": [...]}
|
|
41
|
+
metadata_paths: [csv path per batch]
|
|
42
|
+
batches: ordered batch labels (row order of the engine's concatenation)
|
|
43
|
+
tmpdir: the directory holding the temp files (caller may clean up)
|
|
44
|
+
"""
|
|
45
|
+
if tmpdir is None:
|
|
46
|
+
tmpdir = tempfile.mkdtemp(prefix="m3_marshal_")
|
|
47
|
+
os.makedirs(tmpdir, exist_ok=True)
|
|
48
|
+
|
|
49
|
+
obs = dataset.obs
|
|
50
|
+
batches = list(dataset.batches)
|
|
51
|
+
batch_rows = {c: (obs["batch"].to_numpy() == c) for c in batches}
|
|
52
|
+
|
|
53
|
+
modality_paths: dict[str, list] = {m: [] for m in _MODALITY_ORDER}
|
|
54
|
+
metadata_paths: list[str] = []
|
|
55
|
+
|
|
56
|
+
for ci, c in enumerate(batches):
|
|
57
|
+
rows = batch_rows[c]
|
|
58
|
+
# metadata CSV for this batch (engine reads with pandas; needs the role columns)
|
|
59
|
+
meta_path = os.path.join(tmpdir, f"metadata_{ci}.csv")
|
|
60
|
+
obs.loc[rows].reset_index(drop=True).to_csv(meta_path, index=False)
|
|
61
|
+
metadata_paths.append(meta_path)
|
|
62
|
+
|
|
63
|
+
for m in _MODALITY_ORDER:
|
|
64
|
+
if m in dataset.modalities and bool(dataset.present[m][rows].any()):
|
|
65
|
+
mat = dataset.modalities[m][rows]
|
|
66
|
+
path = os.path.join(tmpdir, f"{m}_{ci}.h5")
|
|
67
|
+
_write_paper_h5(path, mat, dataset.var[m])
|
|
68
|
+
modality_paths[m].append(path)
|
|
69
|
+
else:
|
|
70
|
+
modality_paths[m].append(None)
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
"modality_paths": modality_paths,
|
|
74
|
+
"metadata_paths": metadata_paths,
|
|
75
|
+
"batches": batches,
|
|
76
|
+
"tmpdir": tmpdir,
|
|
77
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Multi-batch assembly: stack single-batch Datasets into one."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import warnings
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import scipy.sparse as sp
|
|
10
|
+
|
|
11
|
+
from m3._dataset import Dataset
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _intersect_ordered(indices: list[pd.Index]) -> pd.Index:
|
|
15
|
+
common = set(indices[0])
|
|
16
|
+
for idx in indices[1:]:
|
|
17
|
+
common &= set(idx)
|
|
18
|
+
# stable order: follow the first batch's ordering
|
|
19
|
+
return pd.Index([g for g in indices[0] if g in common])
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def concat(datasets: list[Dataset]) -> Dataset:
|
|
23
|
+
"""Combine single-batch Datasets into one multi-batch Dataset.
|
|
24
|
+
|
|
25
|
+
- features per modality are harmonized by NAME intersection (first batch's order)
|
|
26
|
+
- a batch missing a whole modality is zero-filled with present[modality]=False
|
|
27
|
+
- batch labels must be unique across inputs
|
|
28
|
+
"""
|
|
29
|
+
if not datasets:
|
|
30
|
+
raise ValueError("concat requires at least one Dataset.")
|
|
31
|
+
if len(datasets) == 1:
|
|
32
|
+
return datasets[0]
|
|
33
|
+
|
|
34
|
+
labels = [c for d in datasets for c in d.batches]
|
|
35
|
+
if len(labels) != len(set(labels)):
|
|
36
|
+
raise ValueError(f"batch labels must be unique across inputs; got {labels}.")
|
|
37
|
+
|
|
38
|
+
all_mods = sorted({m for d in datasets for m in d.modality_names})
|
|
39
|
+
n_total = sum(d.n_cells for d in datasets)
|
|
40
|
+
|
|
41
|
+
out_mods: dict[str, sp.csr_matrix] = {}
|
|
42
|
+
out_var: dict[str, pd.Index] = {}
|
|
43
|
+
out_present: dict[str, np.ndarray] = {}
|
|
44
|
+
|
|
45
|
+
for mod in all_mods:
|
|
46
|
+
carriers = [d for d in datasets if mod in d.modalities]
|
|
47
|
+
common = _intersect_ordered([d.var[mod] for d in carriers])
|
|
48
|
+
if len(common) == 0:
|
|
49
|
+
raise ValueError(f"modality '{mod}': empty feature intersection across batches.")
|
|
50
|
+
if any(len(common) < len(d.var[mod]) for d in carriers):
|
|
51
|
+
warnings.warn(
|
|
52
|
+
f"modality '{mod}': intersection kept {len(common)} features "
|
|
53
|
+
f"(batches had {[len(d.var[mod]) for d in carriers]}).",
|
|
54
|
+
stacklevel=2,
|
|
55
|
+
)
|
|
56
|
+
blocks = []
|
|
57
|
+
present = []
|
|
58
|
+
for d in datasets:
|
|
59
|
+
if mod in d.modalities:
|
|
60
|
+
col_pos = [d.var[mod].get_loc(g) for g in common]
|
|
61
|
+
blocks.append(d.modalities[mod][:, col_pos])
|
|
62
|
+
present.append(np.ones(d.n_cells, dtype=bool))
|
|
63
|
+
else:
|
|
64
|
+
blocks.append(sp.csr_matrix((d.n_cells, len(common)), dtype=np.float32))
|
|
65
|
+
present.append(np.zeros(d.n_cells, dtype=bool))
|
|
66
|
+
out_mods[mod] = sp.vstack(blocks).tocsr()
|
|
67
|
+
out_var[mod] = common
|
|
68
|
+
out_present[mod] = np.concatenate(present)
|
|
69
|
+
|
|
70
|
+
obs = pd.concat([d.obs for d in datasets], ignore_index=True)
|
|
71
|
+
assert obs.shape[0] == n_total
|
|
72
|
+
return Dataset(modalities=out_mods, obs=obs, var=out_var, present=out_present)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Canonical m3 input container."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import scipy.sparse as sp
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Dataset:
|
|
14
|
+
"""Multimodal, multi-batch raw-count container.
|
|
15
|
+
|
|
16
|
+
modalities: {name -> csr_matrix [n_cells, n_features]} raw counts
|
|
17
|
+
obs: DataFrame [n_cells, ...]; must contain a 'batch' column
|
|
18
|
+
var: {name -> pd.Index of feature names}, aligned to each modality's columns
|
|
19
|
+
present: {name -> bool ndarray [n_cells]} per-cell modality presence
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
modalities: dict[str, sp.csr_matrix]
|
|
23
|
+
obs: pd.DataFrame
|
|
24
|
+
var: dict[str, pd.Index]
|
|
25
|
+
present: dict[str, np.ndarray]
|
|
26
|
+
|
|
27
|
+
def __post_init__(self) -> None:
|
|
28
|
+
if not self.modalities:
|
|
29
|
+
raise ValueError("Dataset requires at least one modality.")
|
|
30
|
+
n = self.obs.shape[0]
|
|
31
|
+
if "batch" not in self.obs.columns:
|
|
32
|
+
raise ValueError("obs must contain a 'batch' column.")
|
|
33
|
+
for name, mat in self.modalities.items():
|
|
34
|
+
if mat.shape[0] != n:
|
|
35
|
+
raise ValueError(
|
|
36
|
+
f"modality '{name}' has {mat.shape[0]} rows but obs has {n} (n_cells)."
|
|
37
|
+
)
|
|
38
|
+
if name not in self.var or len(self.var[name]) != mat.shape[1]:
|
|
39
|
+
raise ValueError(
|
|
40
|
+
f"var['{name}'] length must equal modality '{name}' column count."
|
|
41
|
+
)
|
|
42
|
+
if pd.Index(self.var[name]).has_duplicates:
|
|
43
|
+
raise ValueError(
|
|
44
|
+
f"var['{name}'] has duplicate feature names; names must be unique."
|
|
45
|
+
)
|
|
46
|
+
if name not in self.present or self.present[name].shape[0] != n:
|
|
47
|
+
raise ValueError(f"present['{name}'] must be a length-{n} boolean array.")
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def n_cells(self) -> int:
|
|
51
|
+
return self.obs.shape[0]
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def modality_names(self) -> list[str]:
|
|
55
|
+
return list(self.modalities.keys())
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def batches(self) -> list[str]:
|
|
59
|
+
return list(pd.unique(self.obs["batch"]))
|
|
60
|
+
|
|
61
|
+
def __repr__(self) -> str:
|
|
62
|
+
mods = ", ".join(f"{k}:{v.shape[1]}" for k, v in self.modalities.items())
|
|
63
|
+
return f"Dataset(n_cells={self.n_cells}, batches={self.batches}, modalities=[{mods}])"
|