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.
File without changes
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: dspin
3
- Version: 1.5.2
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 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)).
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-&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.
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-&alpha;1/IFN-&beta; and IFN-&gamma; acting on the program P17-IFN-&gamma; 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 ["lambda_l1_j", "lambda_l2_j"])
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 (rec_grad[counter - 1] > 1.5 * rec_grad[counter - 1 - backtrack_gap]):
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 = rec_grad[counter - 1 - backtrack_gap: counter - 1].mean()
1530
- est_prev_grad = rec_grad[counter - 1 - 2 * backtrack_gap: counter - 1 - backtrack_gap].mean()
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
- onmf_rep_ori[:, ii] = onmf_rep_ori[:, ii] / np.percentile(onmf_rep_ori[:, ii], clip_percentile)
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
- perturb_matrix: np.ndarray = None,
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
- perturb_matrix : np.ndarray, optional
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 perturb_matrix is not None:
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'] = 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.01,
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
- sc.set_figure_params(figsize=[4, 4])
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
- for ii in range(num_spin):
145
- plt.text(program_umap_pos[ii, 0], program_umap_pos[ii, 1], str(ii), fontsize=10)
146
- plt.axis('off')
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 subsample:
169
- num_subsample = 20000
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 range(num_spin):
177
- ax = plt.subplot(grid[spin])
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], str(spin), fontsize=12, path_effects=[patheffects.withStroke(linewidth=3, foreground='w')])
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
- sc.set_figure_params(figsize=[0.18 * num_spin + 0.5, 0.18 * num_spin])
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
- plt.yticks(np.arange(len(sample_order)), np.array(sample_list)[sample_order], fontsize=8)
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 + 'top_regulators.png')
833
+ plt.savefig(fig_folder + f'top_regulators_program_{program_index}.png')
775
834
 
776
- plt.show()
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
1
+ Metadata-Version: 2.4
2
2
  Name: dspin
3
- Version: 1.5.2
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 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)).
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
@@ -21,7 +21,7 @@ install_requires = [
21
21
  # ]
22
22
 
23
23
  # Managing your version (adjust accordingly)
24
- version = "1.5.2" # You can manage versioning in a more sophisticated way if needed
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&nbsp; 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&nbsp; 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.
@@ -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
@@ -1,6 +0,0 @@
1
- anndata
2
- matplotlib
3
- scanpy
4
- tqdm
5
- igraph
6
- leidenalg