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.
Files changed (37) hide show
  1. m3_sc-0.3.0/.gitignore +32 -0
  2. m3_sc-0.3.0/LICENSE +21 -0
  3. m3_sc-0.3.0/PKG-INFO +98 -0
  4. m3_sc-0.3.0/README.md +66 -0
  5. m3_sc-0.3.0/pyproject.toml +63 -0
  6. m3_sc-0.3.0/src/m3/__init__.py +29 -0
  7. m3_sc-0.3.0/src/m3/_bridge.py +77 -0
  8. m3_sc-0.3.0/src/m3/_concat.py +72 -0
  9. m3_sc-0.3.0/src/m3/_dataset.py +63 -0
  10. m3_sc-0.3.0/src/m3/_engine/__init__.py +4 -0
  11. m3_sc-0.3.0/src/m3/_engine/feature_selection.py +300 -0
  12. m3_sc-0.3.0/src/m3/_engine/model.py +221 -0
  13. m3_sc-0.3.0/src/m3/_engine/nn.py +115 -0
  14. m3_sc-0.3.0/src/m3/_engine/patient_prediction.py +344 -0
  15. m3_sc-0.3.0/src/m3/_engine/reclassify.py +233 -0
  16. m3_sc-0.3.0/src/m3/_engine/run_M3.py +1518 -0
  17. m3_sc-0.3.0/src/m3/_engine/simulation.py +178 -0
  18. m3_sc-0.3.0/src/m3/_engine/train.py +368 -0
  19. m3_sc-0.3.0/src/m3/_engine/util.py +507 -0
  20. m3_sc-0.3.0/src/m3/_model.py +915 -0
  21. m3_sc-0.3.0/src/m3/datasets/__init__.py +13 -0
  22. m3_sc-0.3.0/src/m3/datasets/_data/liu_demo.h5ad +0 -0
  23. m3_sc-0.3.0/src/m3/datasets/_loaders.py +108 -0
  24. m3_sc-0.3.0/src/m3/io.py +173 -0
  25. m3_sc-0.3.0/tests/test_anndata.py +56 -0
  26. m3_sc-0.3.0/tests/test_augment_batch.py +11 -0
  27. m3_sc-0.3.0/tests/test_concat.py +59 -0
  28. m3_sc-0.3.0/tests/test_construction_contract.py +77 -0
  29. m3_sc-0.3.0/tests/test_dataset.py +60 -0
  30. m3_sc-0.3.0/tests/test_marshal_roundtrip.py +60 -0
  31. m3_sc-0.3.0/tests/test_package.py +33 -0
  32. m3_sc-0.3.0/tests/test_pipeline_smoke.py +53 -0
  33. m3_sc-0.3.0/tests/test_read_h5.py +27 -0
  34. m3_sc-0.3.0/tests/test_read_matrix.py +28 -0
  35. m3_sc-0.3.0/tests/test_top_genes.py +74 -0
  36. m3_sc-0.3.0/tests/test_train_defaults.py +8 -0
  37. 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}])"
@@ -0,0 +1,4 @@
1
+ """Not part of the public API. The user-facing :class:`m3.M3` orchestrates these
2
+ modules. Modules keep their original relative imports (``from .util import ...``)
3
+ and run unchanged; only their location moved into the ``m3._engine`` package.
4
+ """