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.
Files changed (46) hide show
  1. ecotyper_py-0.1.0/LICENSE.md +8 -0
  2. ecotyper_py-0.1.0/PKG-INFO +194 -0
  3. ecotyper_py-0.1.0/README.md +154 -0
  4. ecotyper_py-0.1.0/pyproject.toml +57 -0
  5. ecotyper_py-0.1.0/setup.cfg +4 -0
  6. ecotyper_py-0.1.0/src/ecotyper/__init__.py +17 -0
  7. ecotyper_py-0.1.0/src/ecotyper/_ckernel.py +102 -0
  8. ecotyper_py-0.1.0/src/ecotyper/_nmfk.c +224 -0
  9. ecotyper_py-0.1.0/src/ecotyper/api.py +500 -0
  10. ecotyper_py-0.1.0/src/ecotyper/builtin.py +84 -0
  11. ecotyper_py-0.1.0/src/ecotyper/cli.py +109 -0
  12. ecotyper_py-0.1.0/src/ecotyper/discovery_scrna.py +388 -0
  13. ecotyper_py-0.1.0/src/ecotyper/discovery_steps.py +534 -0
  14. ecotyper_py-0.1.0/src/ecotyper/io.py +98 -0
  15. ecotyper_py-0.1.0/src/ecotyper/model.py +254 -0
  16. ecotyper_py-0.1.0/src/ecotyper/nmf.py +545 -0
  17. ecotyper_py-0.1.0/src/ecotyper/rcompat/__init__.py +4 -0
  18. ecotyper_py-0.1.0/src/ecotyper/rcompat/_fmt.c +129 -0
  19. ecotyper_py-0.1.0/src/ecotyper/rcompat/_ld.c +323 -0
  20. ecotyper_py-0.1.0/src/ecotyper/rcompat/_ld.py +106 -0
  21. ecotyper_py-0.1.0/src/ecotyper/rcompat/cluster.py +386 -0
  22. ecotyper_py-0.1.0/src/ecotyper/rcompat/collate.py +186 -0
  23. ecotyper_py-0.1.0/src/ecotyper/rcompat/fma.py +59 -0
  24. ecotyper_py-0.1.0/src/ecotyper/rcompat/frame.py +187 -0
  25. ecotyper_py-0.1.0/src/ecotyper/rcompat/io.py +302 -0
  26. ecotyper_py-0.1.0/src/ecotyper/rcompat/parse.py +300 -0
  27. ecotyper_py-0.1.0/src/ecotyper/rcompat/rng.py +194 -0
  28. ecotyper_py-0.1.0/src/ecotyper/rcompat/stats.py +822 -0
  29. ecotyper_py-0.1.0/src/ecotyper/recovery_steps.py +538 -0
  30. ecotyper_py-0.1.0/src/ecotyper/rlayout.py +70 -0
  31. ecotyper_py-0.1.0/src/ecotyper/validate.py +91 -0
  32. ecotyper_py-0.1.0/src/ecotyper/workflows.py +744 -0
  33. ecotyper_py-0.1.0/src/ecotyper_py.egg-info/PKG-INFO +194 -0
  34. ecotyper_py-0.1.0/src/ecotyper_py.egg-info/SOURCES.txt +44 -0
  35. ecotyper_py-0.1.0/src/ecotyper_py.egg-info/dependency_links.txt +1 -0
  36. ecotyper_py-0.1.0/src/ecotyper_py.egg-info/entry_points.txt +2 -0
  37. ecotyper_py-0.1.0/src/ecotyper_py.egg-info/requires.txt +25 -0
  38. ecotyper_py-0.1.0/src/ecotyper_py.egg-info/top_level.txt +1 -0
  39. ecotyper_py-0.1.0/tests/test_api.py +266 -0
  40. ecotyper_py-0.1.0/tests/test_collate.py +44 -0
  41. ecotyper_py-0.1.0/tests/test_consensus.py +58 -0
  42. ecotyper_py-0.1.0/tests/test_e2e.py +49 -0
  43. ecotyper_py-0.1.0/tests/test_fast.py +80 -0
  44. ecotyper_py-0.1.0/tests/test_nmf.py +96 -0
  45. ecotyper_py-0.1.0/tests/test_rng.py +41 -0
  46. 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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