dspin 1.3.1__tar.gz → 1.3.3__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.3.1/dspin.egg-info → dspin-1.3.3}/PKG-INFO +1 -1
- dspin-1.3.3/README.md +175 -0
- {dspin-1.3.1 → dspin-1.3.3}/dspin/compute.py +180 -73
- {dspin-1.3.1 → dspin-1.3.3}/dspin/dspin.py +51 -35
- {dspin-1.3.1 → dspin-1.3.3/dspin.egg-info}/PKG-INFO +1 -1
- {dspin-1.3.1 → dspin-1.3.3}/setup.py +1 -1
- dspin-1.3.1/README.md +0 -134
- {dspin-1.3.1 → dspin-1.3.3}/LICENSE +0 -0
- {dspin-1.3.1 → dspin-1.3.3}/__pycache__/__init__.cpython-310.pyc +0 -0
- {dspin-1.3.1 → dspin-1.3.3}/__pycache__/__init__.cpython-37.pyc +0 -0
- {dspin-1.3.1 → dspin-1.3.3}/__pycache__/compute.cpython-310.pyc +0 -0
- {dspin-1.3.1 → dspin-1.3.3}/__pycache__/compute.cpython-37.pyc +0 -0
- {dspin-1.3.1 → dspin-1.3.3}/__pycache__/dspin_abstract.cpython-310.pyc +0 -0
- {dspin-1.3.1 → dspin-1.3.3}/__pycache__/plot.cpython-310.pyc +0 -0
- {dspin-1.3.1 → dspin-1.3.3}/__pycache__/plotting.cpython-37.pyc +0 -0
- {dspin-1.3.1 → dspin-1.3.3}/dspin/__init__.py +0 -0
- {dspin-1.3.1 → dspin-1.3.3}/dspin/annotate.py +0 -0
- {dspin-1.3.1 → dspin-1.3.3}/dspin/plot.py +0 -0
- {dspin-1.3.1 → dspin-1.3.3}/dspin.egg-info/SOURCES.txt +0 -0
- {dspin-1.3.1 → dspin-1.3.3}/dspin.egg-info/dependency_links.txt +0 -0
- {dspin-1.3.1 → dspin-1.3.3}/dspin.egg-info/requires.txt +0 -0
- {dspin-1.3.1 → dspin-1.3.3}/dspin.egg-info/top_level.txt +0 -0
- {dspin-1.3.1 → dspin-1.3.3}/requirements.txt +0 -0
- {dspin-1.3.1 → dspin-1.3.3}/setup.cfg +0 -0
dspin-1.3.3/README.md
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
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
|
+
Two demos of D-SPIN are available on Google Colab.
|
|
22
|
+
|
|
23
|
+
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).
|
|
24
|
+
|
|
25
|
+
[Demo1](https://colab.research.google.com/drive/1Nja8AAcP0L7Ag6DGbyn21SxGafsYX4fA?usp=sharing)
|
|
26
|
+
|
|
27
|
+
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.
|
|
28
|
+
|
|
29
|
+
[Demo2](https://colab.research.google.com/drive/1E8v4awQ-m4D02emG0jp6kmRDmkZ8hjjN?usp=sharing)
|
|
30
|
+
|
|
31
|
+
## Dependencies
|
|
32
|
+
|
|
33
|
+
DSPIN package was tested with the following python packages versions:
|
|
34
|
+
|
|
35
|
+
- python (3.9.18)
|
|
36
|
+
- anndata (0.10.3)
|
|
37
|
+
- matplotlib (3.8.2)
|
|
38
|
+
- scanpy (1.9.6)
|
|
39
|
+
- tqdm (4.65.0)
|
|
40
|
+
- leidenalg (0.10.1)
|
|
41
|
+
- igraph (0.10.8)
|
|
42
|
+
|
|
43
|
+
Note: other versions may work as well.
|
|
44
|
+
|
|
45
|
+
## Input data
|
|
46
|
+
|
|
47
|
+
D‑SPIN can work with many perturbation types **as long as they share the same core regulatory network**:
|
|
48
|
+
|
|
49
|
+
* **Genetic screens** – Perturb‑seq, CRISPR, RNAi
|
|
50
|
+
* **Chemical or signaling cues** – drug treatments, growth‑factor changes
|
|
51
|
+
* **Physiological differences** – healthy vs disease, different patients, time‑course samples
|
|
52
|
+
* **Spatial niches** – local micro‑environments from spatial transcriptomics
|
|
53
|
+
|
|
54
|
+
Because D‑SPIN models the *entire* distribution of transcriptional states, it expects **single‑cell RNA‑seq** data. Bulk data is possible but less informative.
|
|
55
|
+
|
|
56
|
+
#### AnnData requirements
|
|
57
|
+
|
|
58
|
+
* `adata.X` — log‑normalized (`log1p`) count matrix **after** QC: remove low‑quality cells, filter high mitochondrial content, keep highly variable genes.
|
|
59
|
+
* `adata.obs['sample_id']` — condition label; cells with the same value form one perturbation group (≥ 25 cells per group is ideal).
|
|
60
|
+
* `adata.obs['batch']` — batch identifier for cells processed together; D‑SPIN corrects batch effects by comparing conditions within each batch.
|
|
61
|
+
* `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).
|
|
62
|
+
|
|
63
|
+
## Building network models with D‑SPIN
|
|
64
|
+
|
|
65
|
+
#### Gene‑level network models
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from dspin.dspin import DSPIN
|
|
69
|
+
|
|
70
|
+
model = DSPIN(adata, save_path, num_spin=adata.shape[1])
|
|
71
|
+
model.network_inference(
|
|
72
|
+
sample_id_key='sample_id',
|
|
73
|
+
method='pseudo_likelihood',
|
|
74
|
+
directed=True,
|
|
75
|
+
run_with_matlab=False,
|
|
76
|
+
params={'stepsz': 0.05, 'lam_l1_j': 0.01}
|
|
77
|
+
)
|
|
78
|
+
model.response_relative_to_control(
|
|
79
|
+
sample_id_key='sample_id',
|
|
80
|
+
if_control_key='if_control',
|
|
81
|
+
batch_key='batch'
|
|
82
|
+
)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
* **Results saved to `model`:**
|
|
86
|
+
* `model.network` – regulatory weights **J**
|
|
87
|
+
* `model.responses` – gene‑level responses **h**
|
|
88
|
+
* `model.relative_responses` – responses relative to control (per batch)
|
|
89
|
+
|
|
90
|
+
* **Key options**
|
|
91
|
+
* `num_spin=adata.shape[1]` → build a *gene‑level* network
|
|
92
|
+
* `directed=True` → only supported with `pseudo_likelihood`
|
|
93
|
+
* `run_with_matlab=True` → skip Python inference and write variables to `save_path` for the MATLAB version (speeds up very large data sets)
|
|
94
|
+
* `params` → hyper‑parameters such as step size, training epochs, and L1/L2 regularisation
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
#### Program‑level network models
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from dspin.dspin import DSPIN
|
|
102
|
+
|
|
103
|
+
model = DSPIN(adata, save_path, num_spin=15) # 15 gene programs
|
|
104
|
+
model.gene_program_discovery(
|
|
105
|
+
num_repeat=10,
|
|
106
|
+
seed=0,
|
|
107
|
+
cluster_key='cell_type' # balance cell types
|
|
108
|
+
)
|
|
109
|
+
model.network_inference(
|
|
110
|
+
sample_id_key='sample_id',
|
|
111
|
+
method='mcmc_maximum_likelihood'
|
|
112
|
+
)
|
|
113
|
+
model.response_relative_to_control()
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
* **Extra outputs**
|
|
117
|
+
* Gene‑program compositions in `save_path/onmf`
|
|
118
|
+
* Consensus program gene lists in `save_path`
|
|
119
|
+
|
|
120
|
+
* **Tips**
|
|
121
|
+
* Choose `num_spin` ≈ 5 × (number of cell types); keep ≤ 40
|
|
122
|
+
* `cluster_key` lets D‑SPIN down‑sample over‑represented cell types before oNMF
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
#### Finding gene regulators of programs
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
# model_program : program‑level DSPIN object
|
|
130
|
+
# model_gene : gene‑level DSPIN object
|
|
131
|
+
model_gene.program_regulator_discovery(
|
|
132
|
+
model_program.program_representation,
|
|
133
|
+
sample_id_key='sample_id',
|
|
134
|
+
params={'stepsz': 0.02, 'lam_l1_interaction': 0.01}
|
|
135
|
+
)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
* **Prerequisite** – `adata.obs` metadata must match between the two models so each cell’s gene and program states align.
|
|
139
|
+
|
|
140
|
+
* **Outputs in `model_gene`:**
|
|
141
|
+
* `model_gene.program_interactions` – regression coefficients **J** (gene ↔ program)
|
|
142
|
+
* `model_gene.program_activities` – global activity **h** for each program
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
## Application to the T cell population of the immune dictionary dataset
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
<div align="center">
|
|
149
|
+
<img src="figure/readme/Figure2_immune_dictionary.png" width="600">
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<div align="left">
|
|
153
|
+
<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).
|
|
154
|
+
(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>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<div align="center">
|
|
158
|
+
<img src="figure/readme/Figure3_program_network.png" width="600">
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<div align="left">
|
|
162
|
+
<em>Figure Program/gene-level regulatory network inferred by D-SPIN.
|
|
163
|
+
(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.
|
|
164
|
+
(B) Heatmap of the program response of each cytokine. Clustering of the response partitioned the cytokines into 3 major categories.
|
|
165
|
+
(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.
|
|
166
|
+
(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>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
## References
|
|
170
|
+
|
|
171
|
+
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).
|
|
172
|
+
|
|
173
|
+
2. Pratapa, Aditya, et al. "Benchmarking algorithms for gene regulatory network inference from single-cell transcriptomic data." Nature methods 17.2 (2020): 147-154.
|
|
174
|
+
|
|
175
|
+
3. Cui, Ang, et al. "Dictionary of immune responses to cytokines at single-cell resolution." Nature 625.7994 (2024): 377-384.
|
|
@@ -20,6 +20,7 @@ from tqdm import tqdm
|
|
|
20
20
|
from typing import Tuple, List, Callable, Any, Dict
|
|
21
21
|
import warnings
|
|
22
22
|
from scipy.sparse import issparse
|
|
23
|
+
from joblib import Parallel, delayed, parallel_backend
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
def category_balance_number(
|
|
@@ -290,7 +291,9 @@ def compute_onmf(seed: int,
|
|
|
290
291
|
return nmf_model
|
|
291
292
|
|
|
292
293
|
|
|
293
|
-
def onmf_discretize(onmf_rep_ori: np.array,
|
|
294
|
+
def onmf_discretize(onmf_rep_ori: np.array,
|
|
295
|
+
num_init: int = 10,
|
|
296
|
+
fig_folder: str = None) -> np.array:
|
|
294
297
|
"""
|
|
295
298
|
Discretize the ONMF representation using KMeans clustering and visualize the sorted representations.
|
|
296
299
|
|
|
@@ -313,10 +316,11 @@ def onmf_discretize(onmf_rep_ori: np.array, fig_folder: str = None) -> np.array:
|
|
|
313
316
|
21, num_spin))
|
|
314
317
|
|
|
315
318
|
onmf_rep_tri = np.zeros(onmf_rep_ori.shape)
|
|
316
|
-
|
|
319
|
+
print('Processing gene or program expression levels...')
|
|
320
|
+
for ii in tqdm(range(num_spin)):
|
|
317
321
|
# Perform KMeans clustering for each spin/component
|
|
318
|
-
km_fit = KMeans(n_clusters=3, n_init=
|
|
319
|
-
|
|
322
|
+
km_fit = KMeans(n_clusters=3, n_init=num_init).fit(onmf_rep_ori[:, ii].reshape(-1, 1))
|
|
323
|
+
|
|
320
324
|
onmf_rep_tri[:, ii] = (km_fit.labels_ == np.argsort(km_fit.cluster_centers_.reshape(-1))[0]) * \
|
|
321
325
|
(-1) + (km_fit.labels_ == np.argsort(km_fit.cluster_centers_.reshape(-1))[2]) * 1
|
|
322
326
|
|
|
@@ -324,8 +328,7 @@ def onmf_discretize(onmf_rep_ori: np.array, fig_folder: str = None) -> np.array:
|
|
|
324
328
|
if ii < 21:
|
|
325
329
|
ax = plt.subplot(grid[ii])
|
|
326
330
|
plt.plot(np.sort(onmf_rep_ori[:, ii]))
|
|
327
|
-
plt.plot(np.sort(km_fit.cluster_centers_[
|
|
328
|
-
km_fit.labels_].reshape(-1)))
|
|
331
|
+
plt.plot(np.sort(km_fit.cluster_centers_[km_fit.labels_].reshape(-1)))
|
|
329
332
|
|
|
330
333
|
# Save the visual representation
|
|
331
334
|
if fig_folder is not None:
|
|
@@ -524,7 +527,7 @@ def sample_states(samp_full: np.array, onmf_rep_tri: np.array) -> Tuple[np.array
|
|
|
524
527
|
return state_list, samp_list
|
|
525
528
|
|
|
526
529
|
|
|
527
|
-
def
|
|
530
|
+
def para_moments_numpy(j_mat: np.array, h_vec: np.array) -> Tuple[np.array, np.array]:
|
|
528
531
|
"""
|
|
529
532
|
Calculate the correlation and mean parameters given a j matrix and an h vector.
|
|
530
533
|
|
|
@@ -563,51 +566,76 @@ def para_moments(j_mat: np.array, h_vec: np.array) -> Tuple[np.array, np.array]:
|
|
|
563
566
|
|
|
564
567
|
return corr_para, mean_para
|
|
565
568
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
axis: int,
|
|
570
|
-
arr: np.ndarray) -> np.ndarray:
|
|
569
|
+
@numba.njit(fastmath=True, cache=True)
|
|
570
|
+
def para_moments(j_mat: np.ndarray,
|
|
571
|
+
h_vec: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
|
|
571
572
|
"""
|
|
572
|
-
|
|
573
|
+
Calculate the correlation and mean parameters given an interaction
|
|
574
|
+
matrix and an external field vector, using a Numba‑accelerated kernel.
|
|
573
575
|
|
|
574
576
|
Parameters
|
|
575
577
|
----------
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
arr : np.ndarray
|
|
581
|
-
Input array.
|
|
578
|
+
j_mat : np.ndarray
|
|
579
|
+
Interaction matrix.
|
|
580
|
+
h_vec : np.ndarray
|
|
581
|
+
External field vector.
|
|
582
582
|
|
|
583
583
|
Returns
|
|
584
584
|
-------
|
|
585
|
-
np.ndarray
|
|
586
|
-
The
|
|
585
|
+
Tuple[np.ndarray, np.ndarray]
|
|
586
|
+
The correlation parameter array of shape (n, n).
|
|
587
|
+
The mean parameter array of shape (n,).
|
|
587
588
|
"""
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
589
|
+
num_spin: int = j_mat.shape[0]
|
|
590
|
+
|
|
591
|
+
# Copy so we can safely modify the diagonal in‑place
|
|
592
|
+
j_dbl = j_mat.copy()
|
|
593
|
+
for ii in range(num_spin):
|
|
594
|
+
j_dbl[ii, ii] += j_mat[ii, ii]
|
|
591
595
|
|
|
592
|
-
|
|
593
|
-
result[i] = func1d(arr[:, i]) if axis == 0 else func1d(arr[i, :])
|
|
596
|
+
num_sample: int = 3 ** num_spin
|
|
594
597
|
|
|
595
|
-
|
|
598
|
+
partition: float = 0.0
|
|
599
|
+
mean_para = np.zeros(num_spin, dtype=np.float64)
|
|
600
|
+
corr_para = np.zeros((num_spin, num_spin), dtype=np.float64)
|
|
596
601
|
|
|
602
|
+
spins = np.empty(num_spin, dtype=np.int8)
|
|
597
603
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
604
|
+
for idx in range(num_sample):
|
|
605
|
+
# Decode `idx` → base‑3 spin configuration in {‑1, 0, 1}
|
|
606
|
+
tmp = idx
|
|
607
|
+
for s in range(num_spin - 1, -1, -1):
|
|
608
|
+
spins[s] = (tmp % 3) - 1
|
|
609
|
+
tmp //= 3
|
|
602
610
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
611
|
+
# Energy: E = −h·s − ½ sᵀJs
|
|
612
|
+
energy: float = 0.0
|
|
613
|
+
for ii in range(num_spin):
|
|
614
|
+
energy -= h_vec[ii] * spins[ii]
|
|
615
|
+
|
|
616
|
+
js_term: float = 0.0
|
|
617
|
+
for ii in range(num_spin):
|
|
618
|
+
dot: float = 0.0
|
|
619
|
+
for jj in range(num_spin):
|
|
620
|
+
dot += j_dbl[ii, jj] * spins[jj]
|
|
621
|
+
js_term += spins[ii] * dot
|
|
622
|
+
energy -= 0.5 * js_term
|
|
623
|
+
|
|
624
|
+
weight: float = np.exp(-energy)
|
|
625
|
+
|
|
626
|
+
partition += weight
|
|
627
|
+
for ii in range(num_spin):
|
|
628
|
+
mean_para[ii] += spins[ii] * weight
|
|
629
|
+
for jj in range(num_spin):
|
|
630
|
+
corr_para[ii, jj] += spins[ii] * spins[jj] * weight
|
|
608
631
|
|
|
632
|
+
mean_para /= partition
|
|
633
|
+
corr_para /= partition
|
|
609
634
|
|
|
610
|
-
|
|
635
|
+
return corr_para, mean_para
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
@numba.njit(fastmath=True)
|
|
611
639
|
def pseudol_gradient(cur_j: np.ndarray,
|
|
612
640
|
cur_h: np.ndarray,
|
|
613
641
|
cur_state: np.ndarray,
|
|
@@ -632,37 +660,43 @@ def pseudol_gradient(cur_j: np.ndarray,
|
|
|
632
660
|
The gradient of j.
|
|
633
661
|
The gradient of h.
|
|
634
662
|
"""
|
|
635
|
-
num_spin =
|
|
663
|
+
num_spin, num_sample = cur_state.shape
|
|
664
|
+
sample_factor = 1.0 / num_sample
|
|
636
665
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
# Filtering diagonal elements of cur_j matrix.
|
|
641
|
-
j_filt = cur_j.copy()
|
|
642
|
-
np.fill_diagonal(j_filt, 0)
|
|
643
|
-
effective_h = j_filt.dot(cur_state) + cur_h
|
|
666
|
+
diag_j = np.empty(num_spin, dtype=cur_j.dtype)
|
|
667
|
+
for ii in range(num_spin):
|
|
668
|
+
diag_j[ii] = cur_j[ii, ii]
|
|
644
669
|
|
|
670
|
+
j_no_diag = cur_j.copy()
|
|
645
671
|
for ii in range(num_spin):
|
|
646
|
-
|
|
647
|
-
j_sub = cur_j[ii, ii]
|
|
648
|
-
h_sub = effective_h[ii, :]
|
|
672
|
+
j_no_diag[ii, ii] = 0.0
|
|
649
673
|
|
|
650
|
-
|
|
651
|
-
term2 = np.exp(j_sub - h_sub)
|
|
674
|
+
eff_h = j_no_diag @ cur_state + cur_h
|
|
652
675
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
h_eff_grad = cur_state[ii, :] - (term1 - term2) / (term1 + term2 + 1)
|
|
676
|
+
exp_plus = np.empty_like(eff_h)
|
|
677
|
+
exp_minus = np.empty_like(eff_h)
|
|
678
|
+
denom = np.empty_like(eff_h)
|
|
657
679
|
|
|
658
|
-
|
|
680
|
+
for ii in range(num_spin):
|
|
681
|
+
exp_plus[ii, :] = np.exp(diag_j[ii] + eff_h[ii, :])
|
|
682
|
+
exp_minus[ii, :] = np.exp(diag_j[ii] - eff_h[ii, :])
|
|
683
|
+
denom[ii, :] = exp_plus[ii, :] + exp_minus[ii, :] + 1.0
|
|
684
|
+
|
|
685
|
+
# Diagonal (self‑coupling) gradients
|
|
686
|
+
term_diag = cur_state * cur_state - (exp_plus + exp_minus) / denom
|
|
687
|
+
|
|
688
|
+
# Off‑diagonal and field gradients
|
|
689
|
+
h_eff_grad = cur_state - (exp_plus - exp_minus) / denom
|
|
690
|
+
cur_j_grad = (h_eff_grad @ cur_state.T) * sample_factor
|
|
691
|
+
for ii in range(num_spin):
|
|
692
|
+
cur_j_grad[ii, ii] = np.sum(term_diag[ii, :]) * sample_factor
|
|
659
693
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
cur_h_grad[ii] = np.
|
|
694
|
+
cur_h_grad = np.zeros((num_spin, 1), dtype=cur_j.dtype)
|
|
695
|
+
for ii in range(num_spin):
|
|
696
|
+
cur_h_grad[ii, 0] = np.sum(h_eff_grad[ii, :]) * sample_factor
|
|
663
697
|
|
|
664
|
-
|
|
665
|
-
|
|
698
|
+
if not directed:
|
|
699
|
+
cur_j_grad = 0.5 * (cur_j_grad + cur_j_grad.T)
|
|
666
700
|
|
|
667
701
|
return - cur_j_grad, - cur_h_grad
|
|
668
702
|
|
|
@@ -762,10 +796,10 @@ def samp_moments(j_mat: np.ndarray,
|
|
|
762
796
|
return corr_para, mean_para
|
|
763
797
|
|
|
764
798
|
|
|
765
|
-
def compute_gradient(cur_j: np.ndarray,
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
799
|
+
def compute_gradient(cur_j: np.ndarray,
|
|
800
|
+
cur_h: np.ndarray,
|
|
801
|
+
raw_data: List[Tuple[np.ndarray, np.ndarray]],
|
|
802
|
+
method: str, train_dat: Dict[str, Any]) -> Tuple[np.ndarray, np.ndarray]:
|
|
769
803
|
"""
|
|
770
804
|
Compute the gradient based on the specified method.
|
|
771
805
|
|
|
@@ -798,7 +832,7 @@ def compute_gradient(cur_j: np.ndarray,
|
|
|
798
832
|
# Computing gradients using the specified method.
|
|
799
833
|
if method == 'pseudo_likelihood':
|
|
800
834
|
j_grad, h_grad = pseudol_gradient(
|
|
801
|
-
cur_j, cur_h[:, kk
|
|
835
|
+
cur_j, cur_h[:, kk: kk + 1], raw_data[kk], directed=train_dat['directed'])
|
|
802
836
|
h_grad = h_grad.flatten()
|
|
803
837
|
else:
|
|
804
838
|
# Distinguishing between other methods and computing gradients
|
|
@@ -807,7 +841,7 @@ def compute_gradient(cur_j: np.ndarray,
|
|
|
807
841
|
corr_para, mean_para = para_moments(cur_j, cur_h[:, kk])
|
|
808
842
|
elif method == 'mcmc_maximum_likelihood':
|
|
809
843
|
corr_para, mean_para = samp_moments(
|
|
810
|
-
cur_j, cur_h, train_dat['mcmc_samplingsz'], train_dat['mcmc_samplingmix'], train_dat['mcmc_samplegap'])
|
|
844
|
+
cur_j, cur_h[:, kk], train_dat['mcmc_samplingsz'], train_dat['mcmc_samplingmix'], train_dat['mcmc_samplegap'])
|
|
811
845
|
j_grad = corr_para - raw_data[kk][0]
|
|
812
846
|
h_grad = mean_para - raw_data[kk][1].flatten()
|
|
813
847
|
|
|
@@ -817,6 +851,77 @@ def compute_gradient(cur_j: np.ndarray,
|
|
|
817
851
|
return rec_jgrad, rec_hgrad
|
|
818
852
|
|
|
819
853
|
|
|
854
|
+
def _gradient_one_round(kk: int,
|
|
855
|
+
cur_j: np.ndarray,
|
|
856
|
+
cur_h: np.ndarray,
|
|
857
|
+
raw_data_k,
|
|
858
|
+
method: str,
|
|
859
|
+
train_dat: Dict[str, Any]):
|
|
860
|
+
"""Helper that compute gradient for a single round."""
|
|
861
|
+
if method == 'pseudo_likelihood':
|
|
862
|
+
j_grad, h_grad = pseudol_gradient(cur_j, cur_h[:, kk:kk + 1], raw_data_k, directed=train_dat['directed'])
|
|
863
|
+
h_grad = h_grad.flatten()
|
|
864
|
+
else:
|
|
865
|
+
if method == 'maximum_likelihood':
|
|
866
|
+
corr_para, mean_para = para_moments(cur_j, cur_h[:, kk])
|
|
867
|
+
elif method == 'mcmc_maximum_likelihood':
|
|
868
|
+
corr_para, mean_para = samp_moments(
|
|
869
|
+
cur_j, cur_h[:, kk], train_dat['mcmc_samplingsz'], train_dat['mcmc_samplingmix'], train_dat['mcmc_samplegap'])
|
|
870
|
+
j_grad = corr_para - raw_data_k[0]
|
|
871
|
+
h_grad = mean_para - raw_data_k[1].flatten()
|
|
872
|
+
|
|
873
|
+
return kk, j_grad, h_grad
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
def compute_gradient_para(cur_j: np.ndarray,
|
|
877
|
+
cur_h: np.ndarray,
|
|
878
|
+
raw_data: List[Tuple[np.ndarray, np.ndarray]],
|
|
879
|
+
method: str, train_dat: Dict[str, Any]) -> Tuple[np.ndarray, np.ndarray]:
|
|
880
|
+
"""
|
|
881
|
+
Compute the gradient based on the specified method with parallel processing.
|
|
882
|
+
|
|
883
|
+
Parameters
|
|
884
|
+
----------
|
|
885
|
+
cur_j : np.ndarray
|
|
886
|
+
Current j matrix.
|
|
887
|
+
cur_h : np.ndarray
|
|
888
|
+
Current h matrix.
|
|
889
|
+
raw_data : List[Tuple[np.ndarray, np.ndarray]]
|
|
890
|
+
Raw data used to calculate the gradient (each element is a tuple of correlation and mean).
|
|
891
|
+
method : str
|
|
892
|
+
The method to calculate the gradient; possible values are 'pseudo_likelihood',
|
|
893
|
+
'maximum_likelihood', and 'mcmc_maximum_likelihood'.
|
|
894
|
+
train_dat : Dict[str, Any]
|
|
895
|
+
Training data and hyperparameters.
|
|
896
|
+
|
|
897
|
+
Returns
|
|
898
|
+
-------
|
|
899
|
+
Tuple[np.array, np.array]
|
|
900
|
+
The gradient of j.
|
|
901
|
+
The gradient of h.
|
|
902
|
+
"""
|
|
903
|
+
num_spin, num_round = cur_h.shape
|
|
904
|
+
rec_jgrad = np.zeros((num_spin, num_spin, num_round))
|
|
905
|
+
rec_hgrad = np.zeros((num_spin, num_round))
|
|
906
|
+
|
|
907
|
+
n_jobs = train_dat.get('max_workers', min(num_round, os.cpu_count()))
|
|
908
|
+
chunk = train_dat.get('chunk_size', 1) # rounds per task
|
|
909
|
+
backend = 'loky' # process‑based
|
|
910
|
+
|
|
911
|
+
# print(f'Processing with {n_jobs} workers...')
|
|
912
|
+
with parallel_backend(backend, n_jobs=n_jobs):
|
|
913
|
+
results = Parallel(batch_size=chunk)(
|
|
914
|
+
delayed(_gradient_one_round)(kk, cur_j, cur_h, raw_data[kk], method, train_dat)
|
|
915
|
+
for kk in range(num_round)
|
|
916
|
+
)
|
|
917
|
+
|
|
918
|
+
for kk, j_grad, h_grad in results:
|
|
919
|
+
rec_jgrad[:, :, kk] = j_grad
|
|
920
|
+
rec_hgrad[:, kk] = h_grad
|
|
921
|
+
|
|
922
|
+
return rec_jgrad, rec_hgrad
|
|
923
|
+
|
|
924
|
+
|
|
820
925
|
def apply_regularization(rec_jgrad: np.ndarray,
|
|
821
926
|
rec_hgrad: np.ndarray,
|
|
822
927
|
cur_j: np.ndarray,
|
|
@@ -999,6 +1104,8 @@ def learn_network_adam(raw_data: Any,
|
|
|
999
1104
|
|
|
1000
1105
|
backtrack_counter = 0
|
|
1001
1106
|
counter = 1
|
|
1107
|
+
|
|
1108
|
+
tqdm._instances.clear()
|
|
1002
1109
|
pbar = tqdm(total=num_epoch)
|
|
1003
1110
|
|
|
1004
1111
|
while counter <= num_epoch:
|
|
@@ -1029,13 +1136,13 @@ def learn_network_adam(raw_data: Any,
|
|
|
1029
1136
|
|
|
1030
1137
|
# Save training log and print progress
|
|
1031
1138
|
if counter in list_step:
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1139
|
+
if train_dat['save_log']:
|
|
1140
|
+
sio.savemat(save_path + 'train_log.mat',
|
|
1141
|
+
{'list_step': list_step,
|
|
1142
|
+
'rec_hvec_all': rec_hvec_all,
|
|
1143
|
+
'rec_jmat_all': rec_jmat_all,
|
|
1144
|
+
'rec_jgrad_sum_norm': rec_jgrad_sum_norm})
|
|
1145
|
+
|
|
1039
1146
|
pbar.update(rec_gap)
|
|
1040
1147
|
pbar.set_postfix({"Network Gradient": f"{rec_jgrad_sum_norm[counter - 1]:.6f}"})
|
|
1041
1148
|
|
|
@@ -34,6 +34,7 @@ import matplotlib.patheffects as patheffects
|
|
|
34
34
|
import warnings
|
|
35
35
|
import itertools
|
|
36
36
|
from typing import List, Dict, Optional, Tuple, Union
|
|
37
|
+
from scipy.stats import skew
|
|
37
38
|
|
|
38
39
|
|
|
39
40
|
class AbstractDSPIN(ABC):
|
|
@@ -130,7 +131,9 @@ class AbstractDSPIN(ABC):
|
|
|
130
131
|
def sample_list(self, value):
|
|
131
132
|
self._samp_list = value
|
|
132
133
|
|
|
133
|
-
def discretize(self,
|
|
134
|
+
def discretize(self,
|
|
135
|
+
clip_percentile: float = 100,
|
|
136
|
+
num_init: int = 10) -> None:
|
|
134
137
|
"""
|
|
135
138
|
Discretizes the oNMF representation into three states (-1, 0, 1) using K-means clustering.
|
|
136
139
|
|
|
@@ -138,6 +141,8 @@ class AbstractDSPIN(ABC):
|
|
|
138
141
|
----------
|
|
139
142
|
clip_percentile : float, optional
|
|
140
143
|
Percentile at which to clip values before discretization (default is 100).
|
|
144
|
+
num_init : int, optional
|
|
145
|
+
Number of initializations for K-means clustering (default is 10).
|
|
141
146
|
"""
|
|
142
147
|
onmf_rep_ori = self.program_representation_raw.copy()
|
|
143
148
|
fig_folder = self.fig_folder
|
|
@@ -147,7 +152,7 @@ class AbstractDSPIN(ABC):
|
|
|
147
152
|
onmf_rep_ori[:, ii] = onmf_rep_ori[:, ii] / np.percentile(onmf_rep_ori[:, ii], clip_percentile)
|
|
148
153
|
onmf_rep_ori = onmf_rep_ori.clip(0, 1)
|
|
149
154
|
|
|
150
|
-
onmf_rep_tri = onmf_discretize(onmf_rep_ori, fig_folder)
|
|
155
|
+
onmf_rep_tri = onmf_discretize(onmf_rep_ori, num_init, fig_folder)
|
|
151
156
|
self._onmf_rep_tri = onmf_rep_tri
|
|
152
157
|
|
|
153
158
|
def raw_data_corr(self, sample_id_key: str) -> None:
|
|
@@ -209,7 +214,8 @@ class AbstractDSPIN(ABC):
|
|
|
209
214
|
'cur_h': np.zeros((num_spin, num_sample)),
|
|
210
215
|
'save_path': self.save_path,
|
|
211
216
|
'rec_gap': 10,
|
|
212
|
-
'seed': 0
|
|
217
|
+
'seed': 0,
|
|
218
|
+
'save_log': False}
|
|
213
219
|
params.update({'lambda_l1_j': 0.01,
|
|
214
220
|
'lambda_l1_h': 0,
|
|
215
221
|
'lambda_l2_j': 0,
|
|
@@ -332,8 +338,8 @@ class AbstractDSPIN(ABC):
|
|
|
332
338
|
raise ValueError(f"Column '{batch_key}' not found in adata.obs.")
|
|
333
339
|
|
|
334
340
|
sample_list = self._samp_list
|
|
335
|
-
sample_to_control = self.adata.obs.groupby(sample_id_key)[if_control_key].first().to_dict()
|
|
336
|
-
sample_to_batch = self.adata.obs.groupby(sample_id_key)[batch_key].first().to_dict()
|
|
341
|
+
sample_to_control = self.adata.obs.groupby(sample_id_key, observed=False)[if_control_key].first().to_dict()
|
|
342
|
+
sample_to_batch = self.adata.obs.groupby(sample_id_key, observed=False)[batch_key].first().to_dict()
|
|
337
343
|
|
|
338
344
|
if_control = np.array([sample_to_control[sample] for sample in sample_list])
|
|
339
345
|
batch_index = np.array([sample_to_batch[sample] for sample in sample_list])
|
|
@@ -351,7 +357,7 @@ class GeneDSPIN(AbstractDSPIN):
|
|
|
351
357
|
adata: ad.AnnData,
|
|
352
358
|
save_path: str,
|
|
353
359
|
num_spin: int,
|
|
354
|
-
|
|
360
|
+
discretize_params: dict = {}):
|
|
355
361
|
"""
|
|
356
362
|
Initialize the GeneDSPIN object.
|
|
357
363
|
|
|
@@ -363,6 +369,8 @@ class GeneDSPIN(AbstractDSPIN):
|
|
|
363
369
|
Directory path where results will be saved.
|
|
364
370
|
num_spin : int
|
|
365
371
|
Number of spins used in inference.
|
|
372
|
+
use_default_discretize : bool, optional
|
|
373
|
+
Whether to use default discretization. Default is True. Otherwise the discretized data should be explicited setted in program_representation.
|
|
366
374
|
clip_percentile : float, optional
|
|
367
375
|
Percentile threshold for clipping values before discretization. Default is 95.
|
|
368
376
|
"""
|
|
@@ -375,7 +383,26 @@ class GeneDSPIN(AbstractDSPIN):
|
|
|
375
383
|
else:
|
|
376
384
|
self._onmf_rep_ori = adata.X
|
|
377
385
|
|
|
378
|
-
|
|
386
|
+
gene_skew = np.zeros(num_spin)
|
|
387
|
+
|
|
388
|
+
for ii in range(num_spin):
|
|
389
|
+
cur_gene = self._onmf_rep_ori[:, ii]
|
|
390
|
+
gene_skew[ii] = skew(cur_gene, bias=False)
|
|
391
|
+
|
|
392
|
+
if gene_skew.mean() > 2:
|
|
393
|
+
default_clip = 95
|
|
394
|
+
print("Input gene expression seem to be heavy-tailed. Using 95 percentile for clipping before discretization.")
|
|
395
|
+
else:
|
|
396
|
+
default_clip = 100
|
|
397
|
+
|
|
398
|
+
dparams = {'use_default_discretize': True,
|
|
399
|
+
'clip_percentile': default_clip,
|
|
400
|
+
'num_init': 10}
|
|
401
|
+
dparams.update(discretize_params)
|
|
402
|
+
|
|
403
|
+
if dparams['use_default_discretize']:
|
|
404
|
+
self.discretize(clip_percentile=dparams['clip_percentile'],
|
|
405
|
+
num_init=dparams['num_init'])
|
|
379
406
|
|
|
380
407
|
def program_regulator_discovery(self,
|
|
381
408
|
program_representation_raw: np.ndarray,
|
|
@@ -440,10 +467,7 @@ class ProgramDSPIN(AbstractDSPIN):
|
|
|
440
467
|
def __init__(self,
|
|
441
468
|
adata: ad.AnnData,
|
|
442
469
|
save_path: str,
|
|
443
|
-
num_spin: int
|
|
444
|
-
num_onmf_components: int = None,
|
|
445
|
-
prior_programs: List[List[str]] = None,
|
|
446
|
-
num_repeat: int = 10):
|
|
470
|
+
num_spin: int):
|
|
447
471
|
"""
|
|
448
472
|
Initialize the ProgramDSPIN object.
|
|
449
473
|
|
|
@@ -455,12 +479,6 @@ class ProgramDSPIN(AbstractDSPIN):
|
|
|
455
479
|
Directory path for saving results.
|
|
456
480
|
num_spin : int
|
|
457
481
|
Number of spins used in DSPIN.
|
|
458
|
-
num_onmf_components : int, optional
|
|
459
|
-
Number of oNMF components, default is None.
|
|
460
|
-
prior_programs : List[List[str]], optional
|
|
461
|
-
List of predefined gene programs.
|
|
462
|
-
num_repeat : int, optional
|
|
463
|
-
Number of repetitions for oNMF computation. Default is 10.
|
|
464
482
|
"""
|
|
465
483
|
super().__init__(adata, save_path, num_spin)
|
|
466
484
|
|
|
@@ -470,8 +488,6 @@ class ProgramDSPIN(AbstractDSPIN):
|
|
|
470
488
|
self._gene_matrix_large = None
|
|
471
489
|
self._use_data_list = None
|
|
472
490
|
self.gene_program_csv = None
|
|
473
|
-
self.prior_programs = None
|
|
474
|
-
self.preprogram_num = len(prior_programs) if prior_programs else 0
|
|
475
491
|
|
|
476
492
|
@property
|
|
477
493
|
def onmf_decomposition(self):
|
|
@@ -546,32 +562,27 @@ class ProgramDSPIN(AbstractDSPIN):
|
|
|
546
562
|
cluster_key: str = 'leiden',
|
|
547
563
|
mode: str = 'compute_summary',
|
|
548
564
|
prior_programs: List[List[str]] = None,
|
|
549
|
-
params: dict = {}
|
|
565
|
+
params: dict = {},
|
|
566
|
+
discretize_params: dict = {}) -> None:
|
|
550
567
|
"""
|
|
551
568
|
Discover gene programs by performing oNMF decomposition on the annotated data.
|
|
552
569
|
|
|
553
570
|
Parameters
|
|
554
571
|
----------
|
|
555
|
-
num_onmf_components : int, optional
|
|
556
|
-
Number of oNMF components. Default is None.
|
|
557
|
-
num_subsample : int, optional
|
|
558
|
-
Number of samples to use after subsampling. Default is 10000.
|
|
559
|
-
num_subsample_large : int, optional
|
|
560
|
-
Number of samples for large subsampling. Default is None.
|
|
561
572
|
num_repeat : int, optional
|
|
562
573
|
Number of times to repeat oNMF. Default is 10.
|
|
563
574
|
seed : int, optional
|
|
564
575
|
Random seed for reproducibility. Default is 0.
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
max_sample_rate : float, optional
|
|
570
|
-
Maximum sampling rate. Default is 2.
|
|
576
|
+
cluster_key : str, optional
|
|
577
|
+
Key in `adata.obs` for clustering labels. Default is 'leiden'.
|
|
578
|
+
mode: str, optional
|
|
579
|
+
Mode of operation. Options: 'compute_summary', 'compute_only', 'summary_only'. Default is 'compute_summary'.
|
|
571
580
|
prior_programs : List[List[str]], optional
|
|
572
581
|
List of predefined gene programs. Default is None.
|
|
573
|
-
|
|
574
|
-
|
|
582
|
+
params : dict, optional
|
|
583
|
+
Additional parameters for oNMF. Default is an empty dictionary.
|
|
584
|
+
discretize_params : dict, optional
|
|
585
|
+
Parameters for discretization of the oNMF representation. Default is an empty dictionary.
|
|
575
586
|
"""
|
|
576
587
|
|
|
577
588
|
if mode not in ['compute_summary', 'compute_only', 'summary_only']:
|
|
@@ -662,7 +673,12 @@ class ProgramDSPIN(AbstractDSPIN):
|
|
|
662
673
|
self._onmf_rep_ori = onmf_summary.transform(
|
|
663
674
|
gene_matrix / self.matrix_std)
|
|
664
675
|
|
|
665
|
-
|
|
676
|
+
dparams = {'clip_percentile': 100,
|
|
677
|
+
'num_init': 10}
|
|
678
|
+
dparams.update(discretize_params)
|
|
679
|
+
|
|
680
|
+
self.discretize(clip_percentile=dparams['clip_percentile'],
|
|
681
|
+
num_init=dparams['num_init'])
|
|
666
682
|
|
|
667
683
|
|
|
668
684
|
class DSPIN(object):
|
|
@@ -21,7 +21,7 @@ install_requires = [
|
|
|
21
21
|
# ]
|
|
22
22
|
|
|
23
23
|
# Managing your version (adjust accordingly)
|
|
24
|
-
version = "1.3.
|
|
24
|
+
version = "1.3.3" # 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.3.1/README.md
DELETED
|
@@ -1,134 +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
|
-

|
|
6
|
-
|
|
7
|
-
## Installation
|
|
8
|
-
|
|
9
|
-
D-SPIN is implemented in Python and Matlab. The Python code is sufficient for analysis of small datasets with around 20k cells and tens of conditions. The Matlab code is only used for network inference on large datasets as Matlab can be easily deployed on clusters for parallelization with the build-in "parfor" function.
|
|
10
|
-
|
|
11
|
-
The python code can be installed with
|
|
12
|
-
|
|
13
|
-
`pip install dspin`
|
|
14
|
-
|
|
15
|
-
The Matlab code can be downloaded from the folder "DSPIN_matlab", and directly executable in Matlab after specifying the path to the data.
|
|
16
|
-
|
|
17
|
-
## Demos
|
|
18
|
-
|
|
19
|
-
Two demos of D-SPIN are available on Google Colab.
|
|
20
|
-
|
|
21
|
-
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).
|
|
22
|
-
|
|
23
|
-
[Demo1](https://colab.research.google.com/drive/1mjJ09_E9kjEYsGCzf10-aZfuOqHUEV5u?usp=sharing)
|
|
24
|
-
|
|
25
|
-
The second demo reconstructs regulatory network and response vector in a single-cell dataset collected in the ThomsonLab.In the dataset, human peripheral blood mononuclear cells (PBMCs) were treated with various signaling molecules with different dosages.
|
|
26
|
-
|
|
27
|
-
[Demo2](https://colab.research.google.com/drive/1U2WbmKvai3Y_-vlLV0SY1dv1LHdFjg2w?usp=sharing)
|
|
28
|
-
|
|
29
|
-
## Dependencies
|
|
30
|
-
|
|
31
|
-
DSPIN package was tested with the following python packages versions:
|
|
32
|
-
|
|
33
|
-
- python (3.9.18)
|
|
34
|
-
- anndata (0.10.3)
|
|
35
|
-
- matplotlib (3.8.2)
|
|
36
|
-
- scanpy (1.9.6)
|
|
37
|
-
- tqdm (4.65.0)
|
|
38
|
-
- leidenalg (0.10.1)
|
|
39
|
-
- igraph (0.10.8)
|
|
40
|
-
|
|
41
|
-
Note: other versions may work as well.
|
|
42
|
-
|
|
43
|
-
## D-SPIN Overview
|
|
44
|
-
|
|
45
|
-
D-SPIN contains three main steps:
|
|
46
|
-
* Gene program discovery
|
|
47
|
-
* Network inference
|
|
48
|
-
* Network analysis
|
|
49
|
-
|
|
50
|
-
The input data should be AnnData object after typical single-cell preprocessing steps including cell counts normalization, log transformation, highly-varible gene selection, and clustering (optional). The input data should contain the following information. The attribute name is subject to the user while the following are defaults of the code:
|
|
51
|
-
* Normalized, log-transformed and filtered gene expression matrix in adata.X
|
|
52
|
-
* Sample information for each cell in adata.obs['sample_id']
|
|
53
|
-
* Batch information for each cell in adata.obs['batch']
|
|
54
|
-
* Clustering information for each cell in adata.obs['leiden']
|
|
55
|
-
* If some samples are control conditions, the relative response vectors will be computed for each batch with respect to the average of control conditions. The control conditions can be indicated in adata.obs['if_control'] with 1 for control and 0 for non-control. Note that all cells in the same sample and batch should have the same value in adata.obs['if_control'].
|
|
56
|
-
|
|
57
|
-
The D-SPIN is initiallized by model = dspin.DSPIN(adata) with the following major arguments:
|
|
58
|
-
|
|
59
|
-
* adata: AnnData object with the above information
|
|
60
|
-
* save_path: path to save the results
|
|
61
|
-
* num_spin (default 15): number of gene programs in the model
|
|
62
|
-
* filter_threshold (default 0.02): threshold for minimal proportion of cells that each gene program is expressed in
|
|
63
|
-
|
|
64
|
-
### Gene program discovery
|
|
65
|
-
|
|
66
|
-
By default, D-SPIN use orgthogonal non-negative matrix factorization (oNMF) to discover gene programs that coexpress in the data. The user can also specify pre-defined gene programs in full or partial. There are three senarios depending on the num_spin and the input parameter prior_programs.
|
|
67
|
-
* No pre-defined gene programs: empty prior_programs (default)
|
|
68
|
-
* Full pre-defined gene programs: the number of gene programs in prior_programs is equal to num_spin
|
|
69
|
-
* Partial pre-defined gene programs: the number of gene programs in prior_programs is less than num_spin
|
|
70
|
-
|
|
71
|
-
The gene program discovery contains two stages, the first stage subset and normalize the gene matrix, and the second stage run oNMF multiple times on the subset matrix to obtain consensus gene programs. Subsetting the gene matrix is primarily for computational efficiency, and preferrentially sampling cells with smaller population also helps to avoid overfitting on over-represented cell states in the dataset. As the algorithm for oNMF is stochastic, the algorithm is run multiples times and results from multiple run are combined to obtain a set of consensus gene program. Also, the number of oNMF components can be specified by the user to discover gene programs with finer resolution. For example, run the algorithm with 20 components in each individual run but combine the 20 programs into 15 consensus programs.
|
|
72
|
-
|
|
73
|
-
Overall, in the gene program discovery function model.gene_program_discovery() , D-SPIN takes the following major arguments:
|
|
74
|
-
|
|
75
|
-
* num_onmf_components (int, optional): The number of oNMF components. Default is infer based on num_spin and pre-assigned programs.
|
|
76
|
-
* num_subsample (int, optional): Number of cells to use in each individual oNMF components computation. Default is 10000.
|
|
77
|
-
* num_subsample_large (int, optional): Number of cells to use in computing consensus oNMF components. Default is 5 times of num_subsample.
|
|
78
|
-
* num_repeat (int, optional): Number of times to repeat the oNMF computation. Default is 10.
|
|
79
|
-
* balance_obs (str, optional): Cell type or clustering label in adata.obs for balanced sampling. Default is None.
|
|
80
|
-
* balance_method (str, optional): Method used for balancing. Options are 'squareroot', 'equal', 'proportional'. Default is 'squareroot'.
|
|
81
|
-
* max_sample_rate (float, optional): Maximum oversampling rate for underrepresented cell type or clustering categories. Default is 2.
|
|
82
|
-
* prior_programs (List[List[str]], optional): List of pre-assigned gene programs. Default is None.
|
|
83
|
-
* summary_method (str, optional): Method used for computing sensus oNMF components. Options are 'kmeans' and 'leiden'. Default is 'kmeans'.
|
|
84
|
-
|
|
85
|
-
### Network inference
|
|
86
|
-
|
|
87
|
-
In the network inference step, D-SPIN automatically choose between three inference methods depending on the number of gene programs:
|
|
88
|
-
|
|
89
|
-
* Exact maximum-likelihood inference (MLE)
|
|
90
|
-
* Markov-Chain Monte-Carlo (MCMC) maximum-likelihood inference
|
|
91
|
-
* Pseudo-likelihood inference (PL).
|
|
92
|
-
|
|
93
|
-
MLE is exactly solution of the problem but only applies to small network. MCMC is slightly more scalable to networks with 20-40 nodes. PL takes more simplified approximation to the problem which scales to hundreds of nodes, but requires larger cell number for the approximation to be effective. in the gene program discovery function model.network_inference(), D-SPIN takes the following major arguments
|
|
94
|
-
|
|
95
|
-
* sample_col_name (str, optional): Column name in adata.obs for sample information. Default is 'sample_id'
|
|
96
|
-
* control_col_name (str, optional): Column name in adata.obs for whether the cell is in a control sample. If not provided, all cells are treated as non-control and relative response vector are the same as response vector.
|
|
97
|
-
* batch_col_name (str, optional): Column name in adata.obs for batch information. Relative response are computed by the control response vector in the same batch. If not provided, all cells are treated as one batch.
|
|
98
|
-
* method (str, optional): Method used for network inference. Options are 'auto', 'maximum_likelihood', 'mcmc_maximum_likelihood', 'pseudo_likelihood'.
|
|
99
|
-
* example_list (List[str], optional): List of samples to use for network inference. Default is using all samples available in adata.obs[sample_col_name].
|
|
100
|
-
* record_step (int, optional): Number of steps to record training progress. Default is 10.
|
|
101
|
-
* params (Dict[str, Any], optional): Hypterparameters for the gradient descent of network inference. Default is None.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
### Network analysis
|
|
105
|
-
|
|
106
|
-
After network inference, D-SPIN provides three outputs:
|
|
107
|
-
|
|
108
|
-
* Inferred network adjacenty matrix in model.network
|
|
109
|
-
* Inferred pertrubation response in model.response
|
|
110
|
-
* Relative perturbation response to control in model.relative_response
|
|
111
|
-
|
|
112
|
-
Typical downstream analysis includes network visualization, module identification, and graph clustering of perturbation response with the network.
|
|
113
|
-
|
|
114
|
-
## Application to signaling response data of human PBMCs
|
|
115
|
-
|
|
116
|
-
D-SPIN takes single-cell sequencing data of multiple perturbation conditions. In the second demo, PBMCs are treated with different signaling molecules such as CD3 antibody, LPS, IL1B, and TGFB1
|
|
117
|
-
|
|
118
|
-

|
|
119
|
-
|
|
120
|
-
D-SPIN identifies a set of gene programs that coexpress in the data, and represent each cell as a combination of gene program expression states.
|
|
121
|
-
|
|
122
|
-

|
|
123
|
-
|
|
124
|
-
D-SPIN uses cross-correlation and mean of each perturbation condition to inferred a unified regulatory network and the response vector of each perturbation condition. The inference can be parallelized across perturbation conditions. The inference code is in Matlab using "parfor", while for demo purpose Python code (without parallelization) is provided.
|
|
125
|
-
|
|
126
|
-
The inferred regulatory network and perturbations can be jointly analyzed to reveal how perturbations act in the context of the regulatory network.
|
|
127
|
-
|
|
128
|
-

|
|
129
|
-
|
|
130
|
-
## References
|
|
131
|
-
|
|
132
|
-
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).
|
|
133
|
-
|
|
134
|
-
2. Pratapa, Aditya, et al. "Benchmarking algorithms for gene regulatory network inference from single-cell transcriptomic data." Nature methods 17.2 (2020): 147-154.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|