dspin 1.3.0__tar.gz → 1.3.2__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.0
3
+ Version: 1.3.2
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
@@ -76,7 +76,8 @@ import networkx as nx
76
76
 
77
77
  def summary_components(all_components: np.array,
78
78
  num_spin: int,
79
- summary_method: str = 'kmeans') -> List[np.array]:
79
+ summary_method: str = 'kmeans',
80
+ figure_folder: str=None) -> List[np.array]:
80
81
  """
81
82
  Summarize components using a clustering algorithm.
82
83
 
@@ -88,6 +89,8 @@ def summary_components(all_components: np.array,
88
89
  The number of clusters.
89
90
  summary_method : str, optional
90
91
  The method used for summarizing the components. Options are 'kmeans' or 'leiden'. Default is 'kmeans'.
92
+ figure_folder : str, optional
93
+ Folder in which to save the figure.
91
94
 
92
95
  Returns
93
96
  -------
@@ -99,7 +102,12 @@ def summary_components(all_components: np.array,
99
102
  if summary_method == 'kmeans':
100
103
  # Fit KMeans clustering to the components and to their transposed version.
101
104
  kmeans = KMeans(n_clusters=num_spin, random_state=0, n_init=50).fit(all_components)
102
- kmeans_gene = KMeans(n_clusters=2 * num_spin, random_state=0, n_init=10).fit(all_components.T)
105
+
106
+ # take the genes with larger weights for display only
107
+ gene_weight = np.sum(all_components ** 2, axis=0) ** 0.5
108
+ gene_sele_filt = gene_weight > np.mean(gene_weight)
109
+ 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)
103
111
 
104
112
  # Initialize an array to store the average component for each cluster.
105
113
  components_kmeans = np.zeros((num_spin, num_gene))
@@ -115,6 +123,24 @@ def summary_components(all_components: np.array,
115
123
  for ii in range(num_spin):
116
124
  # Determine gene cluster assignments based on the maximum component values.
117
125
  gene_groups_ind.append(np.argmax(components_kmeans, axis=0) == ii)
126
+
127
+
128
+
129
+ sc.set_figure_params(figsize=(8, 4))
130
+
131
+ plt.subplot(1, 2, 1)
132
+ gene_order = np.argsort(kmeans_gene.labels_)
133
+ comp_order = np.argsort(kmeans.labels_)
134
+ plt.imshow(all_components_sub[comp_order, :][:, gene_order], aspect='auto', cmap='Blues', vmax=np.max(all_components) / 10, interpolation='none')
135
+ plt.title('All components')
136
+
137
+ plt.subplot(1, 2, 2)
138
+ plt.imshow(components_kmeans[:, gene_sele_filt][:, gene_order], aspect='auto', cmap='Blues', vmax=np.max(components_kmeans) / 10, interpolation='none')
139
+ plt.title('Kmeans components')
140
+
141
+ if figure_folder is not None:
142
+ plt.savefig(figure_folder + 'onmf_decomposition_summary.png', dpi=300, bbox_inches='tight')
143
+ plt.close()
118
144
 
119
145
  elif summary_method == 'leiden':
120
146
 
@@ -144,8 +170,25 @@ def summary_components(all_components: np.array,
144
170
  diff = optimiser.optimise_partition(partition, n_iterations=- 1, is_membership_fixed=list(membership_fixed))
145
171
 
146
172
  gene_groups_ind = []
173
+ cluster_label_full = np.nan * np.ones(consensus.shape[0])
174
+
147
175
  for ii, part in enumerate(partition):
148
176
  gene_groups_ind.append(part)
177
+ cluster_label_full[part] = ii
178
+
179
+ gene_order = np.argsort(cluster_label_full)
180
+ gene_list_size = np.array([np.sum(cluster_label_full == ii) for ii in range(num_spin)])
181
+
182
+ plt.figure(figsize=[8, 8])
183
+ plt.imshow(consensus[gene_order][:, gene_order], cmap='Blues', vmin=0, vmax=np.percentile(consensus.flatten(), 98))
184
+ for ii in range(1, num_spin):
185
+ plt.axvline(np.sum(gene_list_size[:ii]) - 0.5, color='k')
186
+ plt.axhline(np.sum(gene_list_size[:ii]) - 0.5, color='k')
187
+ plt.grid()
188
+
189
+ if figure_folder is not None:
190
+ plt.savefig(figure_folder + 'onmf_decomposition_summary.png', dpi=300, bbox_inches='tight')
191
+ plt.close()
149
192
 
150
193
  return gene_groups_ind
151
194
 
@@ -194,7 +237,9 @@ def onmf(X: np.array, rank: int, max_iter: int = 500) -> Tuple[np.array, np.arra
194
237
  # progress bar
195
238
  if itr % 10 == 0:
196
239
  error = np.linalg.norm(X - np.dot(A, S), 'fro')
197
- pbar.set_postfix({"Reconstruction Error": f"{error:.2f}"})
240
+ self_product = S.dot(S.T)
241
+ orthogonal_error = np.linalg.norm(self_product - np.diag(np.diag(self_product)), 'fro') / np.linalg.norm(S, 'fro')
242
+ pbar.set_postfix({"Reconstruction error": f"{error:.2f}", "Orthogonal error": f"{orthogonal_error:.4f}"})
198
243
 
199
244
  pbar.close()
200
245
 
@@ -245,7 +290,9 @@ def compute_onmf(seed: int,
245
290
  return nmf_model
246
291
 
247
292
 
248
- def onmf_discretize(onmf_rep_ori: np.array, fig_folder: str = None) -> np.array:
293
+ def onmf_discretize(onmf_rep_ori: np.array,
294
+ num_init: int = 10,
295
+ fig_folder: str = None) -> np.array:
249
296
  """
