dspin 1.3.2__tar.gz → 1.3.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dspin
3
- Version: 1.3.2
3
+ Version: 1.3.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
dspin-1.3.4/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&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).
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&nbsp; 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.
@@ -4,7 +4,6 @@
4
4
  @Author : Jialong Jiang
5
5
  '''
6
6
 
7
-
8
7
  import csv
9
8
  import pandas as pd
10
9
  from typing import List
@@ -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(
@@ -107,7 +108,7 @@ def summary_components(all_components: np.array,
107
108
  gene_weight = np.sum(all_components ** 2, axis=0) ** 0.5
108
109
  gene_sele_filt = gene_weight > np.mean(gene_weight)
109
110
  all_components_sub = all_components[:, gene_sele_filt]
110
- kmeans_gene = KMeans(n_clusters=2 * num_spin, random_state=0, n_init=10).fit(all_components_sub.T)
111
+ kmeans_gene = KMeans(n_clusters=min(2 * num_spin, all_components_sub.shape[1]), random_state=0, n_init=10).fit(all_components_sub.T)
111
112
 
112
113
  # Initialize an array to store the average component for each cluster.
113
114
  components_kmeans = np.zeros((num_spin, num_gene))
@@ -526,7 +527,7 @@ def sample_states(samp_full: np.array, onmf_rep_tri: np.array) -> Tuple[np.array
526
527
  return state_list, samp_list
527
528
 
528
529
 
529
- def para_moments(j_mat: np.array, h_vec: np.array) -> Tuple[np.array, np.array]:
530
+ def para_moments_numpy(j_mat: np.array, h_vec: np.array) -> Tuple[np.array, np.array]:
530
531
  """
531
532
  Calculate the correlation and mean parameters given a j matrix and an h vector.
532
533
 
@@ -565,6 +566,74 @@ def para_moments(j_mat: np.array, h_vec: np.array) -> Tuple[np.array, np.array]:
565
566
 
566
567
  return corr_para, mean_para
567
568
 
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]:
572
+ """
573
+ Calculate the correlation and mean parameters given an interaction
574
+ matrix and an external field vector, using a Numba‑accelerated kernel.
575
+
576
+ Parameters
577
+ ----------
578
+ j_mat : np.ndarray
579
+ Interaction matrix.
580
+ h_vec : np.ndarray
581
+ External field vector.
582
+
583
+ Returns
584
+ -------
585
+ Tuple[np.ndarray, np.ndarray]
586
+ The correlation parameter array of shape (n, n).
587
+ The mean parameter array of shape (n,).
588
+ """
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]
595
+
596
+ num_sample: int = 3 ** num_spin
597
+
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)
601
+
602
+ spins = np.empty(num_spin, dtype=np.int8)
603
+
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
610
+
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
631
+
632
+ mean_para /= partition
633
+ corr_para /= partition
634
+
635
+ return corr_para, mean_para
636
+
568
637
 
569
638
  @numba.njit(fastmath=True)
570
639
  def pseudol_gradient(cur_j: np.ndarray,
@@ -602,7 +671,7 @@ def pseudol_gradient(cur_j: np.ndarray,
602
671
  for ii in range(num_spin):
603
672
  j_no_diag[ii, ii] = 0.0
604
673
 
605
- eff_h = j_no_diag @ cur_state + cur_h.reshape(num_spin, 1)
674
+ eff_h = j_no_diag @ cur_state + cur_h
606
675
 
607
676
  exp_plus = np.empty_like(eff_h)
608
677
  exp_minus = np.empty_like(eff_h)
@@ -727,10 +796,10 @@ def samp_moments(j_mat: np.ndarray,
727
796
  return corr_para, mean_para
728
797
 
729
798
 
730
- def compute_gradient(cur_j: np.ndarray,
731
- cur_h: np.ndarray,
732
- raw_data: List[Tuple[np.ndarray, np.ndarray]],
733
- method: str, train_dat: Dict[str, Any]) -> Tuple[np.ndarray, np.ndarray]:
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]:
734
803
  """
735
804
  Compute the gradient based on the specified method.
736
805
 
@@ -763,7 +832,7 @@ def compute_gradient(cur_j: np.ndarray,
763
832
  # Computing gradients using the specified method.
764
833
  if method == 'pseudo_likelihood':
765
834
  j_grad, h_grad = pseudol_gradient(
766
- cur_j, cur_h[:, kk].reshape(- 1, 1), raw_data[kk], directed=train_dat['directed'])
835
+ cur_j, cur_h[:, kk: kk + 1], raw_data[kk], directed=train_dat['directed'])
767
836
  h_grad = h_grad.flatten()
768
837
  else:
769
838
  # Distinguishing between other methods and computing gradients
@@ -772,7 +841,7 @@ def compute_gradient(cur_j: np.ndarray,
772
841
  corr_para, mean_para = para_moments(cur_j, cur_h[:, kk])
773
842
  elif method == 'mcmc_maximum_likelihood':
774
843
  corr_para, mean_para = samp_moments(
775
- 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'])
776
845
  j_grad = corr_para - raw_data[kk][0]
777
846
  h_grad = mean_para - raw_data[kk][1].flatten()
778
847
 
@@ -782,6 +851,77 @@ def compute_gradient(cur_j: np.ndarray,
782
851
  return rec_jgrad, rec_hgrad
783
852
 
784
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
+
785
925
  def apply_regularization(rec_jgrad: np.ndarray,
786
926
  rec_hgrad: np.ndarray,
787
927
  cur_j: np.ndarray,
@@ -964,6 +1104,8 @@ def learn_network_adam(raw_data: Any,
964
1104
 
965
1105
  backtrack_counter = 0
966
1106
  counter = 1
1107
+
1108
+ tqdm._instances.clear()
967
1109
  pbar = tqdm(total=num_epoch)
968
1110
 
969
1111
  while counter <= num_epoch:
@@ -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):
@@ -206,6 +207,7 @@ class AbstractDSPIN(ABC):
206
207
  self._samp_list = self._samp_list[example_list_ind]
207
208
  raw_data = raw_data[example_list_ind]
208
209
  self._raw_data = raw_data
210
+ print("Using a subset of {} samples for network inference.".format(len(self._samp_list)))
209
211
 
210
212
  num_sample = len(raw_data)
211
213
  params = {'num_epoch': 400,
@@ -337,8 +339,8 @@ class AbstractDSPIN(ABC):
337
339
  raise ValueError(f"Column '{batch_key}' not found in adata.obs.")
338
340
 
339
341
  sample_list = self._samp_list
340
- sample_to_control = self.adata.obs.groupby(sample_id_key)[if_control_key].first().to_dict()
341
- sample_to_batch = self.adata.obs.groupby(sample_id_key)[batch_key].first().to_dict()
342
+ sample_to_control = self.adata.obs.groupby(sample_id_key, observed=False)[if_control_key].first().to_dict()
343
+ sample_to_batch = self.adata.obs.groupby(sample_id_key, observed=False)[batch_key].first().to_dict()
342
344
 
343
345
  if_control = np.array([sample_to_control[sample] for sample in sample_list])
344
346
  batch_index = np.array([sample_to_batch[sample] for sample in sample_list])
@@ -382,8 +384,20 @@ class GeneDSPIN(AbstractDSPIN):
382
384
  else:
383
385
  self._onmf_rep_ori = adata.X
384
386
 
387
+ gene_skew = np.zeros(num_spin)
388
+
389
+ for ii in range(num_spin):
390
+ cur_gene = self._onmf_rep_ori[:, ii]
391
+ gene_skew[ii] = skew(cur_gene, bias=False)
392
+
393
+ if gene_skew.mean() > 2:
394
+ default_clip = 95
395
+ print("Input gene expression seem to be heavy-tailed. Using 95 percentile for clipping before discretization.")
396
+ else:
397
+ default_clip = 100
398
+
385
399
  dparams = {'use_default_discretize': True,
386
- 'clip_percentile': 95,
400
+ 'clip_percentile': default_clip,
387
401
  'num_init': 10}
388
402
  dparams.update(discretize_params)
389
403
 
@@ -484,6 +498,42 @@ class ProgramDSPIN(AbstractDSPIN):
484
498
  def onmf_decomposition(self, value):
485
499
  self._onmf_summary = value
486
500
 
501
+ def compute_onmf_expression(self,
502
+ sample_matrix: np.ndarray,
503
+ gene_group_ind: List[List[int]]):
504
+ """
505
+ Compute the oNMF expression for a given sample matrix and gene components of each program.
506
+
507
+ Parameters
508
+ ----------
509
+ sample_matrix : np.ndarray
510
+ The sample matrix containing gene expressions.
511
+ gene_group_ind : List[List[int]]
512
+ List of lists containing indices of genes for each program component.
513
+
514
+ Returns
515
+ -------
516
+ NMF
517
+ An NMF object containing the computed components.
518
+ """
519
+ num_spin = self.num_spin
520
+ adata = self.adata
521
+
522
+ # Initialize and compute the summary matrix
523
+ components_summary = np.zeros((num_spin, adata.shape[1]))
524
+ for ii in range(num_spin):
525
+ sub_matrix = sample_matrix[:, gene_group_ind[ii]]
526
+ sub_onmf = NMF(n_components=1, init='random', random_state=0).fit(sub_matrix)
527
+ components_summary[ii, gene_group_ind[ii]] = sub_onmf.components_
528
+
529
+ # Normalize the summary components and finalize the onmf_summary object
530
+ components_summary = normalize(components_summary, axis=1, norm='l2')
531
+ onmf_summary = NMF(n_components=num_spin, init='random', random_state=0)
532
+ onmf_summary.components_ = components_summary
533
+
534
+ return onmf_summary
535
+
536
+
487
537
  def summarize_onmf_result(self,
488
538
  large_subsample_matrix: np.ndarray,
489
539
  num_onmf_components: int,
@@ -504,10 +554,6 @@ class ProgramDSPIN(AbstractDSPIN):
504
554
  NMF
505
555
  An NMF object containing the summarized components.
506
556
  """
507
-
508
- # Retrieve the number of spins and annotated data
509
- num_spin = self.num_spin
510
- adata = self.adata
511
557
  prior_programs_mask = self.prior_programs_mask
512
558
  seed_list = self.onmf_parameters['seed_list']
513
559
  summary_method = self.onmf_parameters['summary_method']
@@ -520,7 +566,7 @@ class ProgramDSPIN(AbstractDSPIN):
520
566
  cur_onmf = np.load(self.save_path + f'onmf/onmf_components_{num_onmf_components}_repeat_{cur_seed}.npy', allow_pickle=True).item()
521
567
  rec_components[ii] = cur_onmf.components_
522
568
 
523
- all_components = rec_components.reshape(-1, np.sum(prior_programs_mask))
569
+ all_components = rec_components.reshape(- 1, np.sum(prior_programs_mask))
524
570
 
525
571
  gene_group_ind = summary_components(all_components, num_onmf_components, summary_method=summary_method, figure_folder=self.fig_folder)
526
572
 
@@ -529,17 +575,8 @@ class ProgramDSPIN(AbstractDSPIN):
529
575
  gene_group_ind = [sub_mask_ind[gene_list] for gene_list in gene_group_ind]
530
576
  gene_group_ind += self.prior_programs_ind
531
577
 
532
- # Initialize and compute the summary matrix
533
- components_summary = np.zeros((num_spin, adata.shape[1]))
534
- for ii in range(num_spin):
535
- sub_matrix = large_subsample_matrix[:, gene_group_ind[ii]]
536
- sub_onmf = NMF(n_components=1, init='random', random_state=0).fit(sub_matrix)
537
- components_summary[ii, gene_group_ind[ii]] = sub_onmf.components_
538
-
539
- # Normalize the summary components and finalize the onmf_summary object
540
- components_summary = normalize(components_summary, axis=1, norm='l2')
541
- onmf_summary = NMF(n_components=num_spin, init='random', random_state=0)
542
- onmf_summary.components_ = components_summary
578
+ # Evaluate the expression level of each program and the weights for each gene in the program
579
+ onmf_summary = self.compute_onmf_expression(large_subsample_matrix, gene_group_ind)
543
580
 
544
581
  return onmf_summary
545
582
 
@@ -549,7 +586,6 @@ class ProgramDSPIN(AbstractDSPIN):
549
586
  cluster_key: str = 'leiden',
550
587
  mode: str = 'compute_summary',
551
588
  prior_programs: List[List[str]] = None,
552
- clip_percentile: float = 100,
553
589
  params: dict = {},
554
590
  discretize_params: dict = {}) -> None:
555
591
  """
@@ -567,10 +603,10 @@ class ProgramDSPIN(AbstractDSPIN):
567
603
  Mode of operation. Options: 'compute_summary', 'compute_only', 'summary_only'. Default is 'compute_summary'.
568
604
  prior_programs : List[List[str]], optional
569
605
  List of predefined gene programs. Default is None.
570
- clip_percentile : float, optional
571
- Percentile for clipping values before discretization. Default is 100.
572
606
  params : dict, optional
573
607
  Additional parameters for oNMF. Default is an empty dictionary.
608
+ discretize_params : dict, optional
609
+ Parameters for discretization of the oNMF representation. Default is an empty dictionary.
574
610
  """
575
611
 
576
612
  if mode not in ['compute_summary', 'compute_only', 'summary_only']:
@@ -595,6 +631,7 @@ class ProgramDSPIN(AbstractDSPIN):
595
631
 
596
632
  if onmf_parameters['num_subsample_large'] is None:
597
633
  onmf_parameters['num_subsample_large'] = min(onmf_parameters['num_subsample'] * 5, adata.shape[0])
634
+ print(onmf_parameters['num_subsample_large'])
598
635
 
599
636
  if onmf_parameters['balance_method'] not in ['equal', 'proportional', 'squareroot', None]:
600
637
  raise ValueError('balance_method must be one of equal, proportional, squareroot, or None')
@@ -607,7 +644,13 @@ class ProgramDSPIN(AbstractDSPIN):
607
644
  prior_program_ind = [[np.where(adata.var_names == gene)[0][0] for gene in gene_list] for gene_list in prior_programs]
608
645
  preprogram_flat = [gene for program in prior_programs for gene in program]
609
646
  if len(prior_programs) > num_spin:
610
- raise ValueError('Number of prior_programs must be less than the number of spins')
647
+ raise ValueError('Number of prior programs must be less than the number of programs')
648
+ elif len(prior_programs) == num_spin:
649
+ warnings.warn('Number of prior programs is equal to the number of programs. No oNMF is performed.')
650
+ mode = 'summary_only'
651
+ elif len(prior_programs) == 0:
652
+ warnings.warn('Empty prior programs')
653
+
611
654
  # Generate mask to exclude genes in prior programs from the annotated data
612
655
  prior_program_mask = ~ np.isin(adata.var_names, preprogram_flat)
613
656
  onmf_parameters['num_onmf_components'] -= len(prior_programs)
@@ -639,13 +682,19 @@ class ProgramDSPIN(AbstractDSPIN):
639
682
  compute_onmf_decomposition(sub_gene_matrix_normed, num_onmf_components, seed=cur_seed, params=onmf_parameters, save_path=self.save_path + f'onmf/onmf_components_{num_onmf_components}_repeat_{cur_seed}.npy')
640
683
 
641
684
  if mode == 'compute_summary' or mode == 'summary_only':
685
+
686
+ cluster_label_raw = adata.obs[cluster_key].values
687
+ cluster_label = pd.factorize(cluster_label_raw)[0]
642
688
 
643
689
  # Perform subsampling and standard deviation clipping on the matrix
644
- std_clipped, sub_large_gene_matrix_normed = subsample_normalize_gene_matrix(cur_gene_matrix, cluster_label, num_subsample=onmf_parameters['num_subsample_large'], seed=seed + rep, params=onmf_parameters)
690
+ std_clipped, sub_large_gene_matrix_normed = subsample_normalize_gene_matrix(adata.X, cluster_label, num_subsample=onmf_parameters['num_subsample_large'], seed=seed, params=onmf_parameters)
645
691
  self.matrix_std = std_clipped
646
692
 
647
693
  # Summarize the oNMF decompositions
648
- onmf_summary = self.summarize_onmf_result(sub_large_gene_matrix_normed, num_onmf_components, num_repeat)
694
+ if num_onmf_components > 0:
695
+ onmf_summary = self.summarize_onmf_result(sub_large_gene_matrix_normed, num_onmf_components, num_repeat)
696
+ else:
697
+ onmf_summary = self.compute_onmf_expression(sub_large_gene_matrix_normed, prior_program_ind)
649
698
 
650
699
  self._onmf_summary = onmf_summary
651
700
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dspin
3
- Version: 1.3.2
3
+ Version: 1.3.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
@@ -21,7 +21,7 @@ install_requires = [
21
21
  # ]
22
22
 
23
23
  # Managing your version (adjust accordingly)
24
- version = "1.3.2" # You can manage versioning in a more sophisticated way if needed
24
+ version = "1.3.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.3.2/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
- ![alternativetext](figure/readme/Figure1_20230309_Inna.png)
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
- ![alternativetext](figure/thomsonlab_signaling/example_conditions.png)
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
- ![alternativetext](figure/thomsonlab_signaling/gene_program_example.png)
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
- ![alternativetext](figure/thomsonlab_signaling/joint_network_perturbation.png)
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