dspin 1.3.1__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.1
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
@@ -290,7 +290,9 @@ def compute_onmf(seed: int,
290
290
  return nmf_model
291
291
 
292
292
 
293
- 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:
294
296
  """
295
297
  Discretize the ONMF representation using KMeans clustering and visualize the sorted representations.
296
298
 
@@ -313,10 +315,11 @@ def onmf_discretize(onmf_rep_ori: np.array, fig_folder: str = None) -> np.array:
313
315
  21, num_spin))
314
316
 
315
317
  onmf_rep_tri = np.zeros(onmf_rep_ori.shape)
316
- for ii in range(num_spin):
318
+ print('Processing gene or program expression levels...')
319
+ for ii in tqdm(range(num_spin)):
317
320
  # 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))
321
+ km_fit = KMeans(n_clusters=3, n_init=num_init).fit(onmf_rep_ori[:, ii].reshape(-1, 1))
322
+
320
323
  onmf_rep_tri[:, ii] = (km_fit.labels_ == np.argsort(km_fit.cluster_centers_.reshape(-1))[0]) * \
321
324
  (-1) + (km_fit.labels_ == np.argsort(km_fit.cluster_centers_.reshape(-1))[2]) * 1
322
325
 
@@ -324,8 +327,7 @@ def onmf_discretize(onmf_rep_ori: np.array, fig_folder: str = None) -> np.array:
324
327
  if ii < 21:
325
328
  ax = plt.subplot(grid[ii])
326
329
  plt.plot(np.sort(onmf_rep_ori[:, ii]))
327
- plt.plot(np.sort(km_fit.cluster_centers_[
328
- km_fit.labels_].reshape(-1)))
330
+ plt.plot(np.sort(km_fit.cluster_centers_[km_fit.labels_].reshape(-1)))
329
331
 
330
332
  # Save the visual representation
331
333
  if fig_folder is not None:
@@ -564,50 +566,7 @@ def para_moments(j_mat: np.array, h_vec: np.array) -> Tuple[np.array, np.array]:
564
566
  return corr_para, mean_para
565
567
 
566
568
 
567
- @numba.njit
568
- def np_apply_along_axis(func1d: Callable[[np.ndarray], float],
569
- axis: int,
570
- arr: np.ndarray) -> np.ndarray:
571
- """
572
- Applies a function along the specified axis.
573
-
574
- Parameters
575
- ----------
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.
582
-
583
- Returns
584
- -------
585
- np.ndarray
586
- The result of applying func1d to arr along the specified axis.
587
- """
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])
591
-
592
- for i in range(len(result)):
593
- result[i] = func1d(arr[:, i]) if axis == 0 else func1d(arr[i, :])
594
-
595
- return result
596
-
597
-
598
- @numba.njit
599
- def np_mean(array: np.array, axis: int) -> np.array:
600
- """
601
- Computes the arithmetic mean along the specified axis.
602
-
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)
608
-
609
-
610
- @numba.jit()
569
+ @numba.njit(fastmath=True)
611
570
  def pseudol_gradient(cur_j: np.ndarray,
612
571
  cur_h: np.ndarray,
613
572
  cur_state: np.ndarray,
@@ -632,37 +591,43 @@ def pseudol_gradient(cur_j: np.ndarray,
632
591
  The gradient of j.
633
592
  The gradient of h.
634
593
  """
635
- num_spin = cur_j.shape[0]
636
-
637
- cur_j_grad = np.zeros((num_spin, num_spin))
638
- cur_h_grad = np.zeros((num_spin, 1))
594
+ num_spin, num_sample = cur_state.shape
595
+ sample_factor = 1.0 / num_sample
639
596
 
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
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]
644
600
 
601
+ j_no_diag = cur_j.copy()
645
602
  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, :]
603
+ j_no_diag[ii, ii] = 0.0
649
604
 
650
- term1 = np.exp(j_sub + h_sub)
651
- term2 = np.exp(j_sub - h_sub)
605
+ eff_h = j_no_diag @ cur_state + cur_h.reshape(num_spin, 1)
652
606
 
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)
607
+ exp_plus = np.empty_like(eff_h)
608
+ exp_minus = np.empty_like(eff_h)
609
+ denom = np.empty_like(eff_h)
657
610
 
658
- 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
659
615
 
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)
616
+ # Diagonal (self‑coupling) gradients
617
+ term_diag = cur_state * cur_state - (exp_plus + exp_minus) / denom
663
618
 
664
- if not directed:
665
- cur_j_grad = (cur_j_grad + cur_j_grad.T) / 2
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
624
+
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
628
+
629
+ if not directed:
630
+ cur_j_grad = 0.5 * (cur_j_grad + cur_j_grad.T)
666
631
 
667
632
  return - cur_j_grad, - cur_h_grad
668
633
 
@@ -1029,13 +994,13 @@ def learn_network_adam(raw_data: Any,
1029
994
 
1030
995
  # Save training log and print progress
1031
996
  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]))
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
+
1039
1004
  pbar.update(rec_gap)
1040
1005
  pbar.set_postfix({"Network Gradient": f"{rec_jgrad_sum_norm[counter - 1]:.6f}"})
1041
1006
 
@@ -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:
@@ -209,7 +213,8 @@ class AbstractDSPIN(ABC):
209
213
  'cur_h': np.zeros((num_spin, num_sample)),
210
214
  'save_path': self.save_path,
211
215
  'rec_gap': 10,
212
- 'seed': 0}
216
+ 'seed': 0,
217
+ 'save_log': False}
213
218
  params.update({'lambda_l1_j': 0.01,
214
219
  'lambda_l1_h': 0,
215
220
  'lambda_l2_j': 0,
@@ -351,7 +356,7 @@ class GeneDSPIN(AbstractDSPIN):
351
356
  adata: ad.AnnData,
352
357
  save_path: str,
353
358
  num_spin: int,
354
- clip_percentile: float = 95):
359
+ discretize_params: dict = {}):
355
360
  """
356
361
  Initialize the GeneDSPIN object.
357
362
 
@@ -363,6 +368,8 @@ class GeneDSPIN(AbstractDSPIN):
363
368
  Directory path where results will be saved.
364
369
  num_spin : int
365
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.
366
373
  clip_percentile : float, optional
367
374
  Percentile threshold for clipping values before discretization. Default is 95.
368
375
  """
@@ -375,7 +382,14 @@ class GeneDSPIN(AbstractDSPIN):
375
382
  else:
376
383
  self._onmf_rep_ori = adata.X
377
384
 
378
- 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'])
379
393
 
380
394
  def program_regulator_discovery(self,
381
395
  program_representation_raw: np.ndarray,
@@ -440,10 +454,7 @@ class ProgramDSPIN(AbstractDSPIN):
440
454
  def __init__(self,
441
455
  adata: ad.AnnData,
442
456
  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):
457
+ num_spin: int):
447
458
  """
448
459
  Initialize the ProgramDSPIN object.
449
460
 
@@ -455,12 +466,6 @@ class ProgramDSPIN(AbstractDSPIN):
455
466
  Directory path for saving results.
456
467
  num_spin : int
457
468
  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
469
  """
465
470
  super().__init__(adata, save_path, num_spin)
466
471
 
@@ -470,8 +475,6 @@ class ProgramDSPIN(AbstractDSPIN):
470
475
  self._gene_matrix_large = None
471
476
  self._use_data_list = None
472
477
  self.gene_program_csv = None
473
- self.prior_programs = None
474
- self.preprogram_num = len(prior_programs) if prior_programs else 0
475
478
 
476
479
  @property
477
480
  def onmf_decomposition(self):
@@ -546,32 +549,28 @@ class ProgramDSPIN(AbstractDSPIN):
546
549
  cluster_key: str = 'leiden',
547
550
  mode: str = 'compute_summary',
548
551
  prior_programs: List[List[str]] = None,
549
- params: dict = {}) -> None:
552
+ clip_percentile: float = 100,
553
+ params: dict = {},
554
+ discretize_params: dict = {}) -> None:
550
555
  """
551
556
  Discover gene programs by performing oNMF decomposition on the annotated data.
552
557
 
553
558
  Parameters
554
559
  ----------
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
560
  num_repeat : int, optional
562
561
  Number of times to repeat oNMF. Default is 10.
563
562
  seed : int, optional
564
563
  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.
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'.
571
568
  prior_programs : List[List[str]], optional
572
569
  List of predefined gene programs. Default is None.
573
- summary_method : str, optional
574
- 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.
575
574
  """
576
575
 
577
576
  if mode not in ['compute_summary', 'compute_only', 'summary_only']:
@@ -662,7 +661,12 @@ class ProgramDSPIN(AbstractDSPIN):
662
661
  self._onmf_rep_ori = onmf_summary.transform(
663
662
  gene_matrix / self.matrix_std)
664
663
 
665
- 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'])
666
670
 
667
671
 
668
672
  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.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.1" # 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