250
297
  Discretize the ONMF representation using KMeans clustering and visualize the sorted representations.
251
298
 
@@ -268,10 +315,11 @@ def onmf_discretize(onmf_rep_ori: np.array, fig_folder: str = None) -> np.array:
268
315
  21, num_spin))
269
316
 
270
317
  onmf_rep_tri = np.zeros(onmf_rep_ori.shape)
271
- for ii in range(num_spin):
318
+ print('Processing gene or program expression levels...')
319
+ for ii in tqdm(range(num_spin)):
272
320
  # Perform KMeans clustering for each spin/component
273
- km_fit = KMeans(n_clusters=3, n_init=10).fit(
274
- onmf_rep_ori[:, ii].reshape(-1, 1))
321
+ km_fit = KMeans(n_clusters=3, n_init=num_init).fit(onmf_rep_ori[:, ii].reshape(-1, 1))
322
+
275
323
  onmf_rep_tri[:, ii] = (km_fit.labels_ == np.argsort(km_fit.cluster_centers_.reshape(-1))[0]) * \
276
324
  (-1) + (km_fit.labels_ == np.argsort(km_fit.cluster_centers_.reshape(-1))[2]) * 1
277
325
 
@@ -279,8 +327,7 @@ def onmf_discretize(onmf_rep_ori: np.array, fig_folder: str = None) -> np.array:
279
327
  if ii < 21:
280
328
  ax = plt.subplot(grid[ii])
281
329
  plt.plot(np.sort(onmf_rep_ori[:, ii]))
282
- plt.plot(np.sort(km_fit.cluster_centers_[
283
- km_fit.labels_].reshape(-1)))
330
+ plt.plot(np.sort(km_fit.cluster_centers_[km_fit.labels_].reshape(-1)))
284
331
 
285
332
  # Save the visual representation
286
333
  if fig_folder is not None:
@@ -519,50 +566,7 @@ def para_moments(j_mat: np.array, h_vec: np.array) -> Tuple[np.array, np.array]:
519
566
  return corr_para, mean_para
520
567
 
521
568
 
