dspin 1.3.1__tar.gz → 1.3.3__tar.gz

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