dspin 1.5.2__tar.gz → 1.5.4__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.
- {dspin-1.5.2 → dspin-1.5.4}/LICENSE +0 -0
- {dspin-1.5.2/dspin.egg-info → dspin-1.5.4}/PKG-INFO +15 -3
- dspin-1.5.4/README.md +235 -0
- {dspin-1.5.2 → dspin-1.5.4}/dspin/__init__.py +0 -0
- {dspin-1.5.2 → dspin-1.5.4}/dspin/annotate.py +0 -0
- {dspin-1.5.2 → dspin-1.5.4}/dspin/compute.py +15 -7
- {dspin-1.5.2 → dspin-1.5.4}/dspin/dspin.py +15 -7
- {dspin-1.5.2 → dspin-1.5.4}/dspin/plot.py +171 -55
- dspin-1.5.4/dspin/preprocess.py +135 -0
- {dspin-1.5.2 → dspin-1.5.4/dspin.egg-info}/PKG-INFO +15 -3
- dspin-1.5.4/dspin.egg-info/SOURCES.txt +14 -0
- {dspin-1.5.2 → dspin-1.5.4}/dspin.egg-info/dependency_links.txt +0 -0
- {dspin-1.5.2 → dspin-1.5.4}/dspin.egg-info/requires.txt +0 -0
- {dspin-1.5.2 → dspin-1.5.4}/dspin.egg-info/top_level.txt +0 -0
- {dspin-1.5.2 → dspin-1.5.4}/setup.cfg +0 -0
- {dspin-1.5.2 → dspin-1.5.4}/setup.py +1 -1
- dspin-1.5.2/README.md +0 -179
- dspin-1.5.2/__pycache__/__init__.cpython-310.pyc +0 -0
- dspin-1.5.2/__pycache__/__init__.cpython-37.pyc +0 -0
- dspin-1.5.2/__pycache__/compute.cpython-310.pyc +0 -0
- dspin-1.5.2/__pycache__/compute.cpython-37.pyc +0 -0
- dspin-1.5.2/__pycache__/dspin_abstract.cpython-310.pyc +0 -0
- dspin-1.5.2/__pycache__/plot.cpython-310.pyc +0 -0
- dspin-1.5.2/__pycache__/plotting.cpython-37.pyc +0 -0
- dspin-1.5.2/dspin.egg-info/SOURCES.txt +0 -21
- dspin-1.5.2/requirements.txt +0 -6
|
File without changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: dspin
|
|
3
|
-
Version: 1.5.
|
|
3
|
+
Version: 1.5.4
|
|
4
4
|
Summary: Regulatory network models from single-cell perturbation profiling
|
|
5
5
|
Home-page: https://github.com/JialongJiang/DSPIN
|
|
6
6
|
Author: Jialong Jiang
|
|
@@ -25,12 +25,24 @@ Requires-Dist: scanpy
|
|
|
25
25
|
Requires-Dist: tqdm
|
|
26
26
|
Requires-Dist: igraph
|
|
27
27
|
Requires-Dist: leidenalg
|
|
28
|
+
Dynamic: author
|
|
29
|
+
Dynamic: author-email
|
|
30
|
+
Dynamic: classifier
|
|
31
|
+
Dynamic: description
|
|
32
|
+
Dynamic: description-content-type
|
|
33
|
+
Dynamic: home-page
|
|
34
|
+
Dynamic: keywords
|
|
35
|
+
Dynamic: license
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
Dynamic: requires-dist
|
|
38
|
+
Dynamic: requires-python
|
|
39
|
+
Dynamic: summary
|
|
28
40
|
|
|
29
41
|
# D-SPIN: Constructing regulatory network models from scRNA-seq perturbation profiling
|
|
30
42
|
|
|
31
43
|
D-SPIN is a computational framework for constructing regulatory network models of genes or gene programs from single-cell RNA-seq profiling of perturbation conditions. D-SPIN integrates information from multiple perturbation conditions for network inference with improved efficienty and accuracy. The regulatory network can be defined on single genes or gene programs (groups of coexpressed genes). The package includes implementation of gene program discovery, network inference, and network analysis.
|
|
32
44
|
|
|
33
|
-
If D-SPIN is useful for your research, consider citing "D-SPIN constructs
|
|
45
|
+
If D-SPIN is useful for your research, consider citing "D-SPIN constructs regulatory network models from scRNA-seq that reveal organizing principles of perturbation response" ([Cell](https://doi.org/10.1016/j.cell.2026.04.028)).
|
|
34
46
|
|
|
35
47
|
## Contact
|
|
36
48
|
|
dspin-1.5.4/README.md
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# DSPIN
|
|
2
|
+
|
|
3
|
+
Tutorial, code and examples of the D-SPIN framework for the paper "D-SPIN constructs regulatory network models from scRNA-seq that reveal organizing principles of perturbation response" ([Cell](https://doi.org/10.1016/j.cell.2026.04.028))
|
|
4
|
+
|
|
5
|
+
<div align="center">
|
|
6
|
+
<img src="figure/readme/Overview_20251110.png" width="800">
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
D-SPIN is implemented in Python and MATLAB. The Python package is sufficient for most datasets with <100k cells and a few hundred conditions. The MATLAB implementation is more efficient on large datasets and can be deployed on clusters for parallelization using built-in `parfor`.
|
|
12
|
+
|
|
13
|
+
Install the Python package:
|
|
14
|
+
|
|
15
|
+
`pip install dspin`
|
|
16
|
+
|
|
17
|
+
The MATLAB code is available in the folder `DSPIN_matlab` and is executable after specifying paths to the saved data from the preprocessing with the Python package (see `run_with_matlab=True` below).
|
|
18
|
+
|
|
19
|
+
## Demos
|
|
20
|
+
|
|
21
|
+
A tutorial intended for an invited book chapter is available at bioRxiv:
|
|
22
|
+
|
|
23
|
+
[A guide to D-SPIN: constructing regulatory network models from single-cell RNA-seq perturbation data](https://www.biorxiv.org/content/10.1101/2025.07.28.667326.full).
|
|
24
|
+
|
|
25
|
+
Two demos of D-SPIN are available on Google Colab:
|
|
26
|
+
|
|
27
|
+
- **Demo1**: reconstructs a regulatory network from simulated hematopoietic stem cell (HSC) TF expression data under single-gene perturbations simulated with the BEELINE framework (Pratapa et al., *Nat. Methods*, 2020).
|
|
28
|
+
[Demo1](https://colab.research.google.com/drive/1Nja8AAcP0L7Ag6DGbyn21SxGafsYX4fA?usp=sharing)
|
|
29
|
+
|
|
30
|
+
- **Demo2**: reconstructs a regulatory network and response vectors using a subset of the immune dictionary dataset (Cui et al., *Nature*, 2024), where mice were treated with cytokines and lymph nodes were collected and profiled by scRNA-seq.
|
|
31
|
+
[Demo2](https://colab.research.google.com/drive/1oLECn3ystwtwfIRiSii9oe92QJ9ac5s1?usp=sharing)
|
|
32
|
+
|
|
33
|
+
## Dependencies
|
|
34
|
+
|
|
35
|
+
DSPIN was tested with:
|
|
36
|
+
|
|
37
|
+
- python (3.9.18)
|
|
38
|
+
- anndata (0.10.3)
|
|
39
|
+
- matplotlib (3.8.2)
|
|
40
|
+
- scanpy (1.9.6)
|
|
41
|
+
- tqdm (4.65.0)
|
|
42
|
+
- leidenalg (0.10.1)
|
|
43
|
+
- igraph (0.10.8)
|
|
44
|
+
|
|
45
|
+
Note: other versions may work as well.
|
|
46
|
+
|
|
47
|
+
## Input data
|
|
48
|
+
|
|
49
|
+
D-SPIN can work with many perturbation types under the assumption that the perturbation conditions share the same core regulatory network:
|
|
50
|
+
|
|
51
|
+
- **Genetic screens** – Perturb-seq, CRISPR knockdown/activation, RNAi
|
|
52
|
+
- **Chemical or signaling cues** – drug treatments, growth-factor changes
|
|
53
|
+
- **Physiological differences** – healthy vs disease, different patients, time courses
|
|
54
|
+
- **Spatial niches** – local micro-environments from spatial transcriptomics
|
|
55
|
+
|
|
56
|
+
Because D-SPIN models the *distribution* of transcriptional states, it is designed for **single-cell RNA-seq** data. The model can also work with bulk RNA-seq data but the performance is limited.
|
|
57
|
+
|
|
58
|
+
### AnnData requirements
|
|
59
|
+
|
|
60
|
+
Minimum fields expected by the examples below:
|
|
61
|
+
|
|
62
|
+
- `adata.X` — log-normalized (`log1p`) matrix after QC (filter low-quality cells, high mitochondrial content, and typically use HVGs).
|
|
63
|
+
- `adata.obs['sample_id']` — condition label; cells with the same value form one perturbation group.
|
|
64
|
+
- `adata.obs['batch']` — batch label; perturbation effects are compared within each batch when controls exist.
|
|
65
|
+
- `adata.obs['if_control']` — `True` for controls, `False` otherwise.
|
|
66
|
+
|
|
67
|
+
Practical guidance:
|
|
68
|
+
- Aim for **≥ 25 cells per condition** when possible.
|
|
69
|
+
- If no explicit controls exist, D-SPIN can still compute responses relative to the global average, but matched controls per batch are preferred.
|
|
70
|
+
|
|
71
|
+
## Building network models with D-SPIN
|
|
72
|
+
|
|
73
|
+
### Gene-level network models
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from dspin.dspin import DSPIN
|
|
77
|
+
|
|
78
|
+
model = DSPIN(adata, save_path, num_spin=adata.shape[1]) # gene-level
|
|
79
|
+
model.network_inference(
|
|
80
|
+
sample_id_key='sample_id',
|
|
81
|
+
method='pseudo_likelihood',
|
|
82
|
+
directed=True,
|
|
83
|
+
# optional priors / constraints:
|
|
84
|
+
# sample_list_ordered=None,
|
|
85
|
+
# perturb_matrix_prior=None, # shape: (n_genes, n_samples)
|
|
86
|
+
# prior_network=None, # binary matrix of likely edges (e.g., motif/ATAC prior)
|
|
87
|
+
run_with_matlab=False,
|
|
88
|
+
params={'stepsz': 0.05, 'lambda_l1_j': 0.01}
|
|
89
|
+
)
|
|
90
|
+
model.response_relative_to_control(
|
|
91
|
+
sample_id_key='sample_id',
|
|
92
|
+
if_control_key='if_control',
|
|
93
|
+
batch_key='batch'
|
|
94
|
+
)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Results saved to `model`:**
|
|
98
|
+
- `model.network` – regulatory weights **J**
|
|
99
|
+
- `model.responses` – responses **h** for each condition
|
|
100
|
+
- `model.relative_responses` – responses relative to control (preferably within each batch)
|
|
101
|
+
|
|
102
|
+
**Tips**
|
|
103
|
+
- `num_spin=adata.shape[1]` - gene-level network
|
|
104
|
+
- `directed=True` - only supported with `pseudo_likelihood`
|
|
105
|
+
- `prior_network` (optional) - prior knowledge on network edges (e.g., TF–motif binding, ATAC-seq data)
|
|
106
|
+
- `perturb_matrix_prior` (optional) - prior on direct perturbation targets (e.g., Perturb-seq)
|
|
107
|
+
- `run_with_matlab=True` - write variables to `save_path` for MATLAB inference on large datasets
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
### Program-level network models
|
|
112
|
+
|
|
113
|
+
Program-level models first discover gene programs (via consensus oNMF), then infer a network over programs.
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from dspin.dspin import DSPIN
|
|
117
|
+
|
|
118
|
+
model = DSPIN(adata, save_path, num_spin=20) # e.g., 20 programs
|
|
119
|
+
model.gene_program_discovery(
|
|
120
|
+
num_repeat=10,
|
|
121
|
+
seed=0,
|
|
122
|
+
cluster_key='cell_type' # optional: balance cell types
|
|
123
|
+
)
|
|
124
|
+
model.network_inference(
|
|
125
|
+
sample_id_key='sample_id',
|
|
126
|
+
method='mcmc_maximum_likelihood'
|
|
127
|
+
)
|
|
128
|
+
model.response_relative_to_control(
|
|
129
|
+
sample_id_key='sample_id',
|
|
130
|
+
if_control_key='if_control',
|
|
131
|
+
batch_key='batch'
|
|
132
|
+
)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Extra outputs**
|
|
136
|
+
- Program compositions under `save_path/onmf`
|
|
137
|
+
- Consensus program gene lists under `save_path`
|
|
138
|
+
|
|
139
|
+
**Tips**
|
|
140
|
+
- A practical heuristic is `num_spin` ≈ 5 × (number of major cell types/clusters), but keep `num_spin ≤ 40` for interpretability.
|
|
141
|
+
- `cluster_key` lets D-SPIN down-sample over-represented cell types before oNMF to reduce overfitting.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
### Finding gene regulators of programs
|
|
146
|
+
|
|
147
|
+
Given a program-level model (`model_program`) and a gene-level model (`model_gene`), D-SPIN can regress program activities onto the gene network to nominate regulators.
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
# model_program : program-level DSPIN object
|
|
151
|
+
# model_gene : gene-level DSPIN object
|
|
152
|
+
model_gene.program_regulator_discovery(
|
|
153
|
+
model_program.program_representation,
|
|
154
|
+
sample_id_key='sample_id',
|
|
155
|
+
params={'stepsz': 0.02, 'lambda_l1_interaction': 0.001}
|
|
156
|
+
)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Outputs in `model_gene`:**
|
|
160
|
+
- `model_gene.program_interactions` – regression coefficients (gene ↔ program)
|
|
161
|
+
- `model_gene.program_activities` – global activity for each program
|
|
162
|
+
|
|
163
|
+
**Tips**
|
|
164
|
+
- The two models should be built from `adata` objects with matching `adata.obs` (so each cell aligns between gene- and program-level representations).
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
### Module discovery and visualization (optional)
|
|
169
|
+
|
|
170
|
+
The package includes helpers for module discovery (Leiden clustering) and plotting.
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
import dspin.plot as dp
|
|
174
|
+
|
|
175
|
+
# Example: program-level network
|
|
176
|
+
G, j_filt = dp.create_undirected_network(
|
|
177
|
+
model.network,
|
|
178
|
+
node_names=spin_name, # list of program (or gene) names
|
|
179
|
+
thres_strength=0.05
|
|
180
|
+
)
|
|
181
|
+
module_list = dp.compute_modules(G, resolution=1, seed=0)
|
|
182
|
+
|
|
183
|
+
dp.plot_network_heatmap(j_filt, module_list, spin_name_list=spin_name)
|
|
184
|
+
dp.plot_network_diagram(
|
|
185
|
+
j_filt, module_list,
|
|
186
|
+
pos=None,
|
|
187
|
+
directed=False,
|
|
188
|
+
weight_thres=0.15,
|
|
189
|
+
spin_name_list_short=spin_name_short
|
|
190
|
+
)
|
|
191
|
+
dp.plot_response_heatmap(
|
|
192
|
+
model.relative_responses,
|
|
193
|
+
module_list,
|
|
194
|
+
spin_name_list=spin_name,
|
|
195
|
+
sample_list=model.sample_list
|
|
196
|
+
)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Tips**
|
|
200
|
+
- `resolution` controls Leiden module granularity (higher - smaller modules).
|
|
201
|
+
- For gene-level models, `spin_name` is typically gene names; for program-level models, use program labels or representative gene names.
|
|
202
|
+
|
|
203
|
+
## Application to the T cell population of the immune dictionary dataset
|
|
204
|
+
|
|
205
|
+
<div align="center">
|
|
206
|
+
<img src="figure/readme/Figure2_immune_dictionary_20250928.png" width="600">
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
<div align="left">
|
|
210
|
+
<b>Figure: Overview and program discovery for the immune dictionary dataset (Cui et al., Nature 2024).</b>
|
|
211
|
+
(A) t-SNE embedding for a subset of cell populations from the immune dictionary dataset. The subset dataset includes CD4, CD8, and regulatory T cells treated by 12 different cytokines, as well as corresponding control samples treated by phosphate-buffered saline (PBS).
|
|
212
|
+
(B) Heatmaps of gene expression and discretized gene program levels for control and IFN-α1-treated samples. The gene programs are weighted averages of single-gene expressions that characterize and denoise the major expression pattern of the gene matrix.
|
|
213
|
+
</div>
|
|
214
|
+
<br>
|
|
215
|
+
<div align="center">
|
|
216
|
+
<img src="figure/readme/Figure3_program_network_20261009.png" width="600">
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
<div align="left">
|
|
220
|
+
<b>Figure: Program-level and gene-level regulatory network inferred by D-SPIN.</b>
|
|
221
|
+
(A) Diagram of D-SPIN-inferred network model on gene programs. The network is partitioned into 4 modules, each associated with a T cell type in the population.
|
|
222
|
+
(B) Heatmap of the program response of each cytokine. Clustering of the response partitions the cytokines into 3 major categories.
|
|
223
|
+
(C) Diagram of the core subnetwork of the D-SPIN-inferred gene network model. The node sizes scale with the number of identified interactions. The network is partitioned into 4 modules, each primarily composed of genes that have elevated expression in one specific T cell type.
|
|
224
|
+
(D) Interaction diagram for the subnetwork of IFN-α1/IFN-β and IFN-γ acting on the program P17-IFN-γ response. The D-SPIN model shows that Type I and Type II interferons have different effectors to activate the program.
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
## References
|
|
228
|
+
|
|
229
|
+
1. Jiang, Jialong, et al. "D-SPIN constructs regulatory network models from scRNA-seq that reveal organizing principles of perturbation response." Cell 189(13):4159-4184.e35 (2026).
|
|
230
|
+
|
|
231
|
+
2. Jiang, Jialong, and Thomson, Matt. "A guide to D-SPIN: constructing regulatory network models from single-cell RNA-seq perturbation data." bioRxiv (2025).
|
|
232
|
+
|
|
233
|
+
3. Pratapa, Aditya, et al. "Benchmarking algorithms for gene regulatory network inference from single-cell transcriptomic data." Nature methods 17.2 (2020): 147-154.
|
|
234
|
+
|
|
235
|
+
4. Cui, Ang, et al. "Dictionary of immune responses to cytokines at single-cell resolution." Nature 625.7994 (2024): 377-384.
|
|
File without changes
|
|
File without changes
|
|
@@ -413,6 +413,8 @@ def subsample_normalize_gene_matrix(gene_matrix: np.ndarray,
|
|
|
413
413
|
if method is None:
|
|
414
414
|
gene_matrix_balanced = gene_matrix[np.random.choice(
|
|
415
415
|
num_cell, num_subsample, replace=False), :]
|
|
416
|
+
if issparse(gene_matrix_balanced):
|
|
417
|
+
gene_matrix_balanced = gene_matrix_balanced.toarray()
|
|
416
418
|
else:
|
|
417
419
|
cluster_list, cluster_count = np.unique(cluster_label, return_counts=True)
|
|
418
420
|
|
|
@@ -1343,8 +1345,8 @@ def learn_network_adam(raw_data: Any,
|
|
|
1343
1345
|
if if_backtrack:
|
|
1344
1346
|
print('Backtracking at epoch %d' % counter)
|
|
1345
1347
|
backtrack_counter += 1
|
|
1346
|
-
cur_j = rec_jmat_all[counter - 1 - backtrack_gap, :, :]
|
|
1347
|
-
cur_h = rec_hvec_all[counter - 1 - backtrack_gap, :, :]
|
|
1348
|
+
cur_j = rec_jmat_all[counter - 1 - backtrack_gap, :, :].copy()
|
|
1349
|
+
cur_h = rec_hvec_all[counter - 1 - backtrack_gap, :, :].copy()
|
|
1348
1350
|
|
|
1349
1351
|
mjj, vjj, mhh, vhh = [log_adam_grad[key][0]
|
|
1350
1352
|
for key in ['mjj', 'vjj', 'mhh', 'vhh']]
|
|
@@ -1411,7 +1413,7 @@ def learn_program_regulators(gene_states: List[np.ndarray],
|
|
|
1411
1413
|
train_dat.get(key, None) for key in [
|
|
1412
1414
|
"backtrack_gap", "backtrack_tol"])
|
|
1413
1415
|
|
|
1414
|
-
lambda_l1_j, lambda_l2_j = (train_dat.get(key, 0) for key in ["
|
|
1416
|
+
lambda_l1_j, lambda_l2_j = (train_dat.get(key, 0) for key in ["lambda_l1_interaction", "lambda_l2_j"])
|
|
1415
1417
|
|
|
1416
1418
|
# Initialize parameters (using zeros)
|
|
1417
1419
|
cur_interaction = np.zeros((num_gene, num_program))
|
|
@@ -1429,6 +1431,7 @@ def learn_program_regulators(gene_states: List[np.ndarray],
|
|
|
1429
1431
|
rec_selfh_all = np.zeros((num_epoch, num_program))
|
|
1430
1432
|
|
|
1431
1433
|
rec_grad = np.ones(num_epoch) * np.inf
|
|
1434
|
+
rec_grad_data = np.ones(num_epoch) * np.inf
|
|
1432
1435
|
|
|
1433
1436
|
m_interaction, v_interaction = np.zeros_like(cur_interaction), np.zeros_like(cur_interaction)
|
|
1434
1437
|
m_selfj, v_selfj = np.zeros_like(cur_selfj), np.zeros_like(cur_selfj)
|
|
@@ -1492,6 +1495,8 @@ def learn_program_regulators(gene_states: List[np.ndarray],
|
|
|
1492
1495
|
selfj_grad = - np.sum(rec_selfj_grad * samp_weight[:, None], axis=0)
|
|
1493
1496
|
selfh_grad = - np.sum(rec_selfh_grad * samp_weight[:, None], axis=0)
|
|
1494
1497
|
|
|
1498
|
+
# Data-gradient norm for backtracking; the annealed L1 term grows by design and would trigger it
|
|
1499
|
+
rec_grad_data[counter - 1] = np.linalg.norm(interaction_grad)
|
|
1495
1500
|
if lambda_l1_j > 0:
|
|
1496
1501
|
interaction_grad += lambda_l1_j * (cur_interaction / l1_base).clip(- 1, 1)
|
|
1497
1502
|
if lambda_l2_j > 0:
|
|
@@ -1522,12 +1527,12 @@ def learn_program_regulators(gene_states: List[np.ndarray],
|
|
|
1522
1527
|
pbar.set_postfix({"Network Gradient": f"{rec_grad[counter - 1]:.6f}"})
|
|
1523
1528
|
|
|
1524
1529
|
if_backtrack = False
|
|
1525
|
-
if (counter > backtrack_gap) and (
|
|
1530
|
+
if (counter > backtrack_gap) and (rec_grad_data[counter - 1] > 1.5 * rec_grad_data[counter - 1 - backtrack_gap]):
|
|
1526
1531
|
if_backtrack = True
|
|
1527
1532
|
|
|
1528
1533
|
if (counter > 2 * backtrack_gap) and (counter % backtrack_gap == 0):
|
|
1529
|
-
est_cur_grad =
|
|
1530
|
-
est_prev_grad =
|
|
1534
|
+
est_cur_grad = rec_grad_data[counter - 1 - backtrack_gap: counter - 1].mean()
|
|
1535
|
+
est_prev_grad = rec_grad_data[counter - 1 - 2 * backtrack_gap: counter - 1 - backtrack_gap].mean()
|
|
1531
1536
|
|
|
1532
1537
|
if est_cur_grad > 1.05 * est_prev_grad:
|
|
1533
1538
|
if_backtrack = True
|
|
@@ -1535,8 +1540,11 @@ def learn_program_regulators(gene_states: List[np.ndarray],
|
|
|
1535
1540
|
if if_backtrack:
|
|
1536
1541
|
print('Backtracking at epoch %d' % counter)
|
|
1537
1542
|
backtrack_counter += 1
|
|
1543
|
+
cur_interaction = rec_interaction_all[counter - 1 - backtrack_gap, :, :].copy()
|
|
1544
|
+
cur_selfj = rec_selfj_all[counter - 1 - backtrack_gap, :].copy()
|
|
1545
|
+
cur_selfh = rec_selfh_all[counter - 1 - backtrack_gap, :].copy()
|
|
1538
1546
|
m_interaction, v_interaction, m_selfj, v_selfj, m_selfh, v_selfh = [log_adam_grad[key][0] for key in ["m_interaction", "v_interaction", "m_selfj", "v_selfj", "m_selfh", "v_selfh"]]
|
|
1539
|
-
counter = counter - backtrack_gap
|
|
1547
|
+
counter = counter - backtrack_gap + 1
|
|
1540
1548
|
stepsz = stepsz / 4
|
|
1541
1549
|
if backtrack_counter > backtrack_tol:
|
|
1542
1550
|
print(f'Backtracking more than {backtrack_tol} times, stop training.')
|
|
@@ -168,7 +168,10 @@ class AbstractDSPIN(ABC):
|
|
|
168
168
|
|
|
169
169
|
if clip_percentile < 100:
|
|
170
170
|
for ii in range(onmf_rep_ori.shape[1]):
|
|
171
|
-
|
|
171
|
+
clip_value = np.percentile(onmf_rep_ori[:, ii], clip_percentile)
|
|
172
|
+
if clip_value == 0:
|
|
173
|
+
clip_value = np.max(onmf_rep_ori[:, ii])
|
|
174
|
+
onmf_rep_ori[:, ii] = onmf_rep_ori[:, ii] / clip_value
|
|
172
175
|
onmf_rep_ori = onmf_rep_ori.clip(0, 1)
|
|
173
176
|
|
|
174
177
|
onmf_rep_tri = onmf_discretize(onmf_rep_ori, num_init, fig_folder)
|
|
@@ -262,7 +265,7 @@ class AbstractDSPIN(ABC):
|
|
|
262
265
|
params: dict = None,
|
|
263
266
|
sample_list_ordered: List[str] = None,
|
|
264
267
|
prior_network: np.ndarray = None,
|
|
265
|
-
|
|
268
|
+
perturb_matrix_prior: np.ndarray = None,
|
|
266
269
|
if_control_key: str = 'if_control',
|
|
267
270
|
batch_key: str = 'batch',
|
|
268
271
|
run_with_matlab: bool = False) -> None:
|
|
@@ -283,7 +286,7 @@ class AbstractDSPIN(ABC):
|
|
|
283
286
|
List of ordered sample identifiers. Default is None.
|
|
284
287
|
prior_network : np.ndarray, optional
|
|
285
288
|
Binary matrix of preferred network connectivity from prior knowledge. Default is None.
|
|
286
|
-
|
|
289
|
+
perturb_matrix_prior : np.ndarray, optional
|
|
287
290
|
Matrix of putative relative responses to control samples. Default is None.
|
|
288
291
|
if_control_key : str, optional
|
|
289
292
|
Column name in `adata.obs` indicating whether a sample is a control. Default is 'if_control'.
|
|
@@ -335,7 +338,7 @@ class AbstractDSPIN(ABC):
|
|
|
335
338
|
train_dat['j_prior_mask'] = prior_network
|
|
336
339
|
train_dat['lambda_l1_j_prior_mask'] = 0.01
|
|
337
340
|
|
|
338
|
-
if
|
|
341
|
+
if perturb_matrix_prior is not None:
|
|
339
342
|
if if_control_key not in self.adata.obs.columns:
|
|
340
343
|
raise ValueError(f"Column '{if_control_key}' not found in adata.obs.")
|
|
341
344
|
if batch_key not in self.adata.obs.columns:
|
|
@@ -348,7 +351,7 @@ class AbstractDSPIN(ABC):
|
|
|
348
351
|
if_control = np.array([sample_to_control[sample] for sample in sample_list])
|
|
349
352
|
batch_index = np.array([sample_to_batch[sample] for sample in sample_list])
|
|
350
353
|
|
|
351
|
-
train_dat['perturb_matrix'] =
|
|
354
|
+
train_dat['perturb_matrix'] = perturb_matrix_prior
|
|
352
355
|
train_dat['if_control'] = if_control
|
|
353
356
|
train_dat['batch_index'] = batch_index
|
|
354
357
|
train_dat['lambda_l2_h_rela_prior'] = 0.5
|
|
@@ -464,6 +467,7 @@ class GeneDSPIN(AbstractDSPIN):
|
|
|
464
467
|
if dparams['clip_percentile'] < 100:
|
|
465
468
|
print(f"Input gene expression seem to be heavy-tailed. Using {dparams['clip_percentile']} percentile for clipping before discretization.")
|
|
466
469
|
|
|
470
|
+
np.random.seed(0)
|
|
467
471
|
self.discretize(clip_percentile=dparams['clip_percentile'],
|
|
468
472
|
num_init=dparams['num_init'])
|
|
469
473
|
|
|
@@ -491,7 +495,7 @@ class GeneDSPIN(AbstractDSPIN):
|
|
|
491
495
|
train_dat = {
|
|
492
496
|
'num_epoch': 500,
|
|
493
497
|
'stepsz': 0.01,
|
|
494
|
-
'lambda_l1_interaction': 0.
|
|
498
|
+
'lambda_l1_interaction': 0.001,
|
|
495
499
|
'rec_gap': 10,
|
|
496
500
|
'backtrack_gap': 40,
|
|
497
501
|
'backtrack_tol': 5
|
|
@@ -792,6 +796,9 @@ class ProgramDSPIN(AbstractDSPIN):
|
|
|
792
796
|
self.discretize(clip_percentile=dparams['clip_percentile'],
|
|
793
797
|
num_init=dparams['num_init'])
|
|
794
798
|
|
|
799
|
+
if mode == 'compute_only':
|
|
800
|
+
return
|
|
801
|
+
|
|
795
802
|
num_display_gene = 5
|
|
796
803
|
gene_pd = pd.read_csv(self.gene_program_csv, index_col=None)
|
|
797
804
|
|
|
@@ -802,7 +809,7 @@ class ProgramDSPIN(AbstractDSPIN):
|
|
|
802
809
|
sub_list_filtered = [gene for gene in sub_list if str(gene) != 'nan']
|
|
803
810
|
top_gene_list_filtered.append(sub_list_filtered)
|
|
804
811
|
|
|
805
|
-
program_names = [f'P{ii}-' + ','.join(top_gene_list_filtered[ii]) for ii in range(num_spin)]
|
|
812
|
+
program_names = [f'P{ii + 1}-' + ','.join(top_gene_list_filtered[ii]) for ii in range(num_spin)]
|
|
806
813
|
self.name_list = program_names
|
|
807
814
|
self.name_list_short = [name.split('-')[0] for name in program_names]
|
|
808
815
|
|
|
@@ -845,6 +852,7 @@ class DSPIN(object):
|
|
|
845
852
|
warnings.warn("Negative expression detected. Expect normalized log-transformed data.")
|
|
846
853
|
|
|
847
854
|
# Filtering genes based on the filter_threshold.
|
|
855
|
+
adata = adata.copy()
|
|
848
856
|
sc.pp.filter_genes(adata, min_cells=adata.shape[0] * filter_threshold)
|
|
849
857
|
print(
|
|
850
858
|
'{} genes have expression in more than {} of the cells'.format(
|
|
@@ -19,8 +19,11 @@ from scipy.spatial.distance import pdist, squareform
|
|
|
19
19
|
from scipy.cluster.hierarchy import linkage, leaves_list
|
|
20
20
|
import matplotlib as mpl
|
|
21
21
|
from typing import List, Dict, Optional, Tuple
|
|
22
|
+
import re
|
|
22
23
|
|
|
23
24
|
|
|
25
|
+
cmap_hvec = mpl.colors.LinearSegmentedColormap.from_list("", ['#E84B23', '#FFFFFF', '#3285CC'])
|
|
26
|
+
|
|
24
27
|
def onmf_to_csv(features, gene_name, file_path, thres=0.01, if_write_weights=False):
|
|
25
28
|
"""
|
|
26
29
|
Extract gene names in ONMF to CSV.
|
|
@@ -74,9 +77,6 @@ def onmf_gene_program_info(features, gene_name, num_gene_show, fig_folder=None):
|
|
|
74
77
|
gene_name (List[str]): Names of the genes corresponding to feature indices.
|
|
75
78
|
num_gene_show (int): Number of genes to show in the plot.
|
|
76
79
|
fig_folder (str): Folder where the figure will be saved.
|
|
77
|
-
|
|
78
|
-
Returns:
|
|
79
|
-
str: Path of the created figure.
|
|
80
80
|
"""
|
|
81
81
|
thres = 0.01
|
|
82
82
|
num_spin = features.shape[0]
|
|
@@ -100,10 +100,8 @@ def onmf_gene_program_info(features, gene_name, num_gene_show, fig_folder=None):
|
|
|
100
100
|
if fig_folder is not None:
|
|
101
101
|
plt.savefig(fig_folder + 'onmf_gene_program_info.png', dpi=300, bbox_inches='tight')
|
|
102
102
|
|
|
103
|
-
return(fig_folder + 'onmf_gene_program_info.png')
|
|
104
|
-
|
|
105
103
|
|
|
106
|
-
def assign_program_position(onmf_rep_ori, umap_all, repulsion=2):
|
|
104
|
+
def assign_program_position(onmf_rep_ori, umap_all, repulsion=2, if_plot=True):
|
|
107
105
|
"""
|
|
108
106
|
Assign the position of gene programs on the UMAP plot.
|
|
109
107
|
|
|
@@ -111,7 +109,8 @@ def assign_program_position(onmf_rep_ori, umap_all, repulsion=2):
|
|
|
111
109
|
onmf_rep_ori (numpy.ndarray): The gene or gene program representation of the transformed data.
|
|
112
110
|
umap_all (numpy.ndarray): The UMAP coordinates of all cells.
|
|
113
111
|
repulsion (float): The repulsion strength for the layout optimization.
|
|
114
|
-
|
|
112
|
+
if_plot (bool): Whether to plot the UMAP plot.
|
|
113
|
+
|
|
115
114
|
Returns:
|
|
116
115
|
numpy.ndarray: The assigned positions of gene programs on the UMAP plot.
|
|
117
116
|
"""
|
|
@@ -133,22 +132,24 @@ def assign_program_position(onmf_rep_ori, umap_all, repulsion=2):
|
|
|
133
132
|
opt_res = optimize.minimize(layout_loss_fun, program_umap_pos.flatten())
|
|
134
133
|
program_umap_pos = opt_res.x.reshape(- 1, 2)
|
|
135
134
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if umap_all.shape[0] <= 5e4:
|
|
139
|
-
plt.scatter(umap_all[:, 0], umap_all[:, 1], s=1, c='#bbbbbb', alpha=min(1, 1e4 / umap_all.shape[0]))
|
|
140
|
-
else:
|
|
141
|
-
sele_ind = np.random.choice(umap_all.shape[0], size=50000, replace=False).astype(int)
|
|
142
|
-
plt.scatter(umap_all[sele_ind, 0], umap_all[sele_ind, 1], s=1, c='#bbbbbb', alpha=0.2)
|
|
135
|
+
if if_plot:
|
|
143
136
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
137
|
+
sc.set_figure_params(figsize=[4, 4])
|
|
138
|
+
|
|
139
|
+
if umap_all.shape[0] <= 5e4:
|
|
140
|
+
plt.scatter(umap_all[:, 0], umap_all[:, 1], s=1, c='#bbbbbb', alpha=min(1, 1e4 / umap_all.shape[0]))
|
|
141
|
+
else:
|
|
142
|
+
sele_ind = np.random.choice(umap_all.shape[0], size=50000, replace=False).astype(int)
|
|
143
|
+
plt.scatter(umap_all[sele_ind, 0], umap_all[sele_ind, 1], s=1, c='#bbbbbb', alpha=0.2)
|
|
144
|
+
|
|
145
|
+
for ii in range(num_spin):
|
|
146
|
+
plt.text(program_umap_pos[ii, 0], program_umap_pos[ii, 1], str(ii), fontsize=10)
|
|
147
|
+
plt.axis('off')
|
|
147
148
|
|
|
148
149
|
return program_umap_pos
|
|
149
150
|
|
|
150
151
|
|
|
151
|
-
def gene_program_on_umap(onmf_rep, umap_all, program_umap_pos, fig_folder=None, subsample=True):
|
|
152
|
+
def gene_program_on_umap(onmf_rep, umap_all, program_umap_pos=None, program_names=None,fig_folder=None, subsample=True):
|
|
152
153
|
"""
|
|
153
154
|
Plot gene programs on the UMAP plot.
|
|
154
155
|
|
|
@@ -156,6 +157,7 @@ def gene_program_on_umap(onmf_rep, umap_all, program_umap_pos, fig_folder=None,
|
|
|
156
157
|
onmf_rep (numpy.ndarray): The gene or gene program representation of the transformed data.
|
|
157
158
|
umap_all (numpy.ndarray): The UMAP coordinates of all cells.
|
|
158
159
|
program_umap_pos (numpy.ndarray): The assigned positions of gene programs on the UMAP plot.
|
|
160
|
+
program_names (List[str]): The names of gene programs.
|
|
159
161
|
fig_folder (str): The folder where the output figure is saved.
|
|
160
162
|
subsample (bool): Whether to subsample the data for plotting.
|
|
161
163
|
|
|
@@ -165,16 +167,29 @@ def gene_program_on_umap(onmf_rep, umap_all, program_umap_pos, fig_folder=None,
|
|
|
165
167
|
|
|
166
168
|
num_spin = onmf_rep.shape[1]
|
|
167
169
|
|
|
168
|
-
if
|
|
169
|
-
|
|
170
|
+
if program_umap_pos is None:
|
|
171
|
+
program_umap_pos = assign_program_position(onmf_rep, umap_all, if_plot=False)
|
|
172
|
+
|
|
173
|
+
if program_names is None:
|
|
174
|
+
program_names = [str(ii) for ii in range(num_spin)]
|
|
175
|
+
|
|
176
|
+
# Check if all program names follow the "P<number>" format
|
|
177
|
+
match_nums = [re.search(r'\d+', name) for name in program_names]
|
|
178
|
+
if all(m is not None for m in match_nums):
|
|
179
|
+
program_order = sorted(range(len(program_names)), key=lambda i: int(match_nums[i].group()))
|
|
180
|
+
else:
|
|
181
|
+
program_order = sorted(range(len(program_names)), key=lambda i: program_names[i])
|
|
182
|
+
|
|
183
|
+
num_subsample = 20000
|
|
184
|
+
if subsample & (onmf_rep.shape[0] > num_subsample):
|
|
170
185
|
sub_ind = np.random.choice(onmf_rep.shape[0], num_subsample, replace=False)
|
|
171
186
|
onmf_rep = onmf_rep[sub_ind, :]
|
|
172
187
|
umap_all = umap_all[sub_ind, :]
|
|
173
188
|
|
|
174
189
|
sc.set_figure_params(figsize=[2, 2])
|
|
175
190
|
fig, grid = sc.pl._tools._panel_grid(0.2, 0.06, ncols=6, num_panels=num_spin)
|
|
176
|
-
for spin in
|
|
177
|
-
ax = plt.subplot(grid[
|
|
191
|
+
for ind, spin in enumerate(program_order):
|
|
192
|
+
ax = plt.subplot(grid[ind])
|
|
178
193
|
|
|
179
194
|
plot_data = onmf_rep[:, spin].copy()
|
|
180
195
|
plot_data /= np.percentile(plot_data, 95)
|
|
@@ -182,15 +197,14 @@ def gene_program_on_umap(onmf_rep, umap_all, program_umap_pos, fig_folder=None,
|
|
|
182
197
|
|
|
183
198
|
plt.scatter(umap_all[:, 0], umap_all[:, 1], c=plot_data, s=1,
|
|
184
199
|
alpha=0.5, vmax=1.2, cmap='BuPu', vmin=-0.1)
|
|
185
|
-
plt.text(program_umap_pos[spin, 0], program_umap_pos[spin, 1],
|
|
200
|
+
plt.text(program_umap_pos[spin, 0], program_umap_pos[spin, 1], program_names[spin], fontsize=12, path_effects=[patheffects.withStroke(linewidth=3, foreground='w')])
|
|
186
201
|
ax.set_aspect('equal')
|
|
187
202
|
plt.xticks([])
|
|
188
203
|
plt.yticks([])
|
|
189
|
-
plt.title(spin)
|
|
204
|
+
plt.title(program_names[spin])
|
|
190
205
|
|
|
191
206
|
if fig_folder is not None:
|
|
192
207
|
plt.savefig(fig_folder + 'gene_program_on_umap.png', dpi=300, bbox_inches='tight')
|
|
193
|
-
return fig_folder + 'gene_program_on_umap.png'
|
|
194
208
|
|
|
195
209
|
|
|
196
210
|
def visualize_program_expression(onmf_summary, spin_name, gene_matrix, onmf_rep_tri, fig_folder=None,
|
|
@@ -207,9 +221,6 @@ def visualize_program_expression(onmf_summary, spin_name, gene_matrix, onmf_rep_
|
|
|
207
221
|
num_gene_select (int): The number of genes to be selected for each gene program. Default is 10.
|
|
208
222
|
n_clusters (int): The number of clusters for KMeans clustering. Default is 5.
|
|
209
223
|
subsample (int): The number cell subsampling for visualization. Default is 10000.
|
|
210
|
-
|
|
211
|
-
Returns:
|
|
212
|
-
str: Path of the created figure.
|
|
213
224
|
"""
|
|
214
225
|
# Extracting feature components from the ONMF summary object
|
|
215
226
|
features = onmf_summary.components_
|
|
@@ -265,7 +276,6 @@ def visualize_program_expression(onmf_summary, spin_name, gene_matrix, onmf_rep_
|
|
|
265
276
|
|
|
266
277
|
if fig_folder is not None:
|
|
267
278
|
plt.savefig(fig_folder + 'gene_program_decomposition.png', bbox_inches='tight')
|
|
268
|
-
return fig_folder + 'gene_program_decomposition.png'
|
|
269
279
|
|
|
270
280
|
|
|
271
281
|
def format_label(label):
|
|
@@ -309,6 +319,51 @@ def temporary_spin_name(csv_file, num_gene: int = 4):
|
|
|
309
319
|
return spin_names_formatted
|
|
310
320
|
|
|
311
321
|
|
|
322
|
+
def plot_program_heatmap(onmf_rep: np.ndarray, celltype_label: np.ndarray, program_name: str, fig_folder: Optional[str] = None) -> None:
|
|
323
|
+
|
|
324
|
+
"""
|
|
325
|
+
Plot a heatmap of the program expression.
|
|
326
|
+
|
|
327
|
+
Parameters
|
|
328
|
+
----------
|
|
329
|
+
onmf_rep : np.ndarray
|
|
330
|
+
The program expression.
|
|
331
|
+
celltype_label : np.ndarray
|
|
332
|
+
The celltype label.
|
|
333
|
+
program_name : str
|
|
334
|
+
The program name.
|
|
335
|
+
fig_folder : str
|
|
336
|
+
The folder to save the figure.
|
|
337
|
+
"""
|
|
338
|
+
|
|
339
|
+
celltype_label_list = np.unique(celltype_label)
|
|
340
|
+
num_celltype = len(celltype_label_list)
|
|
341
|
+
num_program = onmf_rep.shape[1]
|
|
342
|
+
|
|
343
|
+
average_onmf_rep = np.zeros((num_celltype, num_program))
|
|
344
|
+
for ii in range(num_celltype):
|
|
345
|
+
average_onmf_rep[ii, :] = np.mean(onmf_rep[celltype_label == celltype_label_list[ii], :], axis=0)
|
|
346
|
+
|
|
347
|
+
for jj in range(num_program):
|
|
348
|
+
average_onmf_rep[:, jj] = average_onmf_rep[:, jj] / np.average(average_onmf_rep[:, jj])
|
|
349
|
+
|
|
350
|
+
celltype_order = leaves_list(linkage(average_onmf_rep))
|
|
351
|
+
program_order = leaves_list(linkage(average_onmf_rep.T))
|
|
352
|
+
|
|
353
|
+
sc.set_figure_params(figsize=[0.24 * num_celltype, 0.24 * num_program])
|
|
354
|
+
|
|
355
|
+
plt.imshow(average_onmf_rep[celltype_order, :][:, program_order].T, cmap='Blues', vmin=0, vmax=2.5, aspect='auto')
|
|
356
|
+
|
|
357
|
+
plt.yticks(np.arange(num_program), np.array(program_name)[program_order], fontsize=10);
|
|
358
|
+
plt.xticks(np.arange(num_celltype), np.array(celltype_label_list)[celltype_order], fontsize=10, rotation=90);
|
|
359
|
+
|
|
360
|
+
plt.xlabel('Cell types')
|
|
361
|
+
plt.ylabel('Gene programs')
|
|
362
|
+
|
|
363
|
+
if fig_folder is not None:
|
|
364
|
+
plt.savefig(fig_folder + 'program_heatmap.png')
|
|
365
|
+
|
|
366
|
+
|
|
312
367
|
def plot_network_heatmap(j_mat: np.ndarray,
|
|
313
368
|
module_list: List[List[int]] = None,
|
|
314
369
|
spin_name_list: Optional[List[str]] = None,
|
|
@@ -319,7 +374,9 @@ def plot_network_heatmap(j_mat: np.ndarray,
|
|
|
319
374
|
Parameters
|
|
320
375
|
----------
|
|
321
376
|
j_mat : np.ndarray
|
|
322
|
-
The network adjacency matrix.
|
|
377
|
+
The network adjacency matrix, with rows as regulators and columns as targets (e.g., nx.to_numpy_array
|
|
378
|
+
of the graph from create_directed_network). model.network is stored as [target, regulator], so pass
|
|
379
|
+
model.network.T for a directed network.
|
|
323
380
|
modules : List[List[int]]
|
|
324
381
|
List of modules, where each module is a list of node indices.
|
|
325
382
|
spin_name_list : List[str], optional
|
|
@@ -338,13 +395,11 @@ def plot_network_heatmap(j_mat: np.ndarray,
|
|
|
338
395
|
spin_order = [spin for cur_list in module_list for spin in cur_list]
|
|
339
396
|
net_class_len = [len(cur_list) for cur_list in module_list]
|
|
340
397
|
|
|
341
|
-
# Create custom colormap
|
|
342
|
-
cmap_hvec = mpl.colors.LinearSegmentedColormap.from_list("", ['#E84B23', '#FFFFFF', '#3285CC'])
|
|
343
|
-
|
|
344
398
|
color_thres = np.percentile(np.abs(j_mat[j_mat != 0]), 95)
|
|
345
399
|
|
|
346
400
|
# Plot heatmap
|
|
347
|
-
|
|
401
|
+
fig_size = np.max([3.7, 0.18 * num_spin])
|
|
402
|
+
sc.set_figure_params(figsize=[fig_size + 0.5, fig_size])
|
|
348
403
|
plt.imshow(j_mat[:, spin_order][spin_order, :], cmap=cmap_hvec, vmin=- color_thres, vmax=color_thres)
|
|
349
404
|
|
|
350
405
|
# Add module boundaries
|
|
@@ -352,16 +407,19 @@ def plot_network_heatmap(j_mat: np.ndarray,
|
|
|
352
407
|
plt.axhline(np.sum(net_class_len[:ii]) - 0.5, color='k', linewidth=1)
|
|
353
408
|
plt.axvline(np.sum(net_class_len[:ii]) - 0.5, color='k', linewidth=1)
|
|
354
409
|
|
|
355
|
-
fsize = 9
|
|
410
|
+
fsize = np.max([9, int(fig_size / num_spin * 50)])
|
|
356
411
|
|
|
357
412
|
# Set labels
|
|
358
413
|
if spin_name_list is not None:
|
|
359
414
|
plt.xticks(np.arange(len(spin_order)), np.array(spin_name_list)[spin_order], rotation=90, fontsize=fsize)
|
|
360
415
|
plt.yticks(np.arange(len(spin_order)), np.array(spin_name_list)[spin_order], fontsize=fsize)
|
|
361
416
|
else:
|
|
362
|
-
plt.xticks(np.arange(len(spin_order)), [f'P{spin}' for spin in spin_order], rotation=90, fontsize=fsize)
|
|
363
|
-
plt.yticks(np.arange(len(spin_order)), [f'P{spin}' for spin in spin_order], fontsize=fsize)
|
|
364
|
-
|
|
417
|
+
plt.xticks(np.arange(len(spin_order)), [f'P{spin + 1}' for spin in spin_order], rotation=90, fontsize=fsize)
|
|
418
|
+
plt.yticks(np.arange(len(spin_order)), [f'P{spin + 1}' for spin in spin_order], fontsize=fsize)
|
|
419
|
+
|
|
420
|
+
plt.xlabel('Targets')
|
|
421
|
+
plt.ylabel('Regulators')
|
|
422
|
+
|
|
365
423
|
plt.colorbar(fraction=0.6 / num_spin)
|
|
366
424
|
plt.grid()
|
|
367
425
|
|
|
@@ -369,8 +427,6 @@ def plot_network_heatmap(j_mat: np.ndarray,
|
|
|
369
427
|
if fig_folder is not None:
|
|
370
428
|
plt.savefig(fig_folder + 'network_heatmap.png', dpi=300, bbox_inches='tight')
|
|
371
429
|
|
|
372
|
-
plt.show()
|
|
373
|
-
|
|
374
430
|
|
|
375
431
|
def plot_response_heatmap(h_vec: np.ndarray,
|
|
376
432
|
module_list: List[List[int]]=None,
|
|
@@ -404,8 +460,6 @@ def plot_response_heatmap(h_vec: np.ndarray,
|
|
|
404
460
|
net_class_len = [len(cur_list) for cur_list in module_list]
|
|
405
461
|
sample_order = leaves_list(linkage(h_vec.T, method='ward'))
|
|
406
462
|
|
|
407
|
-
cmap_hvec = mpl.colors.LinearSegmentedColormap.from_list("", ['#E84B23', '#FFFFFF', '#3285CC'])
|
|
408
|
-
|
|
409
463
|
# Plot heatmap
|
|
410
464
|
sc.set_figure_params(figsize=[0.16 * num_spin + 0.5, 0.16 * num_sample])
|
|
411
465
|
plt.imshow(h_vec.T[sample_order, :][:, spin_order], cmap=cmap_hvec, vmin=- 1, vmax=1, aspect='auto')
|
|
@@ -418,9 +472,12 @@ def plot_response_heatmap(h_vec: np.ndarray,
|
|
|
418
472
|
if spin_name_list is not None:
|
|
419
473
|
plt.xticks(np.arange(len(spin_order)), np.array(spin_name_list)[spin_order], rotation=90, fontsize=8)
|
|
420
474
|
else:
|
|
421
|
-
plt.xticks(np.arange(len(spin_order)), [f'P{spin}' for spin in spin_order], rotation=90, fontsize=8)
|
|
475
|
+
plt.xticks(np.arange(len(spin_order)), [f'P{spin + 1}' for spin in spin_order], rotation=90, fontsize=8)
|
|
422
476
|
|
|
423
|
-
|
|
477
|
+
if sample_list is not None:
|
|
478
|
+
plt.yticks(np.arange(len(sample_order)), np.array(sample_list)[sample_order], fontsize=8)
|
|
479
|
+
else:
|
|
480
|
+
plt.yticks(np.arange(len(sample_order)), [f'S{sample + 1}' for sample in sample_order], fontsize=8)
|
|
424
481
|
|
|
425
482
|
plt.colorbar(fraction=1 / num_spin)
|
|
426
483
|
plt.grid()
|
|
@@ -429,11 +486,10 @@ def plot_response_heatmap(h_vec: np.ndarray,
|
|
|
429
486
|
if fig_folder is not None:
|
|
430
487
|
plt.savefig(fig_folder + 'module_heatmap.png', dpi=300, bbox_inches='tight')
|
|
431
488
|
|
|
432
|
-
plt.show()
|
|
433
|
-
|
|
434
489
|
|
|
435
490
|
def create_undirected_network(j_mat, node_names, thres_strength=0.05):
|
|
436
491
|
|
|
492
|
+
j_mat = j_mat.copy()
|
|
437
493
|
np.fill_diagonal(j_mat, 0)
|
|
438
494
|
|
|
439
495
|
# Filter weak connections
|
|
@@ -450,6 +506,7 @@ def create_undirected_network(j_mat, node_names, thres_strength=0.05):
|
|
|
450
506
|
|
|
451
507
|
def create_directed_network(j_mat, node_names,thres_strength=0.05, thres_direction=0.05):
|
|
452
508
|
|
|
509
|
+
j_mat = j_mat.copy()
|
|
453
510
|
np.fill_diagonal(j_mat, 0)
|
|
454
511
|
|
|
455
512
|
num_spin = j_mat.shape[0]
|
|
@@ -635,7 +692,9 @@ def plot_network_diagram(j_mat: np.ndarray,
|
|
|
635
692
|
Parameters
|
|
636
693
|
----------
|
|
637
694
|
j_mat : np.ndarray
|
|
638
|
-
The network adjacency matrix.
|
|
695
|
+
The network adjacency matrix, with rows as regulators and columns as targets (e.g., nx.to_numpy_array
|
|
696
|
+
of the graph from create_directed_network). model.network is stored as [target, regulator], so pass
|
|
697
|
+
model.network.T for a directed network.
|
|
639
698
|
modules : List[List[int]]
|
|
640
699
|
List of modules, where each module is a list of node indices.
|
|
641
700
|
directed : bool, optional
|
|
@@ -674,7 +733,7 @@ def plot_network_diagram(j_mat: np.ndarray,
|
|
|
674
733
|
if spin_name_list_short is not None:
|
|
675
734
|
label = {ii: spin_name_list_short[ii] for ii in range(num_spin)}
|
|
676
735
|
else:
|
|
677
|
-
label = {ii: f'P{ii}' for ii in range(num_spin)}
|
|
736
|
+
label = {ii: f'P{ii + 1}' for ii in range(num_spin)}
|
|
678
737
|
|
|
679
738
|
# Plot network
|
|
680
739
|
fig_size = 4 * num_spin ** 0.25
|
|
@@ -717,8 +776,9 @@ def plot_network_diagram(j_mat: np.ndarray,
|
|
|
717
776
|
# Save figure if folder is provided
|
|
718
777
|
if fig_folder is not None:
|
|
719
778
|
plt.savefig(fig_folder + 'network_diagram.png', dpi=300, bbox_inches='tight')
|
|
720
|
-
|
|
779
|
+
|
|
721
780
|
plt.show()
|
|
781
|
+
|
|
722
782
|
|
|
723
783
|
def plot_top_regulators(program_interactions: np.ndarray,
|
|
724
784
|
program_index: int,
|
|
@@ -745,13 +805,12 @@ def plot_top_regulators(program_interactions: np.ndarray,
|
|
|
745
805
|
fig_folder : Optional[str], optional
|
|
746
806
|
The folder to save the figure.
|
|
747
807
|
"""
|
|
748
|
-
|
|
749
808
|
cur_regulators = program_interactions[:, program_index]
|
|
750
809
|
if thres is None:
|
|
751
|
-
thres = np.sort(np.abs(cur_regulators))[::-1][20]
|
|
810
|
+
thres = np.sort(np.abs(cur_regulators))[::-1][min(20, len(cur_regulators) - 1)]
|
|
752
811
|
|
|
753
|
-
num_gene_show_pos = np.sum(cur_regulators >= thres)
|
|
754
|
-
num_gene_show_neg = np.sum(cur_regulators <= -thres)
|
|
812
|
+
num_gene_show_pos = np.sum((cur_regulators >= thres) & (cur_regulators > 0))
|
|
813
|
+
num_gene_show_neg = np.sum((cur_regulators <= -thres) & (cur_regulators < 0))
|
|
755
814
|
num_gene_show = num_gene_show_pos + num_gene_show_neg
|
|
756
815
|
|
|
757
816
|
plot_ind_pos = np.argsort(cur_regulators)[::-1][:num_gene_show_pos]
|
|
@@ -771,6 +830,63 @@ def plot_top_regulators(program_interactions: np.ndarray,
|
|
|
771
830
|
plt.title(program_name, fontsize=10)
|
|
772
831
|
|
|
773
832
|
if fig_folder is not None:
|
|
774
|
-
plt.savefig(fig_folder + '
|
|
833
|
+
plt.savefig(fig_folder + f'top_regulators_program_{program_index}.png')
|
|
775
834
|
|
|
776
|
-
|
|
835
|
+
|
|
836
|
+
def plot_all_regulators(program_interactions: np.ndarray,
|
|
837
|
+
gene_list: np.array,
|
|
838
|
+
program_name: Optional[str] = None,
|
|
839
|
+
thres: Optional[float] = 0.2,
|
|
840
|
+
fig_folder: Optional[str] = None) -> None:
|
|
841
|
+
"""
|
|
842
|
+
Plot the all regulators of all programs.
|
|
843
|
+
|
|
844
|
+
Parameters
|
|
845
|
+
----------
|
|
846
|
+
program_interactions : np.ndarray
|
|
847
|
+
The program interactions.
|
|
848
|
+
gene_list : np.array
|
|
849
|
+
The list of genes.
|
|
850
|
+
program_name : Optional[str], optional
|
|
851
|
+
The name of the program.
|
|
852
|
+
thres : Optional[float], optional
|
|
853
|
+
The threshold for the regulators.
|
|
854
|
+
fig_folder : Optional[str], optional
|
|
855
|
+
The folder to save the figure.
|
|
856
|
+
"""
|
|
857
|
+
program_interactions = program_interactions.copy()
|
|
858
|
+
program_interactions_raw = program_interactions.copy()
|
|
859
|
+
num_gene, num_program = program_interactions.shape
|
|
860
|
+
|
|
861
|
+
if thres is not None:
|
|
862
|
+
program_interactions[np.abs(program_interactions) < thres] = 0
|
|
863
|
+
|
|
864
|
+
gene_remove = np.sum(np.abs(program_interactions), axis=1) == 0
|
|
865
|
+
program_interactions = program_interactions[~gene_remove, :]
|
|
866
|
+
program_interactions_raw = program_interactions_raw[~gene_remove, :]
|
|
867
|
+
gene_list = np.array(gene_list)[~gene_remove]
|
|
868
|
+
|
|
869
|
+
num_gene, num_program = program_interactions.shape
|
|
870
|
+
|
|
871
|
+
sc.set_figure_params(figsize=[0.18 * num_gene, 0.2 * num_program])
|
|
872
|
+
|
|
873
|
+
gene_order = leaves_list(linkage(program_interactions_raw))
|
|
874
|
+
program_order = leaves_list(linkage(program_interactions_raw.T))
|
|
875
|
+
|
|
876
|
+
cmap_range = np.percentile(np.abs(program_interactions[program_interactions != 0]), 95)
|
|
877
|
+
|
|
878
|
+
plt.imshow(program_interactions_raw.T[program_order, :][:, gene_order], cmap=cmap_hvec, vmin=-cmap_range, vmax=cmap_range, aspect='auto')
|
|
879
|
+
|
|
880
|
+
plt.xticks(np.arange(num_gene), np.array(gene_list)[gene_order], rotation=90, fontsize=9);
|
|
881
|
+
if program_name is not None:
|
|
882
|
+
plt.yticks(np.arange(num_program), np.array(program_name)[program_order], fontsize=9);
|
|
883
|
+
else:
|
|
884
|
+
plt.yticks(np.arange(num_program), [f'P{program + 1}' for program in program_order], fontsize=9)
|
|
885
|
+
|
|
886
|
+
plt.xlabel('Regulators')
|
|
887
|
+
plt.ylabel('Programs')
|
|
888
|
+
|
|
889
|
+
plt.colorbar(fraction=0.6 / num_program)
|
|
890
|
+
|
|
891
|
+
if fig_folder is not None:
|
|
892
|
+
plt.savefig(fig_folder + 'all_regulator_interactions.png')
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# -*-coding:utf-8 -*-
|
|
2
|
+
'''
|
|
3
|
+
@Time : 2026/07/30
|
|
4
|
+
@Author : Jialong Jiang
|
|
5
|
+
'''
|
|
6
|
+
|
|
7
|
+
import anndata as ad
|
|
8
|
+
import matplotlib.pyplot as plt
|
|
9
|
+
import numpy as np
|
|
10
|
+
import scanpy as sc
|
|
11
|
+
from scipy.optimize import curve_fit
|
|
12
|
+
from scipy.sparse import issparse
|
|
13
|
+
from sklearn.linear_model import LinearRegression
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def select_highly_variable_genes(
|
|
18
|
+
adata: ad.AnnData,
|
|
19
|
+
num_gene_select: int = 3000,
|
|
20
|
+
detect_frac: float = 0.02,
|
|
21
|
+
max_mean_percentile: float = 100,
|
|
22
|
+
fit_method: str = 'nb',
|
|
23
|
+
group_key: Optional[str] = None) -> np.ndarray:
|
|
24
|
+
"""
|
|
25
|
+
Select highly variable genes using a fitted mean-CV model.
|
|
26
|
+
|
|
27
|
+
The input matrix is expected to contain normalized, non-log-transformed
|
|
28
|
+
expression values. Linear fit, negative-binomial fit, and selected highly
|
|
29
|
+
variable gene plots are displayed.
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
adata : ad.AnnData
|
|
34
|
+
Annotated data matrix containing normalized gene expression.
|
|
35
|
+
num_gene_select : int, optional
|
|
36
|
+
Number of highly variable genes to select. Default is 3000.
|
|
37
|
+
detect_frac : float, optional
|
|
38
|
+
Minimum fraction of cells expressing a gene across all cells, or in at
|
|
39
|
+
least one group when ``group_key`` is provided. Default is 0.02.
|
|
40
|
+
max_mean_percentile : float, optional
|
|
41
|
+
Upper percentile of gene means retained for selection. Values below
|
|
42
|
+
100 remove highly expressed housekeeping genes. Default is 100.
|
|
43
|
+
fit_method : str, optional
|
|
44
|
+
Fitted baseline used to rank genes. Options are 'linear' or 'nb'.
|
|
45
|
+
Default is 'nb'.
|
|
46
|
+
group_key : str or None, optional
|
|
47
|
+
Column in ``adata.obs`` used to group cells for the detection filter.
|
|
48
|
+
If None, detection is calculated across all cells. Default is None.
|
|
49
|
+
|
|
50
|
+
Returns
|
|
51
|
+
-------
|
|
52
|
+
np.ndarray
|
|
53
|
+
Names of the selected highly variable genes.
|
|
54
|
+
"""
|
|
55
|
+
batch_size = 2000
|
|
56
|
+
sum_x = np.zeros(adata.n_vars)
|
|
57
|
+
sum_x_squared = np.zeros(adata.n_vars)
|
|
58
|
+
for start in range(0, adata.n_obs, batch_size):
|
|
59
|
+
block = adata.X[start:start + batch_size]
|
|
60
|
+
sum_x += np.asarray(block.sum(axis=0)).ravel()
|
|
61
|
+
sum_x_squared += np.asarray((block.power(2) if issparse(block) else np.square(block)).sum(axis=0)).ravel()
|
|
62
|
+
|
|
63
|
+
data_mean = sum_x / adata.n_obs
|
|
64
|
+
data_var = sum_x_squared / adata.n_obs - data_mean ** 2
|
|
65
|
+
valid = (data_mean > 0) & (data_var > 0)
|
|
66
|
+
|
|
67
|
+
log_mean = np.full(adata.n_vars, np.nan)
|
|
68
|
+
log_cv = np.full(adata.n_vars, np.nan)
|
|
69
|
+
log_mean[valid] = np.log10(data_mean[valid])
|
|
70
|
+
log_cv[valid] = np.log10(np.sqrt(data_var[valid]) / data_mean[valid])
|
|
71
|
+
|
|
72
|
+
def nb_model(x, k, b, pi):
|
|
73
|
+
return k * (x - np.log(1 + pi * np.exp(x))) + b
|
|
74
|
+
|
|
75
|
+
linear_model = LinearRegression().fit(
|
|
76
|
+
log_mean[valid, None], log_cv[valid])
|
|
77
|
+
linear_fit = np.full(adata.n_vars, np.nan)
|
|
78
|
+
linear_fit[valid] = linear_model.predict(log_mean[valid, None])
|
|
79
|
+
|
|
80
|
+
popt, _ = curve_fit(
|
|
81
|
+
nb_model, log_mean[valid], log_cv[valid], [-0.5, 1, 0],
|
|
82
|
+
method='dogbox')
|
|
83
|
+
nb_fit = np.full(adata.n_vars, np.nan)
|
|
84
|
+
nb_fit[valid] = nb_model(log_mean[valid], *popt)
|
|
85
|
+
|
|
86
|
+
if fit_method == 'linear':
|
|
87
|
+
fit_res = linear_fit
|
|
88
|
+
elif fit_method == 'nb':
|
|
89
|
+
fit_res = nb_fit
|
|
90
|
+
else:
|
|
91
|
+
raise ValueError("fit_method must be one of 'linear' or 'nb'")
|
|
92
|
+
|
|
93
|
+
if group_key is None:
|
|
94
|
+
detected = np.asarray((adata.X > 0).mean(axis=0)).ravel()
|
|
95
|
+
else:
|
|
96
|
+
detected = np.zeros(adata.n_vars)
|
|
97
|
+
groups = adata.obs[group_key].astype(str).to_numpy()
|
|
98
|
+
for group in np.unique(groups):
|
|
99
|
+
group_detect = np.asarray(
|
|
100
|
+
(adata.X[groups == group] > 0).mean(axis=0)).ravel()
|
|
101
|
+
detected = np.maximum(detected, group_detect)
|
|
102
|
+
|
|
103
|
+
eligible = (detected > detect_frac) & valid
|
|
104
|
+
if max_mean_percentile < 100:
|
|
105
|
+
mean_cutoff = np.percentile(data_mean[valid], max_mean_percentile)
|
|
106
|
+
eligible &= data_mean <= mean_cutoff
|
|
107
|
+
|
|
108
|
+
score = np.full(adata.n_vars, -np.inf)
|
|
109
|
+
score[valid] = log_cv[valid] - fit_res[valid]
|
|
110
|
+
cutoff = np.sort(score[eligible])[::-1][num_gene_select - 1]
|
|
111
|
+
gene_filt = (score >= cutoff) & eligible
|
|
112
|
+
|
|
113
|
+
sc.set_figure_params(figsize=[3, 3])
|
|
114
|
+
fig, grid = sc.pl._tools._panel_grid(0.1, 0.3, ncols=3, num_panels=3)
|
|
115
|
+
ax = [plt.subplot(grid[ii]) for ii in range(3)]
|
|
116
|
+
ax[0].scatter(log_mean[valid], log_cv[valid], 5)
|
|
117
|
+
ax[0].scatter(log_mean[valid], linear_fit[valid], 5)
|
|
118
|
+
ax[0].set(
|
|
119
|
+
title=f'linear fit (k={linear_model.coef_[0]:.3f})',
|
|
120
|
+
xlabel='log10(mean)', ylabel='log10(cv)')
|
|
121
|
+
|
|
122
|
+
ax[1].scatter(log_mean[valid], log_cv[valid], 5)
|
|
123
|
+
ax[1].scatter(log_mean[valid], nb_fit[valid], 5)
|
|
124
|
+
ax[1].set(
|
|
125
|
+
title=f'NB model (k={popt[0]:.3f})',
|
|
126
|
+
xlabel='log10(mean)', ylabel='log10(cv)')
|
|
127
|
+
|
|
128
|
+
ax[2].scatter(log_mean[valid], log_cv[valid], 1, alpha=0.5)
|
|
129
|
+
ax[2].scatter(
|
|
130
|
+
log_mean[gene_filt], log_cv[gene_filt], 5, alpha=0.5, label='HVG')
|
|
131
|
+
ax[2].legend()
|
|
132
|
+
ax[2].set(xlabel='log10(mean)', ylabel='log10(cv)')
|
|
133
|
+
plt.show()
|
|
134
|
+
|
|
135
|
+
return adata.var_names[gene_filt].to_numpy()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: dspin
|
|
3
|
-
Version: 1.5.
|
|
3
|
+
Version: 1.5.4
|
|
4
4
|
Summary: Regulatory network models from single-cell perturbation profiling
|
|
5
5
|
Home-page: https://github.com/JialongJiang/DSPIN
|
|
6
6
|
Author: Jialong Jiang
|
|
@@ -25,12 +25,24 @@ Requires-Dist: scanpy
|
|
|
25
25
|
Requires-Dist: tqdm
|
|
26
26
|
Requires-Dist: igraph
|
|
27
27
|
Requires-Dist: leidenalg
|
|
28
|
+
Dynamic: author
|
|
29
|
+
Dynamic: author-email
|
|
30
|
+
Dynamic: classifier
|
|
31
|
+
Dynamic: description
|
|
32
|
+
Dynamic: description-content-type
|
|
33
|
+
Dynamic: home-page
|
|
34
|
+
Dynamic: keywords
|
|
35
|
+
Dynamic: license
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
Dynamic: requires-dist
|
|
38
|
+
Dynamic: requires-python
|
|
39
|
+
Dynamic: summary
|
|
28
40
|
|
|
29
41
|
# D-SPIN: Constructing regulatory network models from scRNA-seq perturbation profiling
|
|
30
42
|
|
|
31
43
|
D-SPIN is a computational framework for constructing regulatory network models of genes or gene programs from single-cell RNA-seq profiling of perturbation conditions. D-SPIN integrates information from multiple perturbation conditions for network inference with improved efficienty and accuracy. The regulatory network can be defined on single genes or gene programs (groups of coexpressed genes). The package includes implementation of gene program discovery, network inference, and network analysis.
|
|
32
44
|
|
|
33
|
-
If D-SPIN is useful for your research, consider citing "D-SPIN constructs
|
|
45
|
+
If D-SPIN is useful for your research, consider citing "D-SPIN constructs regulatory network models from scRNA-seq that reveal organizing principles of perturbation response" ([Cell](https://doi.org/10.1016/j.cell.2026.04.028)).
|
|
34
46
|
|
|
35
47
|
## Contact
|
|
36
48
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
setup.py
|
|
4
|
+
dspin/__init__.py
|
|
5
|
+
dspin/annotate.py
|
|
6
|
+
dspin/compute.py
|
|
7
|
+
dspin/dspin.py
|
|
8
|
+
dspin/plot.py
|
|
9
|
+
dspin/preprocess.py
|
|
10
|
+
dspin.egg-info/PKG-INFO
|
|
11
|
+
dspin.egg-info/SOURCES.txt
|
|
12
|
+
dspin.egg-info/dependency_links.txt
|
|
13
|
+
dspin.egg-info/requires.txt
|
|
14
|
+
dspin.egg-info/top_level.txt
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -21,7 +21,7 @@ install_requires = [
|
|
|
21
21
|
# ]
|
|
22
22
|
|
|
23
23
|
# Managing your version (adjust accordingly)
|
|
24
|
-
version = "1.5.
|
|
24
|
+
version = "1.5.4" # You can manage versioning in a more sophisticated way if needed
|
|
25
25
|
|
|
26
26
|
# Long description (usually a README file)
|
|
27
27
|
readme = open("README_PyPI.md").read() # Adjust the file name if necessary
|
dspin-1.5.2/README.md
DELETED
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
# DSPIN
|
|
2
|
-
|
|
3
|
-
Tutorial, code and examples of the D-SPIN framework for the preprint "D-SPIN constructs gene regulatory network models from multiplexed scRNA-seq data revealing organizing principles of cellular perturbation response" ([bioRxiv](https://www.biorxiv.org/content/10.1101/2023.04.19.537364))
|
|
4
|
-
|
|
5
|
-
<div align="center">
|
|
6
|
-
<img src="figure/readme/Overview_20250523.png" width="800">
|
|
7
|
-
</div>
|
|
8
|
-
|
|
9
|
-
## Installation
|
|
10
|
-
|
|
11
|
-
D-SPIN is implemented in Python and MATLAB. The Python code is sufficient for the analysis of most datasets with less than 100k cells and a few hundreds of conditions. The MATLAB code is more efficient on large datasets as MATLAB can be easily deployed on clusters for parallelization with the build-in "parfor" function.
|
|
12
|
-
|
|
13
|
-
The python code can be installed with
|
|
14
|
-
|
|
15
|
-
`pip install dspin`
|
|
16
|
-
|
|
17
|
-
The MATLAB code can be downloaded from the folder "DSPIN_matlab", and directly executable in MATLAB after specifying the path to the data.
|
|
18
|
-
|
|
19
|
-
## Demos
|
|
20
|
-
|
|
21
|
-
A tutorial intended for an invited book chapter is available at [bioRxiv](https://www.biorxiv.org/content/10.1101/2025.07.28.667326).
|
|
22
|
-
|
|
23
|
-
Two demos of D-SPIN are available on Google Colab.
|
|
24
|
-
|
|
25
|
-
The first demo reconstructs the regulatory network of simulated hematopoietic stem cell (HSC) differentiation network with perturbations using the BEELINE framework (Pratapa, Aditya, et al. Nature methods, 2020).
|
|
26
|
-
|
|
27
|
-
[Demo1](https://colab.research.google.com/drive/1Nja8AAcP0L7Ag6DGbyn21SxGafsYX4fA?usp=sharing)
|
|
28
|
-
|
|
29
|
-
The second demo reconstructs regulatory network and response vector in a public single-cell dataset of immune dictionary (Cui, Ang, et al. Nature, 2024), where mouse were treated with different cytokines and lymph nodes were than collected and profiled.
|
|
30
|
-
|
|
31
|
-
[Demo2](https://colab.research.google.com/drive/1E8v4awQ-m4D02emG0jp6kmRDmkZ8hjjN?usp=sharing)
|
|
32
|
-
|
|
33
|
-
## Dependencies
|
|
34
|
-
|
|
35
|
-
DSPIN package was tested with the following python packages versions:
|
|
36
|
-
|
|
37
|
-
- python (3.9.18)
|
|
38
|
-
- anndata (0.10.3)
|
|
39
|
-
- matplotlib (3.8.2)
|
|
40
|
-
- scanpy (1.9.6)
|
|
41
|
-
- tqdm (4.65.0)
|
|
42
|
-
- leidenalg (0.10.1)
|
|
43
|
-
- igraph (0.10.8)
|
|
44
|
-
|
|
45
|
-
Note: other versions may work as well.
|
|
46
|
-
|
|
47
|
-
## Input data
|
|
48
|
-
|
|
49
|
-
D‑SPIN can work with many perturbation types **as long as they share the same core regulatory network**:
|
|
50
|
-
|
|
51
|
-
* **Genetic screens** – Perturb‑seq, CRISPR, RNAi
|
|
52
|
-
* **Chemical or signaling cues** – drug treatments, growth‑factor changes
|
|
53
|
-
* **Physiological differences** – healthy vs disease, different patients, time‑course samples
|
|
54
|
-
* **Spatial niches** – local micro‑environments from spatial transcriptomics
|
|
55
|
-
|
|
56
|
-
Because D‑SPIN models the *entire* distribution of transcriptional states, it expects **single‑cell RNA‑seq** data. Bulk data is possible but less informative.
|
|
57
|
-
|
|
58
|
-
#### AnnData requirements
|
|
59
|
-
|
|
60
|
-
* `adata.X` — log‑normalized (`log1p`) count matrix **after** QC: remove low‑quality cells, filter high mitochondrial content, keep highly variable genes.
|
|
61
|
-
* `adata.obs['sample_id']` — condition label; cells with the same value form one perturbation group (≥ 25 cells per group is ideal).
|
|
62
|
-
* `adata.obs['batch']` — batch identifier for cells processed together; D‑SPIN corrects batch effects by comparing conditions within each batch.
|
|
63
|
-
* `adata.obs['if_control']` — `True` for controls, `False` otherwise; perturbation effects are measured relative to these controls (or to the overall average if no controls exist).
|
|
64
|
-
|
|
65
|
-
## Building network models with D‑SPIN
|
|
66
|
-
|
|
67
|
-
#### Gene‑level network models
|
|
68
|
-
|
|
69
|
-
```python
|
|
70
|
-
from dspin.dspin import DSPIN
|
|
71
|
-
|
|
72
|
-
model = DSPIN(adata, save_path, num_spin=adata.shape[1])
|
|
73
|
-
model.network_inference(
|
|
74
|
-
sample_id_key='sample_id',
|
|
75
|
-
method='pseudo_likelihood',
|
|
76
|
-
directed=True,
|
|
77
|
-
run_with_matlab=False,
|
|
78
|
-
params={'stepsz': 0.05, 'lam_l1_j': 0.01}
|
|
79
|
-
)
|
|
80
|
-
model.response_relative_to_control(
|
|
81
|
-
sample_id_key='sample_id',
|
|
82
|
-
if_control_key='if_control',
|
|
83
|
-
batch_key='batch'
|
|
84
|
-
)
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
* **Results saved to `model`:**
|
|
88
|
-
* `model.network` – regulatory weights **J**
|
|
89
|
-
* `model.responses` – gene‑level responses **h**
|
|
90
|
-
* `model.relative_responses` – responses relative to control (per batch)
|
|
91
|
-
|
|
92
|
-
* **Key options**
|
|
93
|
-
* `num_spin=adata.shape[1]` → build a *gene‑level* network
|
|
94
|
-
* `directed=True` → only supported with `pseudo_likelihood`
|
|
95
|
-
* `run_with_matlab=True` → skip Python inference and write variables to `save_path` for the MATLAB version (speeds up very large data sets)
|
|
96
|
-
* `params` → hyper‑parameters such as step size, training epochs, and L1/L2 regularisation
|
|
97
|
-
|
|
98
|
-
---
|
|
99
|
-
|
|
100
|
-
#### Program‑level network models
|
|
101
|
-
|
|
102
|
-
```python
|
|
103
|
-
from dspin.dspin import DSPIN
|
|
104
|
-
|
|
105
|
-
model = DSPIN(adata, save_path, num_spin=15) # 15 gene programs
|
|
106
|
-
model.gene_program_discovery(
|
|
107
|
-
num_repeat=10,
|
|
108
|
-
seed=0,
|
|
109
|
-
cluster_key='cell_type' # balance cell types
|
|
110
|
-
)
|
|
111
|
-
model.network_inference(
|
|
112
|
-
sample_id_key='sample_id',
|
|
113
|
-
method='mcmc_maximum_likelihood'
|
|
114
|
-
)
|
|
115
|
-
model.response_relative_to_control()
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
* **Extra outputs**
|
|
119
|
-
* Gene‑program compositions in `save_path/onmf`
|
|
120
|
-
* Consensus program gene lists in `save_path`
|
|
121
|
-
|
|
122
|
-
* **Tips**
|
|
123
|
-
* Choose `num_spin` ≈ 5 × (number of cell types); keep ≤ 40
|
|
124
|
-
* `cluster_key` lets D‑SPIN down‑sample over‑represented cell types before oNMF
|
|
125
|
-
|
|
126
|
-
---
|
|
127
|
-
|
|
128
|
-
#### Finding gene regulators of programs
|
|
129
|
-
|
|
130
|
-
```python
|
|
131
|
-
# model_program : program‑level DSPIN object
|
|
132
|
-
# model_gene : gene‑level DSPIN object
|
|
133
|
-
model_gene.program_regulator_discovery(
|
|
134
|
-
model_program.program_representation,
|
|
135
|
-
sample_id_key='sample_id',
|
|
136
|
-
params={'stepsz': 0.02, 'lam_l1_interaction': 0.01}
|
|
137
|
-
)
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
* **Prerequisite** – `adata.obs` metadata must match between the two models so each cell’s gene and program states align.
|
|
141
|
-
|
|
142
|
-
* **Outputs in `model_gene`:**
|
|
143
|
-
* `model_gene.program_interactions` – regression coefficients **J** (gene ↔ program)
|
|
144
|
-
* `model_gene.program_activities` – global activity **h** for each program
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
## Application to the T cell population of the immune dictionary dataset
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
<div align="center">
|
|
151
|
-
<img src="figure/readme/Figure2_immune_dictionary.png" width="600">
|
|
152
|
-
</div>
|
|
153
|
-
|
|
154
|
-
<div align="left">
|
|
155
|
-
<em>Figure Overview and program discovery for the immune dictionary dataset. (A) t-distributed stochastic neighbour (t-SNE) embedding for a subsetted cell population from the immune dictionary dataset \cite{cui2024dictionary}. The subset dataset includes CD4, CD8, and regulatory T cells treated by 12 different cytokines, as well as corresponding control samples treated by phosphate-buffered saline (PBS).
|
|
156
|
-
(B) Heatmaps of gene expression and discretized gene program levels for control and IFN-$\alpha$1-treated samples. The gene programs are weighted averages of single-gene expressions that characterize and denoise the major expression pattern of the gene matrix.</em>
|
|
157
|
-
</div>
|
|
158
|
-
|
|
159
|
-
<div align="center">
|
|
160
|
-
<img src="figure/readme/Figure3_program_network.png" width="600">
|
|
161
|
-
</div>
|
|
162
|
-
|
|
163
|
-
<div align="left">
|
|
164
|
-
<em>Figure Program/gene-level regulatory network inferred by D-SPIN.
|
|
165
|
-
(A) Diagram of D-SPIN-inferred network model on gene programs. The network is partitioned into 4 modules, each associated with a T cell type in the population.
|
|
166
|
-
(B) Heatmap of the program response of each cytokine. Clustering of the response partitioned the cytokines into 3 major categories.
|
|
167
|
-
(C) Diagram of the core subnetwork of the D-SPIN-inferred gene network model. The node sizes scale with the number of identified interactions. The network is partitioned into 4 modules, each primarily composed of genes that have elevated expression in one specific T cell type.
|
|
168
|
-
(D) Interaction diagram for the subnetwork of IFN-$\alpha$1/IFN-$\beta$ and IFN-$\gamma$ acting on the program P18-IFN-$\gamma$ response. D-SPIN model shows that Type I and Type II interferons have different effectors to activate the program.</em>
|
|
169
|
-
</div>
|
|
170
|
-
|
|
171
|
-
## References
|
|
172
|
-
|
|
173
|
-
1. Jiang, Jialong, et al. "D-SPIN constructs gene regulatory network models from multiplexed scRNA-seq data revealing organizing principles of cellular perturbation response." bioRxiv (2023).
|
|
174
|
-
|
|
175
|
-
2. Jiang, Jialong, and Thomson, Matt. "A guide to D-SPIN: constructing regulatory network models from single-cell RNA-seq perturbation data." bioRxiv (2025).
|
|
176
|
-
|
|
177
|
-
3. Pratapa, Aditya, et al. "Benchmarking algorithms for gene regulatory network inference from single-cell transcriptomic data." Nature methods 17.2 (2020): 147-154.
|
|
178
|
-
|
|
179
|
-
4. Cui, Ang, et al. "Dictionary of immune responses to cytokines at single-cell resolution." Nature 625.7994 (2024): 377-384.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
LICENSE
|
|
2
|
-
README.md
|
|
3
|
-
requirements.txt
|
|
4
|
-
setup.py
|
|
5
|
-
__pycache__/__init__.cpython-310.pyc
|
|
6
|
-
__pycache__/__init__.cpython-37.pyc
|
|
7
|
-
__pycache__/compute.cpython-310.pyc
|
|
8
|
-
__pycache__/compute.cpython-37.pyc
|
|
9
|
-
__pycache__/dspin_abstract.cpython-310.pyc
|
|
10
|
-
__pycache__/plot.cpython-310.pyc
|
|
11
|
-
__pycache__/plotting.cpython-37.pyc
|
|
12
|
-
dspin/__init__.py
|
|
13
|
-
dspin/annotate.py
|
|
14
|
-
dspin/compute.py
|
|
15
|
-
dspin/dspin.py
|
|
16
|
-
dspin/plot.py
|
|
17
|
-
dspin.egg-info/PKG-INFO
|
|
18
|
-
dspin.egg-info/SOURCES.txt
|
|
19
|
-
dspin.egg-info/dependency_links.txt
|
|
20
|
-
dspin.egg-info/requires.txt
|
|
21
|
-
dspin.egg-info/top_level.txt
|