ecotyper-py 0.1.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.
- ecotyper_py-0.1.0/LICENSE.md +8 -0
- ecotyper_py-0.1.0/PKG-INFO +194 -0
- ecotyper_py-0.1.0/README.md +154 -0
- ecotyper_py-0.1.0/pyproject.toml +57 -0
- ecotyper_py-0.1.0/setup.cfg +4 -0
- ecotyper_py-0.1.0/src/ecotyper/__init__.py +17 -0
- ecotyper_py-0.1.0/src/ecotyper/_ckernel.py +102 -0
- ecotyper_py-0.1.0/src/ecotyper/_nmfk.c +224 -0
- ecotyper_py-0.1.0/src/ecotyper/api.py +500 -0
- ecotyper_py-0.1.0/src/ecotyper/builtin.py +84 -0
- ecotyper_py-0.1.0/src/ecotyper/cli.py +109 -0
- ecotyper_py-0.1.0/src/ecotyper/discovery_scrna.py +388 -0
- ecotyper_py-0.1.0/src/ecotyper/discovery_steps.py +534 -0
- ecotyper_py-0.1.0/src/ecotyper/io.py +98 -0
- ecotyper_py-0.1.0/src/ecotyper/model.py +254 -0
- ecotyper_py-0.1.0/src/ecotyper/nmf.py +545 -0
- ecotyper_py-0.1.0/src/ecotyper/rcompat/__init__.py +4 -0
- ecotyper_py-0.1.0/src/ecotyper/rcompat/_fmt.c +129 -0
- ecotyper_py-0.1.0/src/ecotyper/rcompat/_ld.c +323 -0
- ecotyper_py-0.1.0/src/ecotyper/rcompat/_ld.py +106 -0
- ecotyper_py-0.1.0/src/ecotyper/rcompat/cluster.py +386 -0
- ecotyper_py-0.1.0/src/ecotyper/rcompat/collate.py +186 -0
- ecotyper_py-0.1.0/src/ecotyper/rcompat/fma.py +59 -0
- ecotyper_py-0.1.0/src/ecotyper/rcompat/frame.py +187 -0
- ecotyper_py-0.1.0/src/ecotyper/rcompat/io.py +302 -0
- ecotyper_py-0.1.0/src/ecotyper/rcompat/parse.py +300 -0
- ecotyper_py-0.1.0/src/ecotyper/rcompat/rng.py +194 -0
- ecotyper_py-0.1.0/src/ecotyper/rcompat/stats.py +822 -0
- ecotyper_py-0.1.0/src/ecotyper/recovery_steps.py +538 -0
- ecotyper_py-0.1.0/src/ecotyper/rlayout.py +70 -0
- ecotyper_py-0.1.0/src/ecotyper/validate.py +91 -0
- ecotyper_py-0.1.0/src/ecotyper/workflows.py +744 -0
- ecotyper_py-0.1.0/src/ecotyper_py.egg-info/PKG-INFO +194 -0
- ecotyper_py-0.1.0/src/ecotyper_py.egg-info/SOURCES.txt +44 -0
- ecotyper_py-0.1.0/src/ecotyper_py.egg-info/dependency_links.txt +1 -0
- ecotyper_py-0.1.0/src/ecotyper_py.egg-info/entry_points.txt +2 -0
- ecotyper_py-0.1.0/src/ecotyper_py.egg-info/requires.txt +25 -0
- ecotyper_py-0.1.0/src/ecotyper_py.egg-info/top_level.txt +1 -0
- ecotyper_py-0.1.0/tests/test_api.py +266 -0
- ecotyper_py-0.1.0/tests/test_collate.py +44 -0
- ecotyper_py-0.1.0/tests/test_consensus.py +58 -0
- ecotyper_py-0.1.0/tests/test_e2e.py +49 -0
- ecotyper_py-0.1.0/tests/test_fast.py +80 -0
- ecotyper_py-0.1.0/tests/test_nmf.py +96 -0
- ecotyper_py-0.1.0/tests/test_rng.py +41 -0
- ecotyper_py-0.1.0/tests/test_stats.py +94 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
STANFORD NON-COMMERCIAL SOFTWARE LICENSE AGREEMENT <br>
|
|
2
|
+
<br> 1. The Board of Trustees of the Leland Stanford Junior University (“Stanford”) provides EcoTyper software and code (“Service”) free of charge for non-commercial use only. Use of the Service by any commercial entity for any purpose, including research, is prohibited.
|
|
3
|
+
<br> 2. By using the Service, you agree to be bound by the terms of this Agreement. Please read it carefully.
|
|
4
|
+
<br> 3. You agree not to use the Service for commercial advantage, or in the course of for-profit activities. You agree not to use the Service on behalf of any organization that is not a non-profit organization. Commercial entities wishing to use this Service should contact Stanford University’s Office of Technology Licensing and reference docket S19-484.
|
|
5
|
+
<br> 4. THE SERVICE IS OFFERED “AS IS”, AND, TO THE EXTENT PERMITTED BY LAW, STANFORD MAKES NO REPRESENTATIONS AND EXTENDS NO WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED. STANFORD SHALL NOT BE LIABLE FOR ANY CLAIMS OR DAMAGES WITH RESPECT TO ANY LOSS OR OTHER CLAIM BY YOU OR ANY THIRD PARTY ON ACCOUNT OF, OR ARISING FROM THE USE OF THE SERVICE. YOU HEREBY AGREE TO DEFEND AND INDEMNIFY STANFORD, ITS TRUSTEES, EMPLOYEES, OFFICERS, STUDENTS, AGENTS, FACULTY, REPRESENTATIVES, AND VOLUNTEERS (“STANFORD INDEMNITEES”) FROM ANY LOSS OR CLAIM ASSERTED AGAINST STANFORD INDEMNITEES ARISING FROM YOUR USE OF THE SERVICE.
|
|
6
|
+
<br> 5. All rights not expressly granted to you in this Agreement are reserved and retained by Stanford or its licensors or content providers. This Agreement provides no license under any patent.
|
|
7
|
+
<br> 6. You agree that this Agreement and any dispute arising under it is governed by the laws of the State of California, United States of America, applicable to agreements negotiated, executed, and performed within California.
|
|
8
|
+
<br> 7. Subject to your compliance with the terms and conditions set forth in this Agreement, Stanford grants you a revocable, non-exclusive, non-transferable right to access and make use of the Service.
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ecotyper-py
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python/AnnData port of EcoTyper (Luca, Steen et al., Cell 2021) with numerical parity to the R implementation
|
|
5
|
+
Author: Ho Seok Lee
|
|
6
|
+
License-Expression: LicenseRef-Stanford-NonCommercial
|
|
7
|
+
Project-URL: Documentation, https://hoseok-lee.github.io/ecotyper-py/
|
|
8
|
+
Project-URL: Original EcoTyper (R), https://github.com/digitalcytometry/ecotyper
|
|
9
|
+
Keywords: ecotyper,anndata,scverse,single-cell,spatial transcriptomics,cell states,NMF
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: C
|
|
12
|
+
Classifier: Operating System :: MacOS
|
|
13
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE.md
|
|
19
|
+
Requires-Dist: numpy>=1.24
|
|
20
|
+
Requires-Dist: scipy>=1.10
|
|
21
|
+
Requires-Dist: pandas>=2.0
|
|
22
|
+
Requires-Dist: anndata>=0.10
|
|
23
|
+
Requires-Dist: numba>=0.59
|
|
24
|
+
Requires-Dist: pyyaml>=6
|
|
25
|
+
Provides-Extra: icu
|
|
26
|
+
Requires-Dist: PyICU>=2.10; extra == "icu"
|
|
27
|
+
Provides-Extra: plot
|
|
28
|
+
Requires-Dist: matplotlib>=3.7; extra == "plot"
|
|
29
|
+
Provides-Extra: sklearn
|
|
30
|
+
Requires-Dist: scikit-learn>=1.3; extra == "sklearn"
|
|
31
|
+
Provides-Extra: test
|
|
32
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
33
|
+
Provides-Extra: docs
|
|
34
|
+
Requires-Dist: sphinx>=7; extra == "docs"
|
|
35
|
+
Requires-Dist: furo; extra == "docs"
|
|
36
|
+
Requires-Dist: myst-parser>=2; extra == "docs"
|
|
37
|
+
Requires-Dist: sphinx-copybutton; extra == "docs"
|
|
38
|
+
Requires-Dist: sphinx-design; extra == "docs"
|
|
39
|
+
Dynamic: license-file
|
|
40
|
+
|
|
41
|
+
# ecotyper-py (Python port of EcoTyper)
|
|
42
|
+
|
|
43
|
+
A Python/AnnData port of **EcoTyper**
|
|
44
|
+
(<https://github.com/digitalcytometry/ecotyper>), the framework for
|
|
45
|
+
identifying cell states and multicellular communities (ecotypes) from bulk,
|
|
46
|
+
single-cell and spatial transcriptomes, by the Newman lab (Stanford).
|
|
47
|
+
|
|
48
|
+
The port reproduces the R implementation: on the README tutorials (compared
|
|
49
|
+
against R on x86-64/glibc), every discrete output is identical, about 60% of the
|
|
50
|
+
files are byte-identical, and all numeric outputs agree to ≤8e-11 relative. See
|
|
51
|
+
the [validation report](https://hoseok-lee.github.io/ecotyper-py/validation.html) for the comparison.
|
|
52
|
+
|
|
53
|
+
## Documentation
|
|
54
|
+
|
|
55
|
+
<https://hoseok-lee.github.io/ecotyper-py/>. Sphinx docs (installation, API reference, step-by-step tutorials, Python vs R
|
|
56
|
+
benchmarks with charts) live in `docs/`. They are built and published by `.github/workflows/docs.yml` on every
|
|
57
|
+
push to `main`. To build them locally:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install -e ".[docs]"
|
|
61
|
+
sphinx-build -b html docs docs/_build/html # open docs/_build/html/index.html
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Credit and license
|
|
65
|
+
|
|
66
|
+
EcoTyper was developed by Bogdan A. Luca, Chloé B. Steen, Aaron M. Newman and
|
|
67
|
+
colleagues. If you use it, cite:
|
|
68
|
+
|
|
69
|
+
- Luca/Steen et al., *Atlas of clinically distinct cell states and ecosystems
|
|
70
|
+
across human solid tumors*, Cell 2021, <https://doi.org/10.1016/j.cell.2021.09.014>
|
|
71
|
+
- Steen/Luca et al., *The landscape of tumor cell states and ecosystems in
|
|
72
|
+
diffuse large B cell lymphoma*, Cancer Cell 2021,
|
|
73
|
+
<https://doi.org/10.1016/j.ccell.2021.08.011>
|
|
74
|
+
|
|
75
|
+
This port is a derivative of EcoTyper and is distributed under the same
|
|
76
|
+
**Stanford non-commercial license** (`LICENSE.md`, included in the package): free for
|
|
77
|
+
non-commercial use only. It is an independent port, not affiliated with or endorsed by
|
|
78
|
+
the EcoTyper authors.
|
|
79
|
+
|
|
80
|
+
CIBERSORTx is closed source and is **not** part of the port. Steps that need it
|
|
81
|
+
(cell type fractions and high-resolution purification) take precomputed
|
|
82
|
+
CIBERSORTx outputs as inputs.
|
|
83
|
+
|
|
84
|
+
## Installation
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
pip install ecotyper-py # the import name is `ecotyper`
|
|
88
|
+
pip install "ecotyper-py[icu]" # optional: R-identical string sorting via PyICU
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
A C compiler (`cc`, `gcc` or `clang`) is needed at run time: the exact-parity kernels ship as C source and are
|
|
92
|
+
compiled on first use. From a source checkout, or with conda:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
conda create -n ecotyper-py -c conda-forge python=3.12 numpy scipy pandas anndata numba pyicu pyyaml pytest
|
|
96
|
+
conda activate ecotyper-py
|
|
97
|
+
pip install -e . # from this folder
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
PyICU is optional; it is used to sort strings the way R does in a UTF-8 locale
|
|
101
|
+
(`ECOTYPER_COLLATION=C` switches to byte order, like R with `LC_COLLATE=C`).
|
|
102
|
+
|
|
103
|
+
## Usage
|
|
104
|
+
|
|
105
|
+
### AnnData (scverse style)
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
import ecotyper as et
|
|
109
|
+
|
|
110
|
+
# recovery: bulk, single-cell or Visium, inferred from the AnnData
|
|
111
|
+
et.recovery(adata, "Carcinoma", cell_type_key="cell_type", sample_key="patient")
|
|
112
|
+
adata.obs["ecotyper_state_Fibroblasts"] # S01, S02, ... (NaN = other cell types)
|
|
113
|
+
adata.obs["ecotyper_ecotype"] # CE1..CE10
|
|
114
|
+
adata.obsm["ecotyper_abundance_Fibroblasts"] # state abundances
|
|
115
|
+
|
|
116
|
+
# discovery from annotated single cells -> in-memory model
|
|
117
|
+
model = et.discovery(adata, cell_type_key="cell_type", sample_key="patient", n_jobs=16)
|
|
118
|
+
et.recovery(new_adata, model, cell_type_key="cell_type")
|
|
119
|
+
model.save("models/crc") # R layout, usable by R EcoTyper too
|
|
120
|
+
model = et.EcoTyperModel.load("models/crc")
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
- Data types: `bulk` (samples x genes), `scRNA` (cells x genes with cell type labels in
|
|
124
|
+
`obs`), `visium` (spots, with CIBERSORTx fractions passed as `fractions=`). Inferred
|
|
125
|
+
from `obsm["spatial"]` and the cell type column; override with `data_type=`.
|
|
126
|
+
- `X` or `layer=` holds raw-scale expression; sparse matrices are accepted.
|
|
127
|
+
- The built-in Carcinoma and Lymphoma models are downloaded (~5 MB each) from a pinned
|
|
128
|
+
commit of the R repository on first use and cached (`ECOTYPER_CACHE`; set
|
|
129
|
+
`ECOTYPER_MODELS` to a local `EcoTyper/` folder to skip the download).
|
|
130
|
+
- Bulk discovery takes CIBERSORTx HiRes (or presorted) profiles:
|
|
131
|
+
`et.discovery({"B.cells": adata_b, "Fibroblasts": adata_fib, ...})`.
|
|
132
|
+
|
|
133
|
+
Results are identical to the file-based workflows below (`tests/test_api.py`).
|
|
134
|
+
|
|
135
|
+
### pandas and files (mirrors the R wrappers)
|
|
136
|
+
|
|
137
|
+
`ecotyper.workflows` has `*_frame` functions (genes x samples DataFrames plus annotation,
|
|
138
|
+
returning every intermediate table) and `*_files` functions that read the R wrappers'
|
|
139
|
+
input files and write the R output layout file for file.
|
|
140
|
+
|
|
141
|
+
### Command line (mirrors the R wrappers, writes the R file layout)
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
ecotyper-py recover-bulk -m bulk_lung_data.txt -a bulk_lung_annotation.txt -d Carcinoma -o out # built-in model
|
|
145
|
+
ecotyper-py recover-scrna -m scRNA_CRC_data.txt -a scRNA_CRC_annotation.txt -d Carcinoma --models ecotyper/EcoTyper -o out
|
|
146
|
+
ecotyper-py recover-visium -i VisiumBreast -f visium_fractions_example.txt -n VisiumBreast -d Carcinoma --models ecotyper/EcoTyper -o out
|
|
147
|
+
ecotyper-py discover -i Tutorial_6/PresortedDiscovery -a Tutorial_6/PresortedDiscovery_annotation.txt -n PresortedDiscovery -o out -t 8
|
|
148
|
+
ecotyper-py discover-scrna -m scRNA_CRC_data.txt -a scRNA_CRC_annotation.txt -n discovery_scRNA_CRC -o out -t 8
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Speed and alternative solver
|
|
152
|
+
|
|
153
|
+
- `exact=True` (default) reproduces R bit-for-bit where R's arithmetic is deterministic (optimised C/numba kernels
|
|
154
|
+
with R's operation order; typically 3-7x faster than R on the single-cell and presorted tutorials).
|
|
155
|
+
- `exact=False` (`--fast` on the CLI) uses BLAS updates and skips R's text round trips. Numerics stay within
|
|
156
|
+
about 1e-10 relative of R, and discrete outputs match on tutorials 1-4 and 6. On tutorial 5, 5 of 11 cell types
|
|
157
|
+
land in a different NMF consensus partition (NMF local optima), so use exact mode when you need R parity.
|
|
158
|
+
- `nmf_method="cd"` (discovery only, `pip install ecotyper-py[sklearn]`) swaps the Brunet updates for scikit-learn's
|
|
159
|
+
coordinate-descent NMF with a Frobenius loss. It is not R-equivalent: chosen ranks and states differ from R.
|
|
160
|
+
See the [performance page](https://hoseok-lee.github.io/ecotyper-py/performance.html) for timings and agreement numbers.
|
|
161
|
+
|
|
162
|
+
### Randomness
|
|
163
|
+
|
|
164
|
+
R's random number generator (Mersenne-Twister with R's seeding, `runif`,
|
|
165
|
+
`sample()` with rejection sampling) is reimplemented in
|
|
166
|
+
`ecotyper.rcompat.rng.RRandom`. Discovery is deterministic as in R
|
|
167
|
+
(`set.seed(1234 + restart)` per NMF run). R's recovery scripts call
|
|
168
|
+
`NMF::rnmf()` **without** seeding, so R's own recovery results change slightly
|
|
169
|
+
from run to run; the port takes an explicit `seed` (default 42), and an R run
|
|
170
|
+
started with `set.seed(42)` (see `validation/r_reference/Rprofile.R`) gives
|
|
171
|
+
the same output.
|
|
172
|
+
|
|
173
|
+
## Layout
|
|
174
|
+
|
|
175
|
+
| module | contents |
|
|
176
|
+
|---|---|
|
|
177
|
+
| `ecotyper.rcompat` | R semantics: RNG, `sample`, number parsing/formatting, `hclust`/`cutree`/`dist`/`silhouette`, `cor`/`var`/`mean`, quantiles, BH, t-test, Wilcoxon, collation, `merge`/`order` |
|
|
178
|
+
| `ecotyper.nmf` | NMF package 0.28: Brunet updates, seeding, connectivity stopping, restarts merging, cophenetic/silhouette/dispersion, `NMFpredict` |
|
|
179
|
+
| `ecotyper.recovery_steps` | state and ecotype recovery (bulk, scRNA, Visium) |
|
|
180
|
+
| `ecotyper.discovery_steps`, `ecotyper.discovery_scrna` | state and ecotype discovery |
|
|
181
|
+
| `ecotyper.api` | `recovery`, `discovery`, `infer_data_type` (scverse-style AnnData API) |
|
|
182
|
+
| `ecotyper.workflows` | pandas and file-based workflows mirroring the R wrappers |
|
|
183
|
+
| `ecotyper.model`, `ecotyper.builtin` | `EcoTyperModel` (load/save/from discovery), built-in model download |
|
|
184
|
+
| `ecotyper.io`, `ecotyper.rlayout` | readers, R-format writers |
|
|
185
|
+
| `ecotyper.validate` | file-by-file comparison with R outputs |
|
|
186
|
+
|
|
187
|
+
Plots (heatmaps) are not ported.
|
|
188
|
+
|
|
189
|
+
## Tests
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
pytest -m "not e2e" # unit parity tests against R fixtures in tests/data
|
|
193
|
+
pytest -m e2e # tutorials vs R reference outputs (needs the R references, see validation/)
|
|
194
|
+
```
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# ecotyper-py (Python port of EcoTyper)
|
|
2
|
+
|
|
3
|
+
A Python/AnnData port of **EcoTyper**
|
|
4
|
+
(<https://github.com/digitalcytometry/ecotyper>), the framework for
|
|
5
|
+
identifying cell states and multicellular communities (ecotypes) from bulk,
|
|
6
|
+
single-cell and spatial transcriptomes, by the Newman lab (Stanford).
|
|
7
|
+
|
|
8
|
+
The port reproduces the R implementation: on the README tutorials (compared
|
|
9
|
+
against R on x86-64/glibc), every discrete output is identical, about 60% of the
|
|
10
|
+
files are byte-identical, and all numeric outputs agree to ≤8e-11 relative. See
|
|
11
|
+
the [validation report](https://hoseok-lee.github.io/ecotyper-py/validation.html) for the comparison.
|
|
12
|
+
|
|
13
|
+
## Documentation
|
|
14
|
+
|
|
15
|
+
<https://hoseok-lee.github.io/ecotyper-py/>. Sphinx docs (installation, API reference, step-by-step tutorials, Python vs R
|
|
16
|
+
benchmarks with charts) live in `docs/`. They are built and published by `.github/workflows/docs.yml` on every
|
|
17
|
+
push to `main`. To build them locally:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install -e ".[docs]"
|
|
21
|
+
sphinx-build -b html docs docs/_build/html # open docs/_build/html/index.html
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Credit and license
|
|
25
|
+
|
|
26
|
+
EcoTyper was developed by Bogdan A. Luca, Chloé B. Steen, Aaron M. Newman and
|
|
27
|
+
colleagues. If you use it, cite:
|
|
28
|
+
|
|
29
|
+
- Luca/Steen et al., *Atlas of clinically distinct cell states and ecosystems
|
|
30
|
+
across human solid tumors*, Cell 2021, <https://doi.org/10.1016/j.cell.2021.09.014>
|
|
31
|
+
- Steen/Luca et al., *The landscape of tumor cell states and ecosystems in
|
|
32
|
+
diffuse large B cell lymphoma*, Cancer Cell 2021,
|
|
33
|
+
<https://doi.org/10.1016/j.ccell.2021.08.011>
|
|
34
|
+
|
|
35
|
+
This port is a derivative of EcoTyper and is distributed under the same
|
|
36
|
+
**Stanford non-commercial license** (`LICENSE.md`, included in the package): free for
|
|
37
|
+
non-commercial use only. It is an independent port, not affiliated with or endorsed by
|
|
38
|
+
the EcoTyper authors.
|
|
39
|
+
|
|
40
|
+
CIBERSORTx is closed source and is **not** part of the port. Steps that need it
|
|
41
|
+
(cell type fractions and high-resolution purification) take precomputed
|
|
42
|
+
CIBERSORTx outputs as inputs.
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install ecotyper-py # the import name is `ecotyper`
|
|
48
|
+
pip install "ecotyper-py[icu]" # optional: R-identical string sorting via PyICU
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
A C compiler (`cc`, `gcc` or `clang`) is needed at run time: the exact-parity kernels ship as C source and are
|
|
52
|
+
compiled on first use. From a source checkout, or with conda:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
conda create -n ecotyper-py -c conda-forge python=3.12 numpy scipy pandas anndata numba pyicu pyyaml pytest
|
|
56
|
+
conda activate ecotyper-py
|
|
57
|
+
pip install -e . # from this folder
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
PyICU is optional; it is used to sort strings the way R does in a UTF-8 locale
|
|
61
|
+
(`ECOTYPER_COLLATION=C` switches to byte order, like R with `LC_COLLATE=C`).
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
### AnnData (scverse style)
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
import ecotyper as et
|
|
69
|
+
|
|
70
|
+
# recovery: bulk, single-cell or Visium, inferred from the AnnData
|
|
71
|
+
et.recovery(adata, "Carcinoma", cell_type_key="cell_type", sample_key="patient")
|
|
72
|
+
adata.obs["ecotyper_state_Fibroblasts"] # S01, S02, ... (NaN = other cell types)
|
|
73
|
+
adata.obs["ecotyper_ecotype"] # CE1..CE10
|
|
74
|
+
adata.obsm["ecotyper_abundance_Fibroblasts"] # state abundances
|
|
75
|
+
|
|
76
|
+
# discovery from annotated single cells -> in-memory model
|
|
77
|
+
model = et.discovery(adata, cell_type_key="cell_type", sample_key="patient", n_jobs=16)
|
|
78
|
+
et.recovery(new_adata, model, cell_type_key="cell_type")
|
|
79
|
+
model.save("models/crc") # R layout, usable by R EcoTyper too
|
|
80
|
+
model = et.EcoTyperModel.load("models/crc")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
- Data types: `bulk` (samples x genes), `scRNA` (cells x genes with cell type labels in
|
|
84
|
+
`obs`), `visium` (spots, with CIBERSORTx fractions passed as `fractions=`). Inferred
|
|
85
|
+
from `obsm["spatial"]` and the cell type column; override with `data_type=`.
|
|
86
|
+
- `X` or `layer=` holds raw-scale expression; sparse matrices are accepted.
|
|
87
|
+
- The built-in Carcinoma and Lymphoma models are downloaded (~5 MB each) from a pinned
|
|
88
|
+
commit of the R repository on first use and cached (`ECOTYPER_CACHE`; set
|
|
89
|
+
`ECOTYPER_MODELS` to a local `EcoTyper/` folder to skip the download).
|
|
90
|
+
- Bulk discovery takes CIBERSORTx HiRes (or presorted) profiles:
|
|
91
|
+
`et.discovery({"B.cells": adata_b, "Fibroblasts": adata_fib, ...})`.
|
|
92
|
+
|
|
93
|
+
Results are identical to the file-based workflows below (`tests/test_api.py`).
|
|
94
|
+
|
|
95
|
+
### pandas and files (mirrors the R wrappers)
|
|
96
|
+
|
|
97
|
+
`ecotyper.workflows` has `*_frame` functions (genes x samples DataFrames plus annotation,
|
|
98
|
+
returning every intermediate table) and `*_files` functions that read the R wrappers'
|
|
99
|
+
input files and write the R output layout file for file.
|
|
100
|
+
|
|
101
|
+
### Command line (mirrors the R wrappers, writes the R file layout)
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
ecotyper-py recover-bulk -m bulk_lung_data.txt -a bulk_lung_annotation.txt -d Carcinoma -o out # built-in model
|
|
105
|
+
ecotyper-py recover-scrna -m scRNA_CRC_data.txt -a scRNA_CRC_annotation.txt -d Carcinoma --models ecotyper/EcoTyper -o out
|
|
106
|
+
ecotyper-py recover-visium -i VisiumBreast -f visium_fractions_example.txt -n VisiumBreast -d Carcinoma --models ecotyper/EcoTyper -o out
|
|
107
|
+
ecotyper-py discover -i Tutorial_6/PresortedDiscovery -a Tutorial_6/PresortedDiscovery_annotation.txt -n PresortedDiscovery -o out -t 8
|
|
108
|
+
ecotyper-py discover-scrna -m scRNA_CRC_data.txt -a scRNA_CRC_annotation.txt -n discovery_scRNA_CRC -o out -t 8
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Speed and alternative solver
|
|
112
|
+
|
|
113
|
+
- `exact=True` (default) reproduces R bit-for-bit where R's arithmetic is deterministic (optimised C/numba kernels
|
|
114
|
+
with R's operation order; typically 3-7x faster than R on the single-cell and presorted tutorials).
|
|
115
|
+
- `exact=False` (`--fast` on the CLI) uses BLAS updates and skips R's text round trips. Numerics stay within
|
|
116
|
+
about 1e-10 relative of R, and discrete outputs match on tutorials 1-4 and 6. On tutorial 5, 5 of 11 cell types
|
|
117
|
+
land in a different NMF consensus partition (NMF local optima), so use exact mode when you need R parity.
|
|
118
|
+
- `nmf_method="cd"` (discovery only, `pip install ecotyper-py[sklearn]`) swaps the Brunet updates for scikit-learn's
|
|
119
|
+
coordinate-descent NMF with a Frobenius loss. It is not R-equivalent: chosen ranks and states differ from R.
|
|
120
|
+
See the [performance page](https://hoseok-lee.github.io/ecotyper-py/performance.html) for timings and agreement numbers.
|
|
121
|
+
|
|
122
|
+
### Randomness
|
|
123
|
+
|
|
124
|
+
R's random number generator (Mersenne-Twister with R's seeding, `runif`,
|
|
125
|
+
`sample()` with rejection sampling) is reimplemented in
|
|
126
|
+
`ecotyper.rcompat.rng.RRandom`. Discovery is deterministic as in R
|
|
127
|
+
(`set.seed(1234 + restart)` per NMF run). R's recovery scripts call
|
|
128
|
+
`NMF::rnmf()` **without** seeding, so R's own recovery results change slightly
|
|
129
|
+
from run to run; the port takes an explicit `seed` (default 42), and an R run
|
|
130
|
+
started with `set.seed(42)` (see `validation/r_reference/Rprofile.R`) gives
|
|
131
|
+
the same output.
|
|
132
|
+
|
|
133
|
+
## Layout
|
|
134
|
+
|
|
135
|
+
| module | contents |
|
|
136
|
+
|---|---|
|
|
137
|
+
| `ecotyper.rcompat` | R semantics: RNG, `sample`, number parsing/formatting, `hclust`/`cutree`/`dist`/`silhouette`, `cor`/`var`/`mean`, quantiles, BH, t-test, Wilcoxon, collation, `merge`/`order` |
|
|
138
|
+
| `ecotyper.nmf` | NMF package 0.28: Brunet updates, seeding, connectivity stopping, restarts merging, cophenetic/silhouette/dispersion, `NMFpredict` |
|
|
139
|
+
| `ecotyper.recovery_steps` | state and ecotype recovery (bulk, scRNA, Visium) |
|
|
140
|
+
| `ecotyper.discovery_steps`, `ecotyper.discovery_scrna` | state and ecotype discovery |
|
|
141
|
+
| `ecotyper.api` | `recovery`, `discovery`, `infer_data_type` (scverse-style AnnData API) |
|
|
142
|
+
| `ecotyper.workflows` | pandas and file-based workflows mirroring the R wrappers |
|
|
143
|
+
| `ecotyper.model`, `ecotyper.builtin` | `EcoTyperModel` (load/save/from discovery), built-in model download |
|
|
144
|
+
| `ecotyper.io`, `ecotyper.rlayout` | readers, R-format writers |
|
|
145
|
+
| `ecotyper.validate` | file-by-file comparison with R outputs |
|
|
146
|
+
|
|
147
|
+
Plots (heatmaps) are not ported.
|
|
148
|
+
|
|
149
|
+
## Tests
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
pytest -m "not e2e" # unit parity tests against R fixtures in tests/data
|
|
153
|
+
pytest -m e2e # tutorials vs R reference outputs (needs the R references, see validation/)
|
|
154
|
+
```
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ecotyper-py"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python/AnnData port of EcoTyper (Luca, Steen et al., Cell 2021) with numerical parity to the R implementation"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "LicenseRef-Stanford-NonCommercial"
|
|
11
|
+
license-files = ["LICENSE.md"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
authors = [{ name = "Ho Seok Lee" }]
|
|
14
|
+
keywords = ["ecotyper", "anndata", "scverse", "single-cell", "spatial transcriptomics", "cell states", "NMF"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: C",
|
|
18
|
+
"Operating System :: MacOS",
|
|
19
|
+
"Operating System :: POSIX :: Linux",
|
|
20
|
+
"Intended Audience :: Science/Research",
|
|
21
|
+
"Topic :: Scientific/Engineering :: Bio-Informatics",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"numpy>=1.24",
|
|
25
|
+
"scipy>=1.10",
|
|
26
|
+
"pandas>=2.0",
|
|
27
|
+
"anndata>=0.10",
|
|
28
|
+
"numba>=0.59",
|
|
29
|
+
"pyyaml>=6",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
icu = ["PyICU>=2.10"]
|
|
34
|
+
plot = ["matplotlib>=3.7"]
|
|
35
|
+
sklearn = ["scikit-learn>=1.3"]
|
|
36
|
+
test = ["pytest>=7"]
|
|
37
|
+
docs = ["sphinx>=7", "furo", "myst-parser>=2", "sphinx-copybutton", "sphinx-design"]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Documentation = "https://hoseok-lee.github.io/ecotyper-py/"
|
|
41
|
+
"Original EcoTyper (R)" = "https://github.com/digitalcytometry/ecotyper"
|
|
42
|
+
|
|
43
|
+
[project.scripts]
|
|
44
|
+
ecotyper-py = "ecotyper.cli:main"
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.package-data]
|
|
47
|
+
ecotyper = ["*.c", "rcompat/*.c"]
|
|
48
|
+
|
|
49
|
+
[tool.setuptools.packages.find]
|
|
50
|
+
where = ["src"]
|
|
51
|
+
|
|
52
|
+
[tool.pytest.ini_options]
|
|
53
|
+
testpaths = ["tests"]
|
|
54
|
+
markers = [
|
|
55
|
+
"r: needs the ecotyper-r conda environment (Rscript) to generate references",
|
|
56
|
+
"e2e: end-to-end comparison against R reference outputs (slow)",
|
|
57
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""EcoTyper in Python, with numerical parity to the R implementation.
|
|
2
|
+
|
|
3
|
+
scverse-style API::
|
|
4
|
+
|
|
5
|
+
import ecotyper as et
|
|
6
|
+
|
|
7
|
+
et.recovery(adata, model="Carcinoma") # bulk, scRNA-seq or Visium (inferred)
|
|
8
|
+
model = et.discovery(adata_sc) # scRNA-seq discovery -> EcoTyperModel
|
|
9
|
+
model.save("my_model"); et.EcoTyperModel.load("my_model")
|
|
10
|
+
|
|
11
|
+
File-based workflows mirroring the R scripts are in :mod:`ecotyper.workflows`.
|
|
12
|
+
"""
|
|
13
|
+
from .api import discovery, infer_data_type, recovery
|
|
14
|
+
from .model import EcoTyperModel
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.0"
|
|
17
|
+
__all__ = ["EcoTyperModel", "discovery", "infer_data_type", "recovery"]
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Build and load the C kernels shipped as source (``_nmfk.c``) with the system C compiler.
|
|
2
|
+
|
|
3
|
+
The R-exact Brunet loop (:func:`ecotyper.nmf.run_brunet`) uses ``_nmfk.c`` when it can be
|
|
4
|
+
compiled: same operations as the numba kernels, blocked so that the compiler vectorises them
|
|
5
|
+
(bit-identical results, verified in ``tests/test_nmf.py``). ``ECOTYPER_NMF_KERNEL=numba``
|
|
6
|
+
forces the numba kernels. Built libraries are cached in ``$ECOTYPER_CACHE`` (default: the
|
|
7
|
+
temporary directory).
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import ctypes as C
|
|
12
|
+
import hashlib
|
|
13
|
+
import os
|
|
14
|
+
import platform
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
import tempfile
|
|
18
|
+
import warnings
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from .rcompat._ld import _compiler
|
|
22
|
+
from .rcompat.fma import USE_FMA
|
|
23
|
+
|
|
24
|
+
_SRC = Path(__file__).with_name("_nmfk.c")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _avx512() -> bool:
|
|
28
|
+
try:
|
|
29
|
+
with open("/proc/cpuinfo") as fh:
|
|
30
|
+
return any(line.startswith("flags") and " avx512f" in line for line in fh)
|
|
31
|
+
except OSError:
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _flags() -> list[str]:
|
|
36
|
+
f = ["-O3", "-ffp-contract=off", "-fno-math-errno", "-fPIC", "-shared"]
|
|
37
|
+
if platform.machine().lower() in ("x86_64", "amd64"):
|
|
38
|
+
# AVX2/AVX-512 and an inlined fma(); cached per machine below. Block width = columns
|
|
39
|
+
# processed together: 4 x BW accumulators should fill the vector registers (measured
|
|
40
|
+
# on a Xeon Gold 6258R: BW=64 with 512-bit vectors 3.5-4x faster than the numba kernel).
|
|
41
|
+
f.append("-march=native")
|
|
42
|
+
f += ["-mprefer-vector-width=512", "-DBW=64"] if _avx512() else ["-DBW=32"]
|
|
43
|
+
else:
|
|
44
|
+
f.append("-DBW=16") # Apple silicon: 32 x 128-bit registers
|
|
45
|
+
if USE_FMA:
|
|
46
|
+
f.append("-DUSE_FMA")
|
|
47
|
+
return f
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def build(src: Path, flags: list[str], optional: tuple = ()) -> Path:
|
|
51
|
+
"""Compile ``src`` into a shared library (cached by source, flags and machine), with the
|
|
52
|
+
``optional`` flags (e.g. ``-fopenmp``) if the compiler accepts them."""
|
|
53
|
+
cc = _compiler()
|
|
54
|
+
if cc is None:
|
|
55
|
+
raise RuntimeError("no C compiler found")
|
|
56
|
+
key = src.read_bytes() + " ".join([sys.platform, platform.machine(), platform.node(), cc] + flags).encode()
|
|
57
|
+
tag = hashlib.sha1(key).hexdigest()[:12]
|
|
58
|
+
cache = Path(os.environ.get("ECOTYPER_CACHE", Path(tempfile.gettempdir()) / "ecotyper_ld"))
|
|
59
|
+
cache.mkdir(parents=True, exist_ok=True)
|
|
60
|
+
so = cache / f"{src.stem}_{tag}.so"
|
|
61
|
+
if so.exists():
|
|
62
|
+
return so
|
|
63
|
+
tmp = cache / f"{src.stem}_{tag}.{os.getpid()}.so"
|
|
64
|
+
for extra in ([list(optional)] if optional else []) + [[]]:
|
|
65
|
+
r = subprocess.run([cc, *flags, *extra, str(src), "-o", str(tmp), "-lm"], capture_output=True, text=True)
|
|
66
|
+
if r.returncode == 0:
|
|
67
|
+
os.replace(tmp, so)
|
|
68
|
+
return so
|
|
69
|
+
raise RuntimeError(f"building {src.name} failed:\n{r.stderr}")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _build() -> Path:
|
|
73
|
+
return build(_SRC, _flags(), ("-fopenmp",))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _load():
|
|
77
|
+
if os.environ.get("ECOTYPER_NMF_KERNEL", "c") == "numba":
|
|
78
|
+
return None
|
|
79
|
+
try:
|
|
80
|
+
lib = C.CDLL(str(_build()))
|
|
81
|
+
except Exception as e: # pragma: no cover
|
|
82
|
+
warnings.warn(f"C NMF kernel unavailable ({e}); using the (slower) numba kernels")
|
|
83
|
+
return None
|
|
84
|
+
dp = C.POINTER(C.c_double)
|
|
85
|
+
i64 = C.c_int64
|
|
86
|
+
lib.brunet_loop.restype = i64
|
|
87
|
+
lib.brunet_loop.argtypes = [dp, dp, dp, dp, i64, i64, i64, i64, C.c_int, i64, i64, C.c_double, C.POINTER(i64),
|
|
88
|
+
C.c_int]
|
|
89
|
+
return lib
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
_LIB = None
|
|
93
|
+
_LOADED = False
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def nmf_lib():
|
|
97
|
+
"""The loaded ``_nmfk`` library, or ``None`` (numba fallback)."""
|
|
98
|
+
global _LIB, _LOADED
|
|
99
|
+
if not _LOADED:
|
|
100
|
+
_LIB = _load()
|
|
101
|
+
_LOADED = True
|
|
102
|
+
return _LIB
|