spaceexpress 0.1.5__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.
- spaceexpress-0.1.5/LICENSE +21 -0
- spaceexpress-0.1.5/MANIFEST.in +2 -0
- spaceexpress-0.1.5/PKG-INFO +81 -0
- spaceexpress-0.1.5/README.md +75 -0
- spaceexpress-0.1.5/RELEASE_0.1.5-en.md +45 -0
- spaceexpress-0.1.5/RELEASE_0.1.5.md +45 -0
- spaceexpress-0.1.5/examples/install_release.sh +14 -0
- spaceexpress-0.1.5/examples/public_pair.py +306 -0
- spaceexpress-0.1.5/examples/public_pair_notebook.py +183 -0
- spaceexpress-0.1.5/examples/run_public_pair_stage.sh +26 -0
- spaceexpress-0.1.5/examples/submit_public_pair.sh +45 -0
- spaceexpress-0.1.5/pyproject.toml +24 -0
- spaceexpress-0.1.5/setup.cfg +4 -0
- spaceexpress-0.1.5/setup.py +9 -0
- spaceexpress-0.1.5/src/SpaceExpress/__init__.py +4 -0
- spaceexpress-0.1.5/src/SpaceExpress/preprocessing.py +109 -0
- spaceexpress-0.1.5/src/SpaceExpress/spaceexpress.py +300 -0
- spaceexpress-0.1.5/src/SpaceExpress/spaceexpress_dse.py +718 -0
- spaceexpress-0.1.5/src/SpaceExpress/utils.py +369 -0
- spaceexpress-0.1.5/src/spaceexpress.egg-info/PKG-INFO +81 -0
- spaceexpress-0.1.5/src/spaceexpress.egg-info/SOURCES.txt +24 -0
- spaceexpress-0.1.5/src/spaceexpress.egg-info/dependency_links.txt +1 -0
- spaceexpress-0.1.5/src/spaceexpress.egg-info/requires.txt +24 -0
- spaceexpress-0.1.5/src/spaceexpress.egg-info/top_level.txt +1 -0
- spaceexpress-0.1.5/tests/test_dse_regressions.py +120 -0
- spaceexpress-0.1.5/tests/test_public_pair.py +47 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Yeojin Kim
|
|
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.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: spaceexpress
|
|
3
|
+
Version: 0.1.5
|
|
4
|
+
Summary: A Python package for spatial transcriptomics
|
|
5
|
+
Author: Yeojin Kim
|
|
6
|
+
Author-email: Yeojin Kim <ykim3030@gatech.edu>
|
|
7
|
+
Project-URL: Repository, https://github.com/YeojinKim220/SpaceExpress
|
|
8
|
+
Keywords: spaceexpress,Python
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: numpy
|
|
13
|
+
Requires-Dist: scipy
|
|
14
|
+
Requires-Dist: pandas>=2.2
|
|
15
|
+
Requires-Dist: anndata>=0.12
|
|
16
|
+
Requires-Dist: scanpy>=1.10
|
|
17
|
+
Requires-Dist: scikit-learn>=1.5
|
|
18
|
+
Requires-Dist: matplotlib>=3.8
|
|
19
|
+
Requires-Dist: torch>=2.1
|
|
20
|
+
Requires-Dist: igraph<1,>=0.11
|
|
21
|
+
Requires-Dist: tqdm
|
|
22
|
+
Requires-Dist: joblib
|
|
23
|
+
Requires-Dist: statsmodels
|
|
24
|
+
Requires-Dist: pygam>=0.9
|
|
25
|
+
Requires-Dist: rpy2<3.6,>=3.5.11
|
|
26
|
+
Provides-Extra: test
|
|
27
|
+
Requires-Dist: pytest; extra == "test"
|
|
28
|
+
Requires-Dist: build; extra == "test"
|
|
29
|
+
Requires-Dist: twine; extra == "test"
|
|
30
|
+
Provides-Extra: notebooks
|
|
31
|
+
Requires-Dist: nbformat; extra == "notebooks"
|
|
32
|
+
Requires-Dist: nbclient; extra == "notebooks"
|
|
33
|
+
Requires-Dist: ipykernel; extra == "notebooks"
|
|
34
|
+
Dynamic: author
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# SpaceExpress 0.1.5
|
|
38
|
+
|
|
39
|
+
Korean version: [RELEASE_0.1.5.md](RELEASE_0.1.5.md)
|
|
40
|
+
|
|
41
|
+
## Changes
|
|
42
|
+
|
|
43
|
+
- Failed gene-embedding-dimension fits (negative or nonfinite statistics) receive FDR=1. Dimensions without a valid empirical-null fit also return FDR=1.
|
|
44
|
+
- Final FDR values are bounded to [0, 1]. The existing empirical-null estimator is not replaced with another adjustment method.
|
|
45
|
+
- `select_hvg_after_outlier` operates on normalized, log-transformed common genes, setting values at or above the pooled mean + 4 sample standard deviations to 0 before batch-aware HVG selection. It modifies copies, not input objects.
|
|
46
|
+
- Initial preprocessing history is stored in `.uns['spaceexpress_preprocessing']`. DSE skips repeated mean+4SD removal when every input has this history. Unmarked legacy inputs retain the previous behavior; mixed histories require an explicit setting.
|
|
47
|
+
- `SpaceExpress_DSE(..., remove_mean_sd_outliers=False)` also explicitly disables subsequent mean+4SD removal. Existing training-time 95% clipping is unchanged. The separate 99% filter in multi-replicate DSE is also retained.
|
|
48
|
+
- Returned AnnData `.varm` includes `DSE-statistic` and `DSE-fit-failed`, so failures can be inspected without inferring them from 0 predictions.
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
Python 3.11 or newer and R are required. Install R packages `lmtest`, `fitdistrplus`, `dplyr`, and `lme4` first, and configure Python to find the R shared library. Python dependencies are declared in package metadata. Use the PyTorch installation appropriate for your CUDA environment.
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
Rscript -e 'install.packages(c("lmtest", "fitdistrplus", "dplyr", "lme4"), repos="https://cloud.r-project.org")'
|
|
56
|
+
python -m pip install 'spaceexpress[notebooks]==0.1.5'
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`examples/install_release.sh BASE_PYTHON NEW_ENV` installs the PyPI release into a new venv sharing an existing scientific/R environment. Package download provenance is recorded in `pip_install_report.json`, with dependencies in `pip_freeze.txt`. This is not a fully isolated installation of all dependencies from scratch.
|
|
60
|
+
|
|
61
|
+
## Running Data
|
|
62
|
+
|
|
63
|
+
`examples/public_pair.py` resolves paths relative to its config and does not overwrite originals. It checks raw counts for at least 3 detected genes and finite spatial coordinates, then applies spatial-bin proportional sampling if needed. Common genes detected in at least 3 observations in each sample are normalized to 10,000, log1p-transformed, and processed as above to select 200 HVGs. Selected raw data and QC metrics are also saved.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
bash examples/submit_public_pair.sh /path/to/env/bin/python /path/to/config_v015.json /path/to/results_v015
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Default Slurm requests are 12 CPU/128 GB for preparation, 1 H100 80 GB GPU with 8 CPU/256 GB RAM for training, 12 CPU/128 GB per DSE job, and 4 CPU/64 GB for the notebook. Each job has a 4-hour limit. Set `SE_ACCOUNT`, `SE_QOS`, `SE_CPU_PARTITION`, `SE_GPU_PARTITION`, and `SE_GPU_CONSTRAINT` to configure your cluster. The default CPU partition is `cpu-medium`. For A100, set `SE_GPU_PARTITION=gpu-a100 SE_GPU_CONSTRAINT=A100-80GB`. These are requests, not measured requirements.
|
|
70
|
+
|
|
71
|
+
Stages run in order: preparation, a 2-epoch GPU probe using all selected observations, main training, k=30/50/100 DSE, and a results notebook. Dependencies require successful prior stages. Main training uses `epochs` and `patience` from the config. Reusing a run directory is rejected; choose a new output path for a new experiment.
|
|
72
|
+
|
|
73
|
+
Outputs include `sample_summary.csv`, QC and gene diagnostics CSVs, selected-raw/prepared/embedded H5ADs, model state, FDR/statistic/failure-mask CSVs, DSE pickles, per-stage JSON/time/memory logs, and an executed `results.ipynb` with figures. The notebook shows original versus actual observation counts, spatial coverage, counts and detected genes, embeddings, failed fits, k sensitivity, and gene expression and fitted results. Only open pickles from trusted runs.
|
|
74
|
+
|
|
75
|
+
## Interpretation and Limits
|
|
76
|
+
|
|
77
|
+
95% clipping can erase a gene's entire training signal in a sample when a sparse gene's upper bound is 0. This can occur after initial 4SD removal and is recorded in `gene_qc_*.csv` and the notebook. This release retains both agreed preprocessing steps and does not automatically substitute another method.
|
|
78
|
+
|
|
79
|
+
Slide-seq observations may be beads and Stereo-seq observations may be spatial bins; observation counts are not necessarily individual-cell counts. Inputs with only Brain tissue annotations are not assigned inferred detailed cell types for visualization. No additional mitochondrial-percentage or detection thresholds are applied automatically; inspect distributions and sample-specific QC before deciding.
|
|
80
|
+
|
|
81
|
+
FDR-range, failure-handling, and observation-preservation checks establish numerical behavior, not biological validity. Review k stability and spatial-neighbor recall as well. A comparison with 1 biological sample per condition does not replace replicated population-level inference.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# SpaceExpress
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
SpaceExpress is a tool for differential spatial expression (DSE) analysis using intrinsic coordinate systems of tissues. It leverages spline models and likelihood ratio testing to identify significant spatial gene expression changes across different biological conditions or experimental groups.
|
|
6
|
+
|
|
7
|
+
## News
|
|
8
|
+
**2024.11.26**
|
|
9
|
+
- Initial release of SpaceExpress, enabling robust differential spatial expression analysis with support for single and multiple replicates.
|
|
10
|
+
|
|
11
|
+
## Getting Started
|
|
12
|
+
To learn how to use SpaceExpress for your spatial transcriptomics data, follow the tutorials:
|
|
13
|
+
|
|
14
|
+
- See **[Tutorials](./docs/source/notebook/)**.
|
|
15
|
+
- **Single Replicate Analysis**: Check [Tutorial 1](./docs/source/notebook/Tutorial_1.ipynb).
|
|
16
|
+
- **Multiple Replicate Analysis**: Check [Tutorial 2](./docs/source/notebook/Tutorial_2.ipynb).
|
|
17
|
+
- **Simulated Data Generation**: Check [Simulated Spatial Transcriptomics Data](./docs/source/notebook/Simulated_Spatial_Transcriptomics_Data.ipynb).
|
|
18
|
+
|
|
19
|
+
These tutorials will guide you through data preparation, model setup, and interpreting the results of differential spatial expression analysis. The expected run time for Tutorial 1 is approximately 1 hour and 30 minutes, while Tutorial 2 is estimated to take around 2 hours and 30 minutes.
|
|
20
|
+
|
|
21
|
+
## Software Dependencies
|
|
22
|
+
To ensure compatibility and optimal performance, please check the required software dependencies specified in the `environment.yml` file. These include key libraries such as `scanpy`, `pandas`, `numpy`, and `rpy2` for seamless integration with R-based statistical models.
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
Install the SpaceExpress environment and package with the following commands (Expected installation time is approximately 5–10 minutes.):
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
git clone https://github.com/YeojinKim220/SpaceExpress.git
|
|
29
|
+
conda env create -f environment.yml
|
|
30
|
+
conda activate spaceexpress-env
|
|
31
|
+
|
|
32
|
+
pip install SpaceExpress
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
This will create and activate a Conda environment with all necessary dependencies and install SpaceExpress in editable mode for local development.
|
|
36
|
+
|
|
37
|
+
<details>
|
|
38
|
+
<summary><b>Note:</b> If the installation via the provided <code>environment.yml</code> file fails, you can manually set up the environment using the steps below:</summary>
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Step 1: Create and activate a new Conda environment
|
|
42
|
+
conda create -n spaceexpress-env python=3.11 scikit-learn pandas matplotlib jupyter scanpy rpy2 -y
|
|
43
|
+
conda activate spaceexpress-env
|
|
44
|
+
|
|
45
|
+
# Step 2: Install additional dependencies
|
|
46
|
+
conda install -y -c conda-forge python-igraph r-lmtest r-fitdistrplus r-dplyr r-lme4
|
|
47
|
+
|
|
48
|
+
# Step 3: Install Python packages using pip
|
|
49
|
+
pip install pygam
|
|
50
|
+
pip install torch
|
|
51
|
+
|
|
52
|
+
# Step 4: Install SpaceExpress
|
|
53
|
+
pip install SpaceExpress
|
|
54
|
+
```
|
|
55
|
+
</details>
|
|
56
|
+
|
|
57
|
+
## Tested Environments and Compatibility
|
|
58
|
+
- **Linux (Red Hat Enterprise Linux 9.4)**: `python-3.11.11`, `scikit-learn-1.6.0`, `pandas-2.2.3`, `matplotlib-3.10.0`, `jupyter-1.1.1`, `scanpy-1.10.4`, `rpy2-3.5.11`, `python-igraph-0.11.8`, `r-lmtest-0.9_40`, `r-fitdistrplus-1.2_1`, `r-dplyr-1.1.4`, `r-lme4-1.1_35.5`, `pygam-0.9.1`, `torch-2.5.1`.
|
|
59
|
+
- **macOS (Sonoma 14.3)**: `python-3.11.10`, `scikit-learn-1.5.1`, `pandas-2.2.2`, `matplotlib-3.9.2`, `jupyter-1.0.0`, `scanpy-1.10.3`, `rpy2-3.5.11`, `python-igraph-0.11.6`, `r-lmtest-0.9_40`, `r-fitdistrplus-1.1_11`, `r-dplyr-1.1.4`, `r-lme4-1.1_35.5`, `pygam-0.9.1`, `torch-2.5.1`.
|
|
60
|
+
|
|
61
|
+
## Guidelines for choosing hyperparameters
|
|
62
|
+
We empirically observed that this hyperparameter setting is effective for datasets with sizes ranging from approximately 3k to 10k cells. For datasets with substantially fewer cells, we recommend using smaller values of the "k" parameter in the SpaceExpress_DSE function.
|
|
63
|
+
|
|
64
|
+
## Citation
|
|
65
|
+
If you use SpaceExpress in your research, please cite:
|
|
66
|
+
```
|
|
67
|
+
@article{kim2024spaceexpress,
|
|
68
|
+
title={SpaceExpress: a method for comparative spatial transcriptomics based on intrinsic coordinate systems of tissues},
|
|
69
|
+
author={Kim, Yeojin and Ojha, Abhishek and Schrader, Alex and Lee, Juyeon and Wu, Zijun and Traniello, Ian M and Robinson, Gene E and Han, Hee Sun and Zhao, Sihai D and Sinha, Saurabh},
|
|
70
|
+
journal={bioRxiv},
|
|
71
|
+
pages={2024--12},
|
|
72
|
+
year={2024},
|
|
73
|
+
publisher={Cold Spring Harbor Laboratory}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# SpaceExpress 0.1.5
|
|
2
|
+
|
|
3
|
+
Korean version: [RELEASE_0.1.5.md](RELEASE_0.1.5.md)
|
|
4
|
+
|
|
5
|
+
## Changes
|
|
6
|
+
|
|
7
|
+
- Failed gene-embedding-dimension fits (negative or nonfinite statistics) receive FDR=1. Dimensions without a valid empirical-null fit also return FDR=1.
|
|
8
|
+
- Final FDR values are bounded to [0, 1]. The existing empirical-null estimator is not replaced with another adjustment method.
|
|
9
|
+
- `select_hvg_after_outlier` operates on normalized, log-transformed common genes, setting values at or above the pooled mean + 4 sample standard deviations to 0 before batch-aware HVG selection. It modifies copies, not input objects.
|
|
10
|
+
- Initial preprocessing history is stored in `.uns['spaceexpress_preprocessing']`. DSE skips repeated mean+4SD removal when every input has this history. Unmarked legacy inputs retain the previous behavior; mixed histories require an explicit setting.
|
|
11
|
+
- `SpaceExpress_DSE(..., remove_mean_sd_outliers=False)` also explicitly disables subsequent mean+4SD removal. Existing training-time 95% clipping is unchanged. The separate 99% filter in multi-replicate DSE is also retained.
|
|
12
|
+
- Returned AnnData `.varm` includes `DSE-statistic` and `DSE-fit-failed`, so failures can be inspected without inferring them from 0 predictions.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Python 3.11 or newer and R are required. Install R packages `lmtest`, `fitdistrplus`, `dplyr`, and `lme4` first, and configure Python to find the R shared library. Python dependencies are declared in package metadata. Use the PyTorch installation appropriate for your CUDA environment.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
Rscript -e 'install.packages(c("lmtest", "fitdistrplus", "dplyr", "lme4"), repos="https://cloud.r-project.org")'
|
|
20
|
+
python -m pip install 'spaceexpress[notebooks]==0.1.5'
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`examples/install_release.sh BASE_PYTHON NEW_ENV` installs the PyPI release into a new venv sharing an existing scientific/R environment. Package download provenance is recorded in `pip_install_report.json`, with dependencies in `pip_freeze.txt`. This is not a fully isolated installation of all dependencies from scratch.
|
|
24
|
+
|
|
25
|
+
## Running Data
|
|
26
|
+
|
|
27
|
+
`examples/public_pair.py` resolves paths relative to its config and does not overwrite originals. It checks raw counts for at least 3 detected genes and finite spatial coordinates, then applies spatial-bin proportional sampling if needed. Common genes detected in at least 3 observations in each sample are normalized to 10,000, log1p-transformed, and processed as above to select 200 HVGs. Selected raw data and QC metrics are also saved.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
bash examples/submit_public_pair.sh /path/to/env/bin/python /path/to/config_v015.json /path/to/results_v015
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Default Slurm requests are 12 CPU/128 GB for preparation, 1 H100 80 GB GPU with 8 CPU/256 GB RAM for training, 12 CPU/128 GB per DSE job, and 4 CPU/64 GB for the notebook. Each job has a 4-hour limit. Set `SE_ACCOUNT`, `SE_QOS`, `SE_CPU_PARTITION`, `SE_GPU_PARTITION`, and `SE_GPU_CONSTRAINT` to configure your cluster. The default CPU partition is `cpu-medium`. For A100, set `SE_GPU_PARTITION=gpu-a100 SE_GPU_CONSTRAINT=A100-80GB`. These are requests, not measured requirements.
|
|
34
|
+
|
|
35
|
+
Stages run in order: preparation, a 2-epoch GPU probe using all selected observations, main training, k=30/50/100 DSE, and a results notebook. Dependencies require successful prior stages. Main training uses `epochs` and `patience` from the config. Reusing a run directory is rejected; choose a new output path for a new experiment.
|
|
36
|
+
|
|
37
|
+
Outputs include `sample_summary.csv`, QC and gene diagnostics CSVs, selected-raw/prepared/embedded H5ADs, model state, FDR/statistic/failure-mask CSVs, DSE pickles, per-stage JSON/time/memory logs, and an executed `results.ipynb` with figures. The notebook shows original versus actual observation counts, spatial coverage, counts and detected genes, embeddings, failed fits, k sensitivity, and gene expression and fitted results. Only open pickles from trusted runs.
|
|
38
|
+
|
|
39
|
+
## Interpretation and Limits
|
|
40
|
+
|
|
41
|
+
95% clipping can erase a gene's entire training signal in a sample when a sparse gene's upper bound is 0. This can occur after initial 4SD removal and is recorded in `gene_qc_*.csv` and the notebook. This release retains both agreed preprocessing steps and does not automatically substitute another method.
|
|
42
|
+
|
|
43
|
+
Slide-seq observations may be beads and Stereo-seq observations may be spatial bins; observation counts are not necessarily individual-cell counts. Inputs with only Brain tissue annotations are not assigned inferred detailed cell types for visualization. No additional mitochondrial-percentage or detection thresholds are applied automatically; inspect distributions and sample-specific QC before deciding.
|
|
44
|
+
|
|
45
|
+
FDR-range, failure-handling, and observation-preservation checks establish numerical behavior, not biological validity. Review k stability and spatial-neighbor recall as well. A comparison with 1 biological sample per condition does not replace replicated population-level inference.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# SpaceExpress 0.1.5
|
|
2
|
+
|
|
3
|
+
English version: [RELEASE_0.1.5-en.md](RELEASE_0.1.5-en.md)
|
|
4
|
+
|
|
5
|
+
## 변경 사항
|
|
6
|
+
|
|
7
|
+
- 실패한 유전자-embedding 차원 적합(음수 또는 비유한 통계량)의 FDR은 1입니다. 유효한 empirical-null 적합이 불가능한 차원도 FDR=1로 반환합니다.
|
|
8
|
+
- 최종 FDR을 [0, 1]로 제한합니다. 기존 empirical-null 추정식을 다른 보정법으로 교체하지 않습니다.
|
|
9
|
+
- `select_hvg_after_outlier`는 정규화·log 변환된 공통 유전자에서 두 표본을 합친 평균 + 4 표본 표준편차 이상의 값을 0으로 바꾼 뒤 batch-aware HVG를 선택합니다. 입력 객체 대신 복사본을 수정합니다.
|
|
10
|
+
- 초기 전처리 이력을 `.uns['spaceexpress_preprocessing']`에 저장합니다. 모든 입력에 이 이력이 있으면 DSE에서 중복 평균+4SD 제거를 건너뜁니다. 이력이 없는 기존 입력은 기존 동작을 유지하고, 이력이 섞여 있으면 명시적 설정을 요구합니다.
|
|
11
|
+
- `SpaceExpress_DSE(..., remove_mean_sd_outliers=False)`로 후속 평균+4SD 제거를 명시적으로 끌 수도 있습니다. 학습 함수의 기존 95% clipping은 변경하지 않았습니다. 다중 반복 표본 DSE의 별도 99% 필터도 유지합니다.
|
|
12
|
+
- 반환 AnnData의 `.varm`에 `DSE-statistic`, `DSE-fit-failed`를 추가하여 실패를 0 예측값으로 추측하지 않고 확인할 수 있습니다.
|
|
13
|
+
|
|
14
|
+
## 설치
|
|
15
|
+
|
|
16
|
+
Python 3.11 이상과 R이 필요합니다. R 패키지 `lmtest`, `fitdistrplus`, `dplyr`, `lme4`를 먼저 설치하고 Python에서 R 공유 라이브러리를 찾을 수 있도록 설정하십시오. Python 의존성은 패키지 메타데이터에 선언했습니다. CUDA에 맞는 PyTorch 설치는 사용하는 GPU 환경을 따릅니다.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
Rscript -e 'install.packages(c("lmtest", "fitdistrplus", "dplyr", "lme4"), repos="https://cloud.r-project.org")'
|
|
20
|
+
python -m pip install 'spaceexpress[notebooks]==0.1.5'
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`examples/install_release.sh BASE_PYTHON NEW_ENV`는 기존 과학 계산·R 환경을 공유하는 새 venv에 PyPI 릴리스를 설치합니다. 패키지 다운로드 출처는 `pip_install_report.json`, 의존성은 `pip_freeze.txt`에 기록합니다. 전체 의존성을 처음부터 새로 설치하는 격리 환경과는 다릅니다.
|
|
24
|
+
|
|
25
|
+
## 데이터 실행
|
|
26
|
+
|
|
27
|
+
`examples/public_pair.py`는 config 기준 상대 경로를 사용하며 원본을 덮어쓰지 않습니다. 원본 count에서 최소 3개 검출 유전자·유한 spatial 좌표를 확인하고, 필요하면 공간 구획별 비례 표집합니다. 두 표본에서 각각 3개 이상 관측치에 검출된 공통 유전자를 정규화(총량 10,000)하고 log1p 후 위 전처리로 HVG 200개를 선택합니다. 원본 선택 데이터와 QC 수치도 저장합니다.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
bash examples/submit_public_pair.sh /path/to/env/bin/python /path/to/config_v015.json /path/to/results_v015
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Slurm 기본 자원은 전처리 12 CPU/128 GB, 학습 H100 80 GB 1장·8 CPU/256 GB RAM, DSE 작업당 12 CPU/128 GB, 노트북 4 CPU/64 GB입니다. 각 작업의 시간 한도는 4시간입니다. `SE_ACCOUNT`, `SE_QOS`, `SE_CPU_PARTITION`, `SE_GPU_PARTITION`, `SE_GPU_CONSTRAINT`로 클러스터 설정을 변경할 수 있습니다. CPU 기본 파티션은 `cpu-medium`입니다. A100 사용 시 `SE_GPU_PARTITION=gpu-a100 SE_GPU_CONSTRAINT=A100-80GB`를 설정하십시오. 이는 요청량이며 측정된 필요량이 아닙니다.
|
|
34
|
+
|
|
35
|
+
전처리, 전체 관측치 2-epoch GPU probe, 본 학습, k=30/50/100 DSE, 결과 노트북 순서로 실행합니다. 앞 단계 성공을 의존 조건으로 사용합니다. 본 학습은 config의 `epochs`, `patience`를 사용합니다. 실행 폴더 재사용은 거부하므로 새 실험에는 새 출력 경로를 지정하십시오.
|
|
36
|
+
|
|
37
|
+
결과에는 `sample_summary.csv`, QC 및 유전자 진단 CSV, 선택 원본·전처리·embedding H5AD, 모델 상태, FDR·통계량·실패 마스크 CSV, DSE pickle, 단계별 JSON·시간·메모리 로그, 그림이 포함된 실행 완료 `results.ipynb`가 포함됩니다. Notebook에서는 원본 대비 실제 관측치 수, 공간 분포, counts·검출 유전자 수, embedding, 실패 적합, k 민감도, 유전자 발현과 적합 결과를 확인합니다. Pickle은 신뢰하는 실행에서 생성한 파일만 여십시오.
|
|
38
|
+
|
|
39
|
+
## 해석과 한계
|
|
40
|
+
|
|
41
|
+
95% clipping은 희소 유전자의 상한이 0이 되면 그 표본에서 해당 유전자의 학습 신호 전체를 없앨 수 있습니다. 초기 4SD 제거 이후에도 발생할 수 있으며, `gene_qc_*.csv`와 notebook에 기록합니다. 이번 릴리스는 합의된 두 전처리를 유지하므로 이를 자동으로 다른 방법으로 바꾸지 않습니다.
|
|
42
|
+
|
|
43
|
+
Slide-seq는 bead, Stereo-seq는 spatial bin일 수 있으므로 관측치 수를 실제 단일 세포 수와 동일시하지 않습니다. Brain tissue annotation만 있는 입력에서 세부 세포 유형을 추정해 표시하지 않습니다. 미토콘드리아 비율이나 검출량에 추가 임계값을 자동 적용하지 않으며, 분포와 표본별 QC를 검토한 뒤 결정해야 합니다.
|
|
44
|
+
|
|
45
|
+
FDR 범위·실패 처리·관측치 보존 검사는 수치적 동작 검사이지 생물학적 검증이 아닙니다. k 안정성과 공간 이웃 보존율도 함께 검토하십시오. 조건당 생물학적 표본 1개인 비교는 반복 표본 기반의 집단 수준 추론을 대신하지 못합니다.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
BASE_PYTHON=${1:?Pass a Python 3.11+ executable with the scientific/R environment}
|
|
4
|
+
ENV_DIR=${2:?Pass a new environment directory}
|
|
5
|
+
if [[ -e "$ENV_DIR" ]]; then
|
|
6
|
+
echo "Refusing to overwrite an existing environment: $ENV_DIR" >&2
|
|
7
|
+
exit 1
|
|
8
|
+
fi
|
|
9
|
+
"$BASE_PYTHON" -m venv --system-site-packages "$ENV_DIR"
|
|
10
|
+
"$ENV_DIR/bin/python" -m pip install --index-url https://pypi.org/simple \
|
|
11
|
+
--no-cache-dir --report "$ENV_DIR/pip_install_report.json" 'spaceexpress[notebooks]==0.1.5'
|
|
12
|
+
"$ENV_DIR/bin/python" -c 'import importlib.metadata as m, SpaceExpress; assert m.version("spaceexpress") == "0.1.5"; print(m.version("spaceexpress"), SpaceExpress.__file__)'
|
|
13
|
+
"$ENV_DIR/bin/python" -m pip freeze > "$ENV_DIR/pip_freeze.txt"
|
|
14
|
+
"$ENV_DIR/bin/python" -m pip check
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Reproducible two-sample run; paths in a config are relative to that config."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import csv
|
|
7
|
+
import hashlib
|
|
8
|
+
import importlib.metadata
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import pickle
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
import time
|
|
15
|
+
import traceback
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
import anndata as ad
|
|
19
|
+
import numpy as np
|
|
20
|
+
import pandas as pd
|
|
21
|
+
import scanpy as sc
|
|
22
|
+
import scipy.sparse as sp
|
|
23
|
+
import torch
|
|
24
|
+
from scipy.sparse.csgraph import connected_components
|
|
25
|
+
from sklearn.neighbors import kneighbors_graph
|
|
26
|
+
|
|
27
|
+
import SpaceExpress as se
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def write_json(path, value):
|
|
31
|
+
path = Path(path)
|
|
32
|
+
temporary = path.with_suffix(path.suffix + '.tmp')
|
|
33
|
+
temporary.write_text(json.dumps(value, indent=2, sort_keys=True) + '\n')
|
|
34
|
+
temporary.replace(path)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def balanced_positions(frame, n, seed):
|
|
38
|
+
if len(frame) < n:
|
|
39
|
+
raise ValueError(f'Requested {n} observations but only {len(frame)} pass QC')
|
|
40
|
+
if len(frame) == n:
|
|
41
|
+
return frame.index.to_numpy()
|
|
42
|
+
work = frame.copy()
|
|
43
|
+
for axis in ('x', 'y'):
|
|
44
|
+
values = work[axis].to_numpy(float)
|
|
45
|
+
edges = np.linspace(values.min(), values.max(), 11)
|
|
46
|
+
work[f'_{axis}bin'] = np.clip(np.digitize(values, edges[1:-1]), 0, 9)
|
|
47
|
+
rng = np.random.default_rng(seed)
|
|
48
|
+
selected = []
|
|
49
|
+
for indices in work.groupby(['_xbin', '_ybin'], observed=True).groups.values():
|
|
50
|
+
quota = int(np.floor(n * len(indices) / len(work)))
|
|
51
|
+
selected.extend(rng.choice(np.asarray(indices), quota, replace=False).tolist())
|
|
52
|
+
pool = work.index[~work.index.isin(selected)].to_numpy()
|
|
53
|
+
selected.extend(rng.choice(pool, n-len(selected), replace=False).tolist())
|
|
54
|
+
return np.asarray(selected)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def count_adata(matrix, obs, genes, coords):
|
|
58
|
+
matrix = sp.csr_matrix(matrix, dtype=np.float32)
|
|
59
|
+
if not np.isfinite(matrix.data).all() or (matrix.data < 0).any():
|
|
60
|
+
raise ValueError('Raw counts must be finite and nonnegative')
|
|
61
|
+
if not np.allclose(matrix.data, np.rint(matrix.data)):
|
|
62
|
+
raise ValueError('Expected raw integer counts, not normalized expression')
|
|
63
|
+
result = ad.AnnData(matrix, obs=obs.copy(), var=pd.DataFrame(index=genes))
|
|
64
|
+
if not result.obs_names.is_unique or not result.var_names.is_unique:
|
|
65
|
+
raise ValueError('Duplicate observation or gene names in raw input')
|
|
66
|
+
result.obsm['spatial'] = np.asarray(coords, dtype=np.float32)
|
|
67
|
+
result.obs['total_counts'] = np.asarray(matrix.sum(axis=1)).ravel()
|
|
68
|
+
result.obs['n_genes_by_counts'] = np.asarray((matrix > 0).sum(axis=1)).ravel()
|
|
69
|
+
mt = result.var_names.str.upper().str.startswith('MT-')
|
|
70
|
+
result.obs['pct_counts_mt'] = 100 * np.asarray(matrix[:, mt].sum(axis=1)).ravel() / np.maximum(result.obs['total_counts'], 1)
|
|
71
|
+
return result
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def load_slide(sample):
|
|
75
|
+
locations = pd.read_csv(sample['locations']).set_index('barcode')
|
|
76
|
+
labels = pd.read_csv(sample['cell_types']).drop(columns=['Unnamed: 0'], errors='ignore').set_index('barcode')
|
|
77
|
+
if not locations.index.is_unique or not labels.index.is_unique:
|
|
78
|
+
raise ValueError('Duplicate barcodes in locations or cell types')
|
|
79
|
+
with open(sample['dge'], newline='') as handle:
|
|
80
|
+
genes = next(csv.reader(handle))[1:]
|
|
81
|
+
matrices, barcodes = [], []
|
|
82
|
+
for chunk in pd.read_csv(sample['dge'], chunksize=256,
|
|
83
|
+
dtype={g: np.float32 for g in genes}):
|
|
84
|
+
barcodes.extend(chunk['barcode'].astype(str))
|
|
85
|
+
matrices.append(sp.csr_matrix(chunk[genes].to_numpy(dtype=np.float32)))
|
|
86
|
+
matrix = sp.vstack(matrices, format='csr')
|
|
87
|
+
obs = pd.DataFrame(index=pd.Index(barcodes, name='barcode'))
|
|
88
|
+
obs = obs.join(locations[['x', 'y']]).join(labels[['max_cell_type']])
|
|
89
|
+
obs['annotation'] = obs['max_cell_type'].astype('string').fillna('Unknown').astype(str)
|
|
90
|
+
return count_adata(matrix, obs, genes, obs[['x', 'y']].to_numpy())
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def load_stereo(sample, root, index):
|
|
94
|
+
backed = ad.read_h5ad(sample['h5ad'], backed='r')
|
|
95
|
+
try:
|
|
96
|
+
full_obs = backed.obs.copy()
|
|
97
|
+
full_obs[['x', 'y']] = np.asarray(backed.obsm['spatial'])[:, :2]
|
|
98
|
+
full_obs.to_csv(root / f'source_observations_{index}.csv')
|
|
99
|
+
mask = full_obs['annotation'].astype(str).eq(sample['annotation']).to_numpy()
|
|
100
|
+
sub = backed[np.flatnonzero(mask), :].to_memory()
|
|
101
|
+
source_count = backed.n_obs
|
|
102
|
+
finally:
|
|
103
|
+
backed.file.close()
|
|
104
|
+
counts = sub.layers['count']
|
|
105
|
+
result = count_adata(counts, sub.obs, sub.var_names, sub.obsm['spatial'])
|
|
106
|
+
return result, source_count
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def graph_policy(pair):
|
|
110
|
+
scans, choices = [], []
|
|
111
|
+
for a in pair:
|
|
112
|
+
counts = {}
|
|
113
|
+
for k in range(4, 21):
|
|
114
|
+
graph = kneighbors_graph(a.obsm['spatial'], k, include_self=False)
|
|
115
|
+
counts[k] = int(connected_components(graph.maximum(graph.T), directed=False)[0])
|
|
116
|
+
choices.append(min(counts, key=lambda k: (counts[k], k)))
|
|
117
|
+
scans.append(counts)
|
|
118
|
+
k = max(choices)
|
|
119
|
+
return k, [{'components_by_k': scan, 'components_at_selected_k': scan[k]} for scan in scans]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def prepare(config, root, status):
|
|
123
|
+
pair, summaries = [], []
|
|
124
|
+
for i, (sample, target) in enumerate(zip(config['samples'], config['target_spots'])):
|
|
125
|
+
if config['type'] == 'slide':
|
|
126
|
+
a = load_slide(sample)
|
|
127
|
+
source_count = a.n_obs
|
|
128
|
+
else:
|
|
129
|
+
a, source_count = load_stereo(sample, root, i)
|
|
130
|
+
selected_tissue_count = a.n_obs
|
|
131
|
+
coords = a.obsm['spatial']
|
|
132
|
+
eligible = np.isfinite(coords).all(axis=1) & (a.obs['n_genes_by_counts'].to_numpy() >= 3)
|
|
133
|
+
frame = a.obs.copy()
|
|
134
|
+
frame[['x', 'y']] = coords
|
|
135
|
+
frame['qc_pass'] = eligible
|
|
136
|
+
candidates = pd.DataFrame(coords[eligible], index=np.flatnonzero(eligible), columns=['x', 'y'])
|
|
137
|
+
chosen = np.sort(balanced_positions(candidates, target, config['seed'] + i))
|
|
138
|
+
frame['selected_for_training'] = False
|
|
139
|
+
frame.iloc[chosen, frame.columns.get_loc('selected_for_training')] = True
|
|
140
|
+
frame.to_csv(root / f'qc_observations_{i}.csv')
|
|
141
|
+
a = a[chosen].copy()
|
|
142
|
+
a.obs['source_sample'] = sample['name']
|
|
143
|
+
a.obs['condition'] = sample['condition']
|
|
144
|
+
a.layers['counts'] = a.X.copy()
|
|
145
|
+
a.write_h5ad(root / f'raw_selected_{i}.h5ad', compression='gzip')
|
|
146
|
+
summaries.append({'sample': sample['name'], 'original_observations': source_count,
|
|
147
|
+
'tissue_observations': selected_tissue_count,
|
|
148
|
+
'qc_observations': int(eligible.sum()), 'training_observations': a.n_obs,
|
|
149
|
+
'original_genes': a.n_vars})
|
|
150
|
+
pair.append(a)
|
|
151
|
+
common = sorted(set(pair[0].var_names).intersection(pair[1].var_names))
|
|
152
|
+
pair = [a[:, common].copy() for a in pair]
|
|
153
|
+
keep = np.logical_and.reduce([np.asarray((a.X > 0).sum(axis=0)).ravel() >= 3 for a in pair])
|
|
154
|
+
pair = [a[:, keep].copy() for a in pair]
|
|
155
|
+
for a in pair:
|
|
156
|
+
sc.pp.normalize_total(a, target_sum=1e4)
|
|
157
|
+
sc.pp.log1p(a)
|
|
158
|
+
pair, hvg, diagnostics = se.select_hvg_after_outlier(pair, n_top_genes=config['n_hvg'], z_threshold=4)
|
|
159
|
+
graph_k, graph_scan = graph_policy(pair)
|
|
160
|
+
clipping = []
|
|
161
|
+
for i, a in enumerate(pair):
|
|
162
|
+
dense = a.X.toarray()
|
|
163
|
+
q95 = np.percentile(dense, 95, axis=0)
|
|
164
|
+
zeros = a.var_names[(q95 == 0) & (dense.max(axis=0) > 0)].tolist()
|
|
165
|
+
clipping.append({'sample': config['samples'][i]['name'],
|
|
166
|
+
'genes_zeroed_by_embedding_q95': zeros,
|
|
167
|
+
'genes_already_all_zero_after_4sd': a.var_names[dense.max(axis=0) == 0].tolist()})
|
|
168
|
+
pd.DataFrame({'gene': a.var_names, 'embedding_q95': q95,
|
|
169
|
+
'positive_observations_after_4sd': (dense > 0).sum(axis=0)}).to_csv(root / f'gene_qc_{i}.csv', index=False)
|
|
170
|
+
if not np.isfinite(dense).all():
|
|
171
|
+
raise ValueError('Nonfinite prepared expression')
|
|
172
|
+
a.write_h5ad(root / f'prepared_condition{i}.h5ad', compression='gzip')
|
|
173
|
+
pd.Series(hvg, name='gene').to_csv(root / 'hvg_genes.csv', index=False)
|
|
174
|
+
pd.DataFrame(summaries).to_csv(root / 'sample_summary.csv', index=False)
|
|
175
|
+
status.update(samples=summaries, hvg_preprocessing=diagnostics, clipping=clipping,
|
|
176
|
+
graph_k=graph_k, graph_scan=graph_scan)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def embed(config, root, status, probe=False):
|
|
180
|
+
if not torch.cuda.is_available():
|
|
181
|
+
raise RuntimeError('CUDA required for the full-size embedding stages')
|
|
182
|
+
pair = [ad.read_h5ad(root / f'prepared_condition{i}.h5ad') for i in range(2)]
|
|
183
|
+
if [a.n_obs for a in pair] != config['target_spots']:
|
|
184
|
+
raise ValueError('Prepared observation counts do not match config')
|
|
185
|
+
graph_k = json.loads((root / 'prepare_status.json').read_text())['graph_k']
|
|
186
|
+
shortest = []
|
|
187
|
+
for i, a in enumerate(pair):
|
|
188
|
+
path = root / f'shortest_condition{i}.pkl'
|
|
189
|
+
if not path.exists():
|
|
190
|
+
temporary = path.with_suffix('.partial')
|
|
191
|
+
if temporary.exists():
|
|
192
|
+
raise RuntimeError(f'Incomplete shortest paths exist: {temporary}; inspect before retrying')
|
|
193
|
+
se.shortest_path(a.obsm['spatial'], str(temporary), k=graph_k)
|
|
194
|
+
temporary.replace(path)
|
|
195
|
+
shortest.append(str(path))
|
|
196
|
+
torch.cuda.reset_peak_memory_stats()
|
|
197
|
+
start = time.perf_counter()
|
|
198
|
+
epochs = 2 if probe else config['epochs']
|
|
199
|
+
embeddings, model = se.train_SpaceExpress_multi(pair, shortest, epochs=epochs,
|
|
200
|
+
patience=config['patience'], emb_dim=4, hid_dim=32, random_seed=config['seed'], save_model=True)
|
|
201
|
+
status.update(device=torch.cuda.get_device_name(), epochs_requested=epochs,
|
|
202
|
+
training_seconds=time.perf_counter()-start,
|
|
203
|
+
peak_cuda_allocated_gib=torch.cuda.max_memory_allocated()/2**30,
|
|
204
|
+
peak_cuda_reserved_gib=torch.cuda.max_memory_reserved()/2**30,
|
|
205
|
+
device_memory_gib=torch.cuda.get_device_properties(0).total_memory/2**30)
|
|
206
|
+
for a, emb in zip(pair, embeddings):
|
|
207
|
+
if emb.shape != (a.n_obs, 4) or not np.isfinite(emb).all() or np.any(emb.std(axis=0) < 1e-8):
|
|
208
|
+
raise ValueError('Nonfinite, collapsed, or incorrectly sized embedding')
|
|
209
|
+
status['embedding_shapes'] = [list(e.shape) for e in embeddings]
|
|
210
|
+
if not probe:
|
|
211
|
+
for i, (a, emb) in enumerate(zip(pair, embeddings)):
|
|
212
|
+
a.obsm['SpaceExpress'] = emb
|
|
213
|
+
a.write_h5ad(root / f'embedded_condition{i}.h5ad', compression='gzip')
|
|
214
|
+
with (root / 'embedding.pkl').open('wb') as handle:
|
|
215
|
+
pickle.dump(embeddings, handle)
|
|
216
|
+
torch.save(model.state_dict(), root / 'model_state.pt')
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def dse(config, root, status, k):
|
|
220
|
+
out = root / f'k{k}'
|
|
221
|
+
out.mkdir(exist_ok=True)
|
|
222
|
+
pair = [ad.read_h5ad(root / f'embedded_condition{i}.h5ad') for i in range(2)]
|
|
223
|
+
embeddings = [a.obsm['SpaceExpress'] for a in pair]
|
|
224
|
+
fdr, fitted = se.SpaceExpress_DSE(embeddings, pair, k=k,
|
|
225
|
+
n_jobs=int(os.environ.get('SLURM_CPUS_PER_TASK', '4')), multi=False)
|
|
226
|
+
values = fdr.to_numpy(float)
|
|
227
|
+
failed = fitted[0].varm['DSE-fit-failed'].to_numpy().T
|
|
228
|
+
if not np.isfinite(values).all() or ((values < 0) | (values > 1)).any():
|
|
229
|
+
raise ValueError('Invalid FDR range')
|
|
230
|
+
if not np.all(values[failed] == 1):
|
|
231
|
+
raise ValueError('Failed fits did not receive FDR=1')
|
|
232
|
+
if any(a.uns['spaceexpress_dse']['remove_mean_sd_outliers'] for a in fitted):
|
|
233
|
+
raise ValueError('Duplicate mean+4SD removal was unexpectedly enabled')
|
|
234
|
+
if not all(np.isfinite(a.obsm[key]).all() for a in fitted for key in ['DSE-pred', 'DSE-inter']):
|
|
235
|
+
raise ValueError('Nonfinite predictions')
|
|
236
|
+
if failed.all():
|
|
237
|
+
raise ValueError('All DSE fits failed')
|
|
238
|
+
fdr.to_csv(out / 'fdr.csv')
|
|
239
|
+
fitted[0].varm['DSE-statistic'].to_csv(out / 'test_statistics.csv')
|
|
240
|
+
fitted[0].varm['DSE-fit-failed'].to_csv(out / 'fit_failed.csv')
|
|
241
|
+
with (out / 'result.pkl').open('wb') as handle:
|
|
242
|
+
pickle.dump([fdr, fitted], handle, protocol=pickle.HIGHEST_PROTOCOL)
|
|
243
|
+
genes = fdr.columns[(fdr < 0.001).any(axis=0)].tolist()
|
|
244
|
+
status.update(k=k, fdr_min=float(values.min()), fdr_max=float(values.max()),
|
|
245
|
+
failed_gene_dimensions=int(failed.sum()),
|
|
246
|
+
failed_genes=fdr.columns[failed.any(axis=0)].tolist(),
|
|
247
|
+
dse_gene_count_0_001=len(genes), dse_genes_0_001=genes,
|
|
248
|
+
repeated_mean_sd_removal=False)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def main():
|
|
252
|
+
parser = argparse.ArgumentParser()
|
|
253
|
+
parser.add_argument('--config', type=Path, required=True)
|
|
254
|
+
parser.add_argument('--output', type=Path, required=True)
|
|
255
|
+
parser.add_argument('--stage', choices=['prepare', 'probe', 'embed', 'dse', 'report'], required=True)
|
|
256
|
+
parser.add_argument('--k', type=int, choices=[30, 50, 100], default=30)
|
|
257
|
+
args = parser.parse_args()
|
|
258
|
+
config_path = args.config.resolve()
|
|
259
|
+
config = json.loads(config_path.read_text())
|
|
260
|
+
for sample in config['samples']:
|
|
261
|
+
for key in ('dge', 'locations', 'cell_types', 'h5ad'):
|
|
262
|
+
if key in sample:
|
|
263
|
+
sample[key] = str((config_path.parent / sample[key]).resolve())
|
|
264
|
+
root = args.output.resolve()
|
|
265
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
266
|
+
stage = f'dse_k{args.k}' if args.stage == 'dse' else args.stage
|
|
267
|
+
status_path = root / f'{stage}_status.json'
|
|
268
|
+
if status_path.exists() and json.loads(status_path.read_text())['status'] == 'PASS':
|
|
269
|
+
raise RuntimeError(f'{stage} already completed; use a new output directory for a fresh run')
|
|
270
|
+
version = importlib.metadata.version('spaceexpress')
|
|
271
|
+
if version != '0.1.5':
|
|
272
|
+
raise RuntimeError(f'Expected installed release 0.1.5, found {version}')
|
|
273
|
+
snapshot = root / 'config_resolved.json'
|
|
274
|
+
if snapshot.exists():
|
|
275
|
+
if json.loads(snapshot.read_text()) != config:
|
|
276
|
+
raise ValueError('Config changed within a run; choose a new output directory')
|
|
277
|
+
else:
|
|
278
|
+
write_json(snapshot, config)
|
|
279
|
+
(root / 'pip_freeze.txt').write_text(subprocess.check_output([sys.executable, '-m', 'pip', 'freeze'], text=True))
|
|
280
|
+
sources = {p.name: hashlib.sha256(p.read_bytes()).hexdigest() for p in Path(se.__file__).parent.glob('*.py')}
|
|
281
|
+
status = {'status': 'RUNNING', 'stage': stage, 'version': version,
|
|
282
|
+
'python': sys.executable, 'package_path': se.__file__, 'source_sha256': sources,
|
|
283
|
+
'slurm_job_id': os.environ.get('SLURM_JOB_ID')}
|
|
284
|
+
write_json(status_path, status)
|
|
285
|
+
start = time.perf_counter()
|
|
286
|
+
try:
|
|
287
|
+
if args.stage == 'prepare':
|
|
288
|
+
prepare(config, root, status)
|
|
289
|
+
elif args.stage in ('probe', 'embed'):
|
|
290
|
+
embed(config, root, status, probe=args.stage == 'probe')
|
|
291
|
+
elif args.stage == 'dse':
|
|
292
|
+
dse(config, root, status, args.k)
|
|
293
|
+
else:
|
|
294
|
+
from public_pair_notebook import build_and_execute
|
|
295
|
+
build_and_execute(root)
|
|
296
|
+
status['status'] = 'PASS'
|
|
297
|
+
except Exception as exc:
|
|
298
|
+
status.update(status='ERROR', error=repr(exc), traceback=traceback.format_exc())
|
|
299
|
+
raise
|
|
300
|
+
finally:
|
|
301
|
+
status['elapsed_seconds'] = time.perf_counter()-start
|
|
302
|
+
write_json(status_path, status)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
if __name__ == '__main__':
|
|
306
|
+
main()
|