522
- @numba.njit
523
- def np_apply_along_axis(func1d: Callable[[np.ndarray], float],
524
- axis: int,
525
- arr: np.ndarray) -> np.ndarray:
526
- """
527
- Applies a function along the specified axis.
528
-
529
- Parameters
530
- ----------
531
- func1d : Callable[[np.ndarray], float]
532
- A 1-D function to be applied.
533
- axis : int
534
- Axis along which the function should be applied.
535
- arr : np.ndarray
536
- Input array.
537
-
538
- Returns
539
- -------
540
- np.ndarray
541
- The result of applying func1d to arr along the specified axis.
542
- """
543
- assert arr.ndim == 2
544
- assert axis in [0, 1]
545
- result = np.empty(arr.shape[1]) if axis == 0 else np.empty(arr.shape[0])
546
-
547
- for i in range(len(result)):
548
- result[i] = func1d(arr[:, i]) if axis == 0 else func1d(arr[i, :])
549
-
550
- return result
551
-
552
-
553
- @numba.njit
554
- def np_mean(array: np.array, axis: int) -> np.array:
555
- """
556
- Computes the arithmetic mean along the specified axis.
557
-
558
- Parameters:
559
- array (np.array): Input array.
560
- axis (int): Axis along which the mean is computed. 1 is for row-wise, 0 is for column-wise.
561
- """
562
- return np_apply_along_axis(np.mean, axis, array)
563
-
564
-
565
- @numba.jit()
569
+ @numba.njit(fastmath=True)
566
570
  def pseudol_gradient(cur_j: np.ndarray,
567
571
  cur_h: np.ndarray,
568
572
  cur_state: np.ndarray,
@@ -587,37 +591,43 @@ def pseudol_gradient(cur_j: np.ndarray,
587
591
  The gradient of j.
588
592
  The gradient of h.
589
593
  """
590
- num_spin = cur_j.shape[0]
594
+ num_spin, num_sample = cur_state.shape
595
+ sample_factor = 1.0 / num_sample
591
596
 
592
- cur_j_grad = np.zeros((num_spin, num_spin))
593
- cur_h_grad = np.zeros((num_spin, 1))
594
-
595
- # Filtering diagonal elements of cur_j matrix.
596
- j_filt = cur_j.copy()
597
- np.fill_diagonal(j_filt, 0)
598
- effective_h = j_filt.dot(cur_state) + cur_h
597
+ diag_j = np.empty(num_spin, dtype=cur_j.dtype)
598
+ for ii in range(num_spin):
599
+ diag_j[ii] = cur_j[ii, ii]
599
600
 
601
+ j_no_diag = cur_j.copy()
600
602
  for ii in range(num_spin):
601
- # Calculating gradients for each spin.
602
- j_sub = cur_j[ii, ii]
603
- h_sub = effective_h[ii, :]
603
+ j_no_diag[ii, ii] = 0.0
604
604
 
605
- term1 = np.exp(j_sub + h_sub)
606
- term2 = np.exp(j_sub - h_sub)
605
+ eff_h = j_no_diag @ cur_state + cur_h.reshape(num_spin, 1)
607
606
 
608
- # Compute gradients.
609
- j_sub_grad = cur_state[ii, :] ** 2 - \
610
- (term1 + term2) / (term1 + term2 + 1)
611
- h_eff_grad = cur_state[ii, :] - (term1 - term2) / (term1 + term2 + 1)
607
+ exp_plus = np.empty_like(eff_h)
608
+ exp_minus = np.empty_like(eff_h)
609
+ denom = np.empty_like(eff_h)
612
610
 
613
- j_off_sub_grad = h_eff_grad * cur_state
611
+ for ii in range(num_spin):
612
+ exp_plus[ii, :] = np.exp(diag_j[ii] + eff_h[ii, :])
613
+ exp_minus[ii, :] = np.exp(diag_j[ii] - eff_h[ii, :])
614
+ denom[ii, :] = exp_plus[ii, :] + exp_minus[ii, :] + 1.0
615
+
616
+ # Diagonal (self‑coupling) gradients
617
+ term_diag = cur_state * cur_state - (exp_plus + exp_minus) / denom
618
+
619
+ # Off‑diagonal and field gradients
620
+ h_eff_grad = cur_state - (exp_plus - exp_minus) / denom
621
+ cur_j_grad = (h_eff_grad @ cur_state.T) * sample_factor
622
+ for ii in range(num_spin):
623
+ cur_j_grad[ii, ii] = np.sum(term_diag[ii, :]) * sample_factor
614
624
 
615
- cur_j_grad[ii, :] = np_mean(j_off_sub_grad, axis=1)
616
- cur_j_grad[ii, ii] = np.mean(j_sub_grad)
617
- cur_h_grad[ii] = np.mean(h_eff_grad)
625
+ cur_h_grad = np.zeros((num_spin, 1), dtype=cur_j.dtype)
626
+ for ii in range(num_spin):
627
+ cur_h_grad[ii, 0] = np.sum(h_eff_grad[ii, :]) * sample_factor
618
628
 
619
- if not directed:
620
- cur_j_grad = (cur_j_grad + cur_j_grad.T) / 2
629
+ if not directed:
630
+ cur_j_grad = 0.5 * (cur_j_grad + cur_j_grad.T)
621
631
 
622
632
  return - cur_j_grad, - cur_h_grad
623
633
 
@@ -927,6 +937,8 @@ def learn_network_adam(raw_data: Any,
927
937
  The trained h matrix.
928
938
  The training log.
929
939
  """
940
+ np.random.seed(train_dat['seed'])
941
+
930
942
  # Retrieve training parameters and data
931
943
  num_spin, num_round = train_dat['cur_h'].shape
932
944
  num_epoch, stepsz, rec_gap = (
@@ -936,10 +948,7 @@ def learn_network_adam(raw_data: Any,
936
948
  list_step = np.arange(num_epoch, 0, - rec_gap)[::-1]
937
949
  cur_j, cur_h = (train_dat.get(key, None) for key in ["cur_j", "cur_h"])
938
950
  save_path = train_dat.get('save_path', None)
939
- backtrack_gap, backtrack_tol = (
940
- train_dat.get(
941
- key, None) for key in [
942
- "backtrack_gap", "backtrack_tol"])
951
+ backtrack_gap, backtrack_tol = (train_dat.get(key, None) for key in ["backtrack_gap", "backtrack_tol"])
943
952
 
944
953
  # Initialize variables to store parameters, gradients, and other values
945
954
  # during training
@@ -985,15 +994,15 @@ def learn_network_adam(raw_data: Any,
985
994
 
986
995
  # Save training log and print progress
987
996
  if counter in list_step:
988
- sio.savemat(save_path + 'train_log.mat',
989
- {'list_step': list_step,
990
- 'rec_hvec_all': rec_hvec_all,
991
- 'rec_jmat_all': rec_jmat_all,
992
- 'rec_jgrad_sum_norm': rec_jgrad_sum_norm})
993
- # print('Progress: %d, Network gradient: %f' % (
994
- # np.round(100 * counter / num_epoch, 2), rec_jgrad_sum_norm[counter - 1]))
997
+ if train_dat['save_log']:
998
+ sio.savemat(save_path + 'train_log.mat',
999
+ {'list_step': list_step,
1000
+ 'rec_hvec_all': rec_hvec_all,
1001
+ 'rec_jmat_all': rec_jmat_all,
1002
+ 'rec_jgrad_sum_norm': rec_jgrad_sum_norm})
1003
+
995
1004
  pbar.update(rec_gap)
996
- pbar.set_postfix({"Network Gradient": f"{rec_jgrad_sum_norm[counter - 1]:.4f}"})
1005
+ pbar.set_postfix({"Network Gradient": f"{rec_jgrad_sum_norm[counter - 1]:.6f}"})
997
1006
 
998
1007
  # Handle backtracking
999
1008
 
@@ -130,7 +130,9 @@ class AbstractDSPIN(ABC):
130
130
  def sample_list(self, value):
131
131
  self._samp_list = value
132
132
 
133
- def discretize(self, clip_percentile: float = 100) -> None:
133
+ def discretize(self,
134
+ clip_percentile: float = 100,
135
+ num_init: int = 10) -> None:
134
136
  """
135
137
  Discretizes the oNMF representation into three states (-1, 0, 1) using K-means clustering.
136
138
 
@@ -138,6 +140,8 @@ class AbstractDSPIN(ABC):
138
140
  ----------
139
141
  clip_percentile : float, optional
140
142
  Percentile at which to clip values before discretization (default is 100).
143
+ num_init : int, optional
144
+ Number of initializations for K-means clustering (default is 10).
141
145
  """
142
146
  onmf_rep_ori = self.program_representation_raw.copy()
143
147
  fig_folder = self.fig_folder
@@ -147,7 +151,7 @@ class AbstractDSPIN(ABC):
147
151
  onmf_rep_ori[:, ii] = onmf_rep_ori[:, ii] / np.percentile(onmf_rep_ori[:, ii], clip_percentile)
148
152
  onmf_rep_ori = onmf_rep_ori.clip(0, 1)
149
153
 
150
- onmf_rep_tri = onmf_discretize(onmf_rep_ori, fig_folder)
154
+ onmf_rep_tri = onmf_discretize(onmf_rep_ori, num_init, fig_folder)
151
155
  self._onmf_rep_tri = onmf_rep_tri
152
156
 
153
157
  def raw_data_corr(self, sample_id_key: str) -> None:
@@ -204,17 +208,19 @@ class AbstractDSPIN(ABC):
204
208
  self._raw_data = raw_data
205
209
 
206
210
  num_sample = len(raw_data)
207
- params = {'num_epoch': 200,
211
+ params = {'num_epoch': 400,
208
212
  'cur_j': np.zeros((num_spin, num_spin)),
209
213
  'cur_h': np.zeros((num_spin, num_sample)),
210
214
  'save_path': self.save_path,
211
- 'rec_gap': 10}
215
+ 'rec_gap': 10,
216
+ 'seed': 0,
217
+ 'save_log': False}
212
218
  params.update({'lambda_l1_j': 0.01,
213
219
  'lambda_l1_h': 0,
214
220
  'lambda_l2_j': 0,
215
221
  'lambda_l2_h': 0.005})
216
- params.update({'backtrack_gap': 20,
217
- 'backtrack_tol': 4})
222
+ params.update({'backtrack_gap': 40,
223
+ 'backtrack_tol': 5})
218
224
 
219
225
  if method == 'maximum_likelihood':
220
226
  params['stepsz'] = 0.2
@@ -350,7 +356,7 @@ class GeneDSPIN(AbstractDSPIN):
350
356
  adata: ad.AnnData,
351
357
  save_path: str,
352
358
  num_spin: int,
353
- clip_percentile: float = 95):
359
+ discretize_params: dict = {}):
354
360
  """
355
361
  Initialize the GeneDSPIN object.
356
362
 
@@ -362,6 +368,8 @@ class GeneDSPIN(AbstractDSPIN):
362
368
  Directory path where results will be saved.
363
369
  num_spin : int
364
370
  Number of spins used in inference.
371
+ use_default_discretize : bool, optional
372
+ Whether to use default discretization. Default is True. Otherwise the discretized data should be explicited setted in program_representation.
365
373
  clip_percentile : float, optional
366
374
  Percentile threshold for clipping values before discretization. Default is 95.
367
375
  """
@@ -374,7 +382,14 @@ class GeneDSPIN(AbstractDSPIN):
374
382
  else:
375
383
  self._onmf_rep_ori = adata.X
376
384
 
377
- self.discretize(clip_percentile)
385
+ dparams = {'use_default_discretize': True,
386
+ 'clip_percentile': 95,
387
+ 'num_init': 10}
388
+ dparams.update(discretize_params)
389
+
390
+ if dparams['use_default_discretize']:
391
+ self.discretize(clip_percentile=dparams['clip_percentile'],
392
+ num_init=dparams['num_init'])
378
393
 
379
394
  def program_regulator_discovery(self,
380
395
  program_representation_raw: np.ndarray,
@@ -439,10 +454,7 @@ class ProgramDSPIN(AbstractDSPIN):
439
454
  def __init__(self,
440
455
  adata: ad.AnnData,
441
456
  save_path: str,
442
- num_spin: int,
443
- num_onmf_components: int = None,
444
- prior_programs: List[List[str]] = None,
445
- num_repeat: int = 10):
457
+ num_spin: int):
446
458
  """
447
459
  Initialize the ProgramDSPIN object.
448
460
 
@@ -454,12 +466,6 @@ class ProgramDSPIN(AbstractDSPIN):
454
466
  Directory path for saving results.
455
467
  num_spin : int
456
468
  Number of spins used in DSPIN.
457
- num_onmf_components : int, optional
458
- Number of oNMF components, default is None.
459
- prior_programs : List[List[str]], optional
460
- List of predefined gene programs.
461
- num_repeat : int, optional
462
- Number of repetitions for oNMF computation. Default is 10.
463
469
  """
464
470
  super().__init__(adata, save_path, num_spin)
465
471
 
@@ -469,8 +475,6 @@ class ProgramDSPIN(AbstractDSPIN):
469
475
  self._gene_matrix_large = None
470
476
  self._use_data_list = None
471
477
  self.gene_program_csv = None
472
- self.prior_programs = None
473
- self.preprogram_num = len(prior_programs) if prior_programs else 0
474
478
 
475
479
  @property
476
480
  def onmf_decomposition(self):
@@ -518,7 +522,7 @@ class ProgramDSPIN(AbstractDSPIN):
518
522
 
519
523
  all_components = rec_components.reshape(-1, np.sum(prior_programs_mask))
520
524
 
521
- gene_group_ind = summary_components(all_components, num_onmf_components, summary_method=summary_method)
525
+ gene_group_ind = summary_components(all_components, num_onmf_components, summary_method=summary_method, figure_folder=self.fig_folder)
522
526
 
523
527
  # Mask the prior programs and add them to the gene group indices
524
528
  sub_mask_ind = np.where(prior_programs_mask)[0]
@@ -545,32 +549,28 @@ class ProgramDSPIN(AbstractDSPIN):
545
549
  cluster_key: str = 'leiden',
546
550
  mode: str = 'compute_summary',
547
551
  prior_programs: List[List[str]] = None,
548
- params: dict = {}) -> None:
552
+ clip_percentile: float = 100,
553
+ params: dict = {},
554
+ discretize_params: dict = {}) -> None:
549
555
  """
550
556
  Discover gene programs by performing oNMF decomposition on the annotated data.
551
557
 
552
558
  Parameters
553
559
  ----------
554
- num_onmf_components : int, optional
555
- Number of oNMF components. Default is None.
556
- num_subsample : int, optional
557
- Number of samples to use after subsampling. Default is 10000.
558
- num_subsample_large : int, optional
559
- Number of samples for large subsampling. Default is None.
560
560
  num_repeat : int, optional
561
561
  Number of times to repeat oNMF. Default is 10.
562
562
  seed : int, optional
563
563
  Random seed for reproducibility. Default is 0.
564
- balance_obs : str, optional
565
- Observation category to balance. Default is None.
566
- balance_method : str, optional
567
- Method used for balancing categories. Default is None.
568
- max_sample_rate : float, optional
569
- Maximum sampling rate. Default is 2.
564
+ cluster_key : str, optional
565
+ Key in `adata.obs` for clustering labels. Default is 'leiden'.
566
+ mode: str, optional
567
+ Mode of operation. Options: 'compute_summary', 'compute_only', 'summary_only'. Default is 'compute_summary'.
570
568
  prior_programs : List[List[str]], optional
571
569
  List of predefined gene programs. Default is None.
572
- summary_method : str, optional
573
- Method to summarize oNMF components. Default is 'kmeans'.
570
+ clip_percentile : float, optional
571
+ Percentile for clipping values before discretization. Default is 100.
572
+ params : dict, optional
573
+ Additional parameters for oNMF. Default is an empty dictionary.
574
574
  """
575
575
 
576
576
  if mode not in ['compute_summary', 'compute_only', 'summary_only']:
@@ -661,7 +661,12 @@ class ProgramDSPIN(AbstractDSPIN):
661
661
  self._onmf_rep_ori = onmf_summary.transform(
662
662
  gene_matrix / self.matrix_std)
663
663
 
664
- self.discretize()
664
+ dparams = {'clip_percentile': 100,
665
+ 'num_init': 10}
666
+ dparams.update(discretize_params)
667
+
668
+ self.discretize(clip_percentile=dparams['clip_percentile'],
669
+ num_init=dparams['num_init'])
665
670
 
666
671
 
667
672
  class DSPIN(object):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dspin
3
- Version: 1.3.0
3
+ Version: 1.3.2
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.0" # You can manage versioning in a more sophisticated way if needed
24
+ version = "1.3.2" # 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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes