dspin 1.3.4__tar.gz → 1.4.0__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
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: dspin
3
- Version: 1.3.4
3
+ Version: 1.4.0
4
4
  Summary: Regulatory network models from single-cell perturbation profiling
5
5
  Home-page: https://github.com/JialongJiang/DSPIN
6
6
  Author: Jialong Jiang
@@ -25,6 +25,18 @@ Requires-Dist: scanpy
25
25
  Requires-Dist: tqdm
26
26
  Requires-Dist: igraph
27
27
  Requires-Dist: leidenalg
28
+ Dynamic: author
29
+ Dynamic: author-email
30
+ Dynamic: classifier
31
+ Dynamic: description
32
+ Dynamic: description-content-type
33
+ Dynamic: home-page
34
+ Dynamic: keywords
35
+ Dynamic: license
36
+ Dynamic: license-file
37
+ Dynamic: requires-dist
38
+ Dynamic: requires-python
39
+ Dynamic: summary
28
40
 
29
41
  # D-SPIN: Constructing regulatory network models from scRNA-seq perturbation profiling
30
42
 
@@ -0,0 +1,18 @@
1
+
2
+ from . import annotate
3
+ from . import compute
4
+ from . import plot
5
+ from . import dspin
6
+
7
+ # Import main classes for easy access
8
+ from .dspin import DSPIN, GeneDSPIN, ProgramDSPIN, AbstractDSPIN
9
+
10
+ __all__ = [
11
+ 'DSPIN',
12
+ 'GeneDSPIN',
13
+ 'ProgramDSPIN',
14
+ 'AbstractDSPIN',
15
+ 'annotate',
16
+ 'compute',
17
+ 'plot'
18
+ ]
@@ -232,15 +232,16 @@ def onmf(X: np.array, rank: int, max_iter: int = 500) -> Tuple[np.array, np.arra
232
232
  coef_S = AtX / S.dot(AtX.T).dot(S)
233
233
  S = np.nan_to_num(S * coef_S) #, posinf=1e5)
234
234
 
235
- pbar.update(1)
235
+ # pbar.update(1)
236
236
 
237
237
  # Calculate the reconstruction error every 10 iterations and update the
238
238
  # progress bar
239
- if itr % 10 == 0:
239
+ if itr % 20 == 0:
240
240
  error = np.linalg.norm(X - np.dot(A, S), 'fro')
241
241
  self_product = S.dot(S.T)
242
242
  orthogonal_error = np.linalg.norm(self_product - np.diag(np.diag(self_product)), 'fro') / np.linalg.norm(S, 'fro')
243
243
  pbar.set_postfix({"Reconstruction error": f"{error:.2f}", "Orthogonal error": f"{orthogonal_error:.4f}"})
244
+ pbar.update(20)
244
245
 
245
246
  pbar.close()
246
247
 
@@ -278,6 +279,8 @@ def compute_onmf(seed: int,
278
279
  """
279
280
  # Generate a random seed
280
281
  np.random.seed(seed)
282
+
283
+ tqdm._instances.clear()
281
284
  # Factorized Matrices
282
285
  H, W = onmf(gene_matrix, num_spin, max_iter)
283
286
 
@@ -608,7 +611,7 @@ def para_moments(j_mat: np.ndarray,
608
611
  spins[s] = (tmp % 3) - 1
609
612
  tmp //= 3
610
613
 
611
- # Energy: E = −h·s − ½ sᵀJs
614
+ # Energy: E = −h·s − s'Js / 2
612
615
  energy: float = 0.0
613
616
  for ii in range(num_spin):
614
617
  energy -= h_vec[ii] * spins[ii]
@@ -796,6 +799,28 @@ def samp_moments(j_mat: np.ndarray,
796
799
  return corr_para, mean_para
797
800
 
798
801
 
802
+ def _gradient_one_round(kk: int,
803
+ cur_j: np.ndarray,
804
+ cur_h: np.ndarray,
805
+ raw_data_k,
806
+ method: str,
807
+ train_dat: Dict[str, Any]):
808
+ """Helper that compute gradient for a single round."""
809
+ if method == 'pseudo_likelihood':
810
+ j_grad, h_grad = pseudol_gradient(cur_j, cur_h[:, kk:kk + 1], raw_data_k, directed=train_dat['directed'])
811
+ h_grad = h_grad.flatten()
812
+ else:
813
+ if method == 'maximum_likelihood':
814
+ corr_para, mean_para = para_moments(cur_j, cur_h[:, kk])
815
+ elif method == 'mcmc_maximum_likelihood':
816
+ corr_para, mean_para = samp_moments(
817
+ cur_j, cur_h[:, kk], train_dat['mcmc_samplingsz'], train_dat['mcmc_samplingmix'], train_dat['mcmc_samplegap'])
818
+ j_grad = corr_para - raw_data_k[0]
819
+ h_grad = mean_para - raw_data_k[1].flatten()
820
+
821
+ return kk, j_grad, h_grad
822
+
823
+
799
824
  def compute_gradient(cur_j: np.ndarray,
800
825
  cur_h: np.ndarray,
801
826
  raw_data: List[Tuple[np.ndarray, np.ndarray]],
@@ -828,49 +853,32 @@ def compute_gradient(cur_j: np.ndarray,
828
853
  rec_jgrad = np.zeros((num_spin, num_spin, num_round))
829
854
  rec_hgrad = np.zeros((num_spin, num_round))
830
855
 
831
- for kk in range(num_round):
832
- # Computing gradients using the specified method.
833
- if method == 'pseudo_likelihood':
856
+ if method == 'pseudo_likelihood':
857
+ for kk in range(num_round):
858
+ # Computing gradients using the specified method.
834
859
  j_grad, h_grad = pseudol_gradient(
835
860
  cur_j, cur_h[:, kk: kk + 1], raw_data[kk], directed=train_dat['directed'])
836
861
  h_grad = h_grad.flatten()
837
- else:
838
- # Distinguishing between other methods and computing gradients
839
- # accordingly
840
- if method == 'maximum_likelihood':
841
- corr_para, mean_para = para_moments(cur_j, cur_h[:, kk])
842
- elif method == 'mcmc_maximum_likelihood':
843
- corr_para, mean_para = samp_moments(
844
- cur_j, cur_h[:, kk], train_dat['mcmc_samplingsz'], train_dat['mcmc_samplingmix'], train_dat['mcmc_samplegap'])
845
- j_grad = corr_para - raw_data[kk][0]
846
- h_grad = mean_para - raw_data[kk][1].flatten()
847
-
848
- rec_jgrad[:, :, kk] = j_grad
849
- rec_hgrad[:, kk] = h_grad
850
-
851
- return rec_jgrad, rec_hgrad
852
862
 
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()
863
+ rec_jgrad[:, :, kk] = j_grad
864
+ rec_hgrad[:, kk] = h_grad
864
865
  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
866
+ n_jobs = train_dat.get('max_workers', min(num_round, os.cpu_count()))
867
+ chunk = train_dat.get('chunk_size', 1) # rounds per task
868
+ backend = 'loky' # process‑based
869
+
870
+ # print(f'Processing with {n_jobs} workers...')
871
+ with parallel_backend(backend, n_jobs=n_jobs):
872
+ results = Parallel(batch_size=chunk)(
873
+ delayed(_gradient_one_round)(kk, cur_j, cur_h, raw_data[kk], method, train_dat)
874
+ for kk in range(num_round)
875
+ )
876
+
877
+ for kk, j_grad, h_grad in results:
878
+ rec_jgrad[:, :, kk] = j_grad
879
+ rec_hgrad[:, kk] = h_grad
880
+
881
+ return rec_jgrad, rec_hgrad
874
882
 
875
883
 
876
884
  def compute_gradient_para(cur_j: np.ndarray,
@@ -990,10 +998,13 @@ def apply_regularization(rec_jgrad: np.ndarray,
990
998
 
991
999
  if 'lambda_l2_j_prior_mask' in train_dat:
992
1000
  rec_jgrad += train_dat['lambda_l2_j_prior_mask'] * (cur_j * (train_dat['j_prior_mask'] == 0))[:, :, np.newaxis]
1001
+
1002
+ if 'lambda_l1_j_prior_mask' in train_dat:
1003
+ rec_jgrad += train_dat['lambda_l1_j_prior_mask'] * ((cur_j * (train_dat['j_prior_mask'] == 0))[:, :, np.newaxis] / 1e-3).clip(-1, 1)
993
1004
 
994
1005
  if print_regu:
995
1006
  print('Regularization parameters')
996
- all_regu_list = ['lambda_l1_j', 'lambda_l1_h', 'lambda_l2_j', 'lambda_l2_h', 'lambda_l2_j_prior', 'lambda_l2_j_prior_mask', 'lambda_l2_h_rela_prior', 'lambda_l1_h_rela', 'lambda_l2_h_rela']
1007
+ all_regu_list = ['lambda_l1_j', 'lambda_l1_h', 'lambda_l2_j', 'lambda_l2_h', 'lambda_l2_j_prior', 'lambda_l2_j_prior_mask', 'lambda_l2_h_rela_prior', 'lambda_l1_h_rela', 'lambda_l2_h_rela', 'lambda_l1_j_prior_mask']
997
1008
  all_values = [train_dat.get(key, 0) for key in all_regu_list]
998
1009
  for key, value in zip(all_regu_list, all_values):
999
1010
  if value > 0:
@@ -1153,7 +1164,6 @@ def learn_network_adam(raw_data: Any,
1153
1164
  if (counter > backtrack_gap) and (rec_jgrad_sum_norm[counter - 1] > 1.5 * rec_jgrad_sum_norm[counter - 1 - backtrack_gap]):
1154
1165
  if_backtrack = True
1155
1166
 
1156
-
1157
1167
  if (counter > 2 * backtrack_gap) and (counter % backtrack_gap == 0):
1158
1168
 
1159
1169
  est_cur_grad = rec_jgrad_sum_norm[counter - 1 - backtrack_gap: counter - 1].mean()
@@ -1162,7 +1172,6 @@ def learn_network_adam(raw_data: Any,
1162
1172
  if est_cur_grad > 1.05 * est_prev_grad:
1163
1173
  if_backtrack = True
1164
1174
 
1165
-
1166
1175
  if if_backtrack:
1167
1176
  print('Backtracking at epoch %d' % counter)
1168
1177
  backtrack_counter += 1
@@ -1180,8 +1189,7 @@ def learn_network_adam(raw_data: Any,
1180
1189
 
1181
1190
  pbar.close()
1182
1191
 
1183
- # Retrieve parameters corresponding to the minimum gradient norm found
1184
- # during training
1192
+ # Retrieve parameters corresponding to the minimum gradient norm found during training
1185
1193
  trace_epoch = 50 + num_epoch - counter
1186
1194
  pos = num_epoch - trace_epoch + np.argmin(rec_jgrad_sum_norm[- trace_epoch: ])
1187
1195
  # print(pos)
@@ -1221,16 +1229,14 @@ def learn_program_regulators(gene_states: List[np.ndarray],
1221
1229
  num_round = len(gene_states)
1222
1230
 
1223
1231
  num_epoch, stepsz, rec_gap = (
1224
- train_dat.get(
1225
- key, None) for key in [
1232
+ train_dat.get(key, None) for key in [
1226
1233
  "num_epoch", "stepsz", "rec_gap"])
1227
1234
  list_step = np.arange(num_epoch, 0, - rec_gap)[::-1]
1228
1235
 
1229
1236
  save_path = train_dat.get('save_path', None)
1230
1237
 
1231
1238
  backtrack_gap, backtrack_tol = (
1232
- train_dat.get(
1233
- key, None) for key in [
1239
+ train_dat.get(key, None) for key in [
1234
1240
  "backtrack_gap", "backtrack_tol"])
1235
1241
 
1236
1242
  lambda_l1_j, lambda_l2_j = (train_dat.get(key, 0) for key in ["lambda_l1_j", "lambda_l2_j"])
@@ -1246,6 +1252,10 @@ def learn_program_regulators(gene_states: List[np.ndarray],
1246
1252
  samp_weight = samp_weight / np.sum(samp_weight)
1247
1253
 
1248
1254
  # Preallocate gradient norm recording (optional)
1255
+ rec_interaction_all = np.zeros((num_epoch, num_gene, num_program))
1256
+ rec_selfj_all = np.zeros((num_epoch, num_program))
1257
+ rec_selfh_all = np.zeros((num_epoch, num_program))
1258
+
1249
1259
  rec_grad = np.zeros(num_epoch)
1250
1260
 
1251
1261
  m_interaction, v_interaction = np.zeros_like(cur_interaction), np.zeros_like(cur_interaction)
@@ -1265,9 +1275,14 @@ def learn_program_regulators(gene_states: List[np.ndarray],
1265
1275
  rec_interaction_grad = np.zeros((num_round, num_gene, num_program))
1266
1276
  rec_selfj_grad = np.zeros((num_round, num_program))
1267
1277
  rec_selfh_grad = np.zeros((num_round, num_program))
1268
-
1278
+
1279
+ log_adam_grad = {name: deque(maxlen=backtrack_gap)
1280
+ for name in ["m_interaction", "v_interaction", "m_selfj", "v_selfj", "m_selfh", "v_selfh"]}
1281
+
1282
+ tqdm._instances.clear()
1269
1283
  pbar = tqdm(total=num_epoch)
1270
1284
  counter = 1
1285
+ backtrack_counter = 0
1271
1286
 
1272
1287
  while counter <= num_epoch:
1273
1288
 
@@ -1320,15 +1335,52 @@ def learn_program_regulators(gene_states: List[np.ndarray],
1320
1335
  update_val, m_selfh, v_selfh = update_adam(selfh_grad, m_selfh, v_selfh, counter, stepsz)
1321
1336
  cur_selfh = cur_selfh - update_val
1322
1337
 
1338
+ rec_interaction_all[counter - 1, :, :] = cur_interaction
1339
+ rec_selfj_all[counter - 1, :] = cur_selfj
1340
+ rec_selfh_all[counter - 1, :] = cur_selfh
1341
+
1342
+ for name, value in zip(["m_interaction", "v_interaction", "m_selfj", "v_selfj", "m_selfh", "v_selfh"], [m_interaction, v_interaction, m_selfj, v_selfj, m_selfh, v_selfh]):
1343
+ log_adam_grad[name].append(value)
1344
+
1323
1345
  # Record gradient norm (optional)
1324
1346
  rec_grad[counter - 1] = np.linalg.norm(interaction_grad)
1325
1347
 
1326
1348
  if counter in list_step:
1327
1349
  pbar.update(rec_gap)
1328
- pbar.set_postfix({"Network Gradient": f"{rec_grad[counter - 1]:.4f}"})
1350
+ pbar.set_postfix({"Network Gradient": f"{rec_grad[counter - 1]:.6f}"})
1351
+
1352
+ if_backtrack = False
1353
+ if (counter > backtrack_gap) and (rec_grad[counter - 1] > 1.5 * rec_grad[counter - 1 - backtrack_gap]):
1354
+ if_backtrack = True
1355
+
1356
+ if (counter > 2 * backtrack_gap) and (counter % backtrack_gap == 0):
1357
+ est_cur_grad = rec_grad[counter - 1 - backtrack_gap: counter - 1].mean()
1358
+ est_prev_grad = rec_grad[counter - 1 - 2 * backtrack_gap: counter - 1 - backtrack_gap].mean()
1359
+
1360
+ if est_cur_grad > 1.05 * est_prev_grad:
1361
+ if_backtrack = True
1329
1362
 
1330
- counter += 1
1363
+ if if_backtrack:
1364
+ print('Backtracking at epoch %d' % counter)
1365
+ backtrack_counter += 1
1366
+ m_interaction, v_interaction, m_selfj, v_selfj, m_selfh, v_selfh = [log_adam_grad[key][0] for key in ["m_interaction", "v_interaction", "m_selfj", "v_selfj", "m_selfh", "v_selfh"]]
1367
+ counter = counter - backtrack_gap
1368
+ stepsz = stepsz / 4
1369
+ if backtrack_counter > backtrack_tol:
1370
+ print(f'Backtracking more than {backtrack_tol} times, stop training.')
1371
+ break
1372
+ else:
1373
+ counter += 1
1331
1374
 
1375
+ pbar.close()
1376
+
1377
+ # Retrieve parameters corresponding to the minimum gradient norm found during training
1378
+ trace_epoch = 50 + num_epoch - counter
1379
+ pos = num_epoch - trace_epoch + np.argmin(rec_grad[- trace_epoch: ])
1380
+ cur_interaction = rec_interaction_all[pos, :, :]
1381
+ cur_selfj = rec_selfj_all[pos, :]
1382
+ cur_selfh = rec_selfh_all[pos, :]
1383
+
1332
1384
  return cur_interaction, cur_selfj, cur_selfh
1333
1385
 
1334
1386
 
@@ -35,6 +35,9 @@ import warnings
35
35
  import itertools
36
36
  from typing import List, Dict, Optional, Tuple, Union
37
37
  from scipy.stats import skew
38
+ import leidenalg as la
39
+ import igraph as ig
40
+ import matplotlib as mpl
38
41
 
39
42
 
40
43
  class AbstractDSPIN(ABC):
@@ -107,6 +110,18 @@ class AbstractDSPIN(ABC):
107
110
  def sample_list(self):
108
111
  return self._samp_list
109
112
 
113
+ @property
114
+ def modules(self):
115
+ return self._modules
116
+
117
+ @property
118
+ def name_list(self):
119
+ return self._name_list
120
+
121
+ @property
122
+ def name_list_short(self):
123
+ return self._name_list_short
124
+
110
125
  @program_representation_raw.setter
111
126
  def program_representation_raw(self, value):
112
127
  self._onmf_rep_ori = value
@@ -131,6 +146,18 @@ class AbstractDSPIN(ABC):
131
146
  def sample_list(self, value):
132
147
  self._samp_list = value
133
148
 
149
+ @modules.setter
150
+ def modules(self, value):
151
+ self._modules = value
152
+
153
+ @name_list.setter
154
+ def name_list(self, value):
155
+ self._name_list = value
156
+
157
+ @name_list_short.setter
158
+ def name_list_short(self, value):
159
+ self._name_list_short = value
160
+
134
161
  def discretize(self,
135
162
  clip_percentile: float = 100,
136
163
  num_init: int = 10) -> None:
@@ -201,11 +228,11 @@ class AbstractDSPIN(ABC):
201
228
  num_spin = self.num_spin
202
229
  raw_data = self._raw_data
203
230
 
204
- if self.example_list is not None:
205
- example_list_ind = [list(self._samp_list).index(samp)
206
- for samp in self.example_list]
207
- self._samp_list = self._samp_list[example_list_ind]
208
- raw_data = raw_data[example_list_ind]
231
+ if self.sample_list_ordered is not None:
232
+ sample_list_ordered_ind = [list(self._samp_list).index(samp)
233
+ for samp in self.sample_list_ordered]
234
+ self._samp_list = self._samp_list[sample_list_ordered_ind]
235
+ raw_data = raw_data[sample_list_ordered_ind]
209
236
  self._raw_data = raw_data
210
237
  print("Using a subset of {} samples for network inference.".format(len(self._samp_list)))
211
238
 
@@ -241,7 +268,9 @@ class AbstractDSPIN(ABC):
241
268
  method: str = 'auto',
242
269
  directed: bool = False,
243
270
  params: dict = None,
244
- example_list: List[str] = None,
271
+ sample_list_ordered: List[str] = None,
272
+ prior_network: np.ndarray = None,
273
+ perturb_matrix: np.ndarray = None,
245
274
  run_with_matlab: bool = False) -> None:
246
275
  """
247
276
  Perform network inference using a specified method and parameters.
@@ -256,8 +285,12 @@ class AbstractDSPIN(ABC):
256
285
  Whether to infer a directed network. Default is False.
257
286
  params : dict, optional
258
287
  Additional parameters for network inference. Default is None.
259
- example_list : List[str], optional
260
- List of example sample identifiers. Default is None.
288
+ sample_list_ordered : List[str], optional
289
+ List of ordered sample identifiers. Default is None.
290
+ prior_network : np.ndarray, optional
291
+ Binary matrix of preferred network connectivity from prior knowledge. Default is None.
292
+ perturb_matrix : np.ndarray, optional
293
+ Matrix of putative relative responses to control samples. Default is None.
261
294
  run_with_matlab : bool, optional
262
295
  If True, prepares data for MATLAB execution instead of running inference in Python. Default is False.
263
296
  """
@@ -275,8 +308,8 @@ class AbstractDSPIN(ABC):
275
308
  if method == 'auto':
276
309
  samp_list = np.unique(self.adata.obs[sample_id_key])
277
310
  num_sample = len(samp_list)
278
- if example_list is not None:
279
- num_sample = len(example_list)
311
+ if sample_list_ordered is not None:
312
+ num_sample = len(sample_list_ordered)
280
313
  if num_sample > 30:
281
314
  method = 'pseudo_likelihood'
282
315
  else:
@@ -296,9 +329,18 @@ class AbstractDSPIN(ABC):
296
329
  else:
297
330
  self.raw_data_corr(sample_id_key)
298
331
 
299
- self.example_list = example_list
332
+ self.sample_list_ordered = sample_list_ordered
300
333
 
301
334
  train_dat = self.default_params(method)
335
+
336
+ if prior_network is not None:
337
+ train_dat['j_prior_mask'] = prior_network
338
+ train_dat['lambda_l1_j_prior_mask'] = 0.01
339
+
340
+ if perturb_matrix is not None:
341
+ train_dat['perturb_matrix'] = perturb_matrix
342
+ train_dat['lambda_l2_h_rela_prior'] = 0.5
343
+
302
344
  train_dat['directed'] = directed
303
345
  if params is not None:
304
346
  train_dat.update(params)
@@ -347,6 +389,62 @@ class AbstractDSPIN(ABC):
347
389
 
348
390
  self._relative_responses = compute_relative_responses(self.responses, if_control, batch_index)
349
391
 
392
+ def compute_modules(self,
393
+ resolution: float = 1.0,
394
+ seed: int = 0,
395
+ thres: float = 0) -> List[List[int]]:
396
+ """
397
+ Compute network modules using Leiden community detection.
398
+
399
+ Parameters
400
+ ----------
401
+ resolution : float, optional
402
+ Resolution parameter for Leiden community detection. Higher values lead to more modules. Default is 1.0.
403
+ seed : int, optional
404
+ Random seed for Leiden community detection. Default is 0.
405
+ thres : float, optional
406
+ Threshold for filtering weak connections. Default is 0.
407
+
408
+ Returns
409
+ -------
410
+ List[List[int]]
411
+ List of modules, where each module is a list of node indices.
412
+ """
413
+ if not hasattr(self, '_network') or self._network is None:
414
+ raise ValueError("Network must be inferred before computing modules. Run network_inference() first.")
415
+
416
+ j_mat = self._network.copy()
417
+ np.fill_diagonal(j_mat, 0)
418
+
419
+ # Filter weak connections
420
+ j_filt = j_mat.copy()
421
+ j_filt[np.abs(j_mat) < thres] = 0
422
+ np.fill_diagonal(j_filt, 0)
423
+
424
+ # Create networkx graph
425
+ G = nx.from_numpy_array(j_filt)
426
+ G = ig.Graph.from_networkx(G)
427
+
428
+ # Separate positive and negative edges
429
+ G_pos = G.subgraph_edges(G.es.select(weight_gt=0), delete_vertices=False)
430
+ G_neg = G.subgraph_edges(G.es.select(weight_lt=0), delete_vertices=False)
431
+ G_neg.es['weight'] = [-w for w in G_neg.es['weight']]
432
+
433
+ # Apply Leiden community detection
434
+ part_pos = la.RBConfigurationVertexPartition(G_pos, weights='weight', resolution_parameter=resolution)
435
+ part_neg = la.RBConfigurationVertexPartition(G_neg, weights='weight', resolution_parameter=resolution)
436
+
437
+ optimiser = la.Optimiser()
438
+ optimiser.set_rng_seed(seed)
439
+
440
+ diff = optimiser.optimise_partition_multiplex([part_pos, part_neg], layer_weights=[1, -1])
441
+
442
+ # Extract modules
443
+ net_class = list(part_pos)
444
+ self._modules = net_class
445
+
446
+ print(f'Identified {len(net_class)} modules with sizes {np.array([len(cur_list) for cur_list in net_class])}')
447
+
350
448
 
351
449
  class GeneDSPIN(AbstractDSPIN):
352
450
  """
@@ -392,7 +490,6 @@ class GeneDSPIN(AbstractDSPIN):
392
490
 
393
491
  if gene_skew.mean() > 2:
394
492
  default_clip = 95
395
- print("Input gene expression seem to be heavy-tailed. Using 95 percentile for clipping before discretization.")
396
493
  else:
397
494
  default_clip = 100
398
495
 
@@ -400,13 +497,20 @@ class GeneDSPIN(AbstractDSPIN):
400
497
  'clip_percentile': default_clip,
401
498
  'num_init': 10}
402
499
  dparams.update(discretize_params)
403
-
500
+
501
+ self.name_list = self.adata.var_names.tolist()
502
+ self.name_list_short = self.adata.var_names.tolist()
503
+
404
504
  if dparams['use_default_discretize']:
505
+
506
+ if dparams['clip_percentile'] < 100:
507
+ print(f"Input gene expression seem to be heavy-tailed. Using {dparams['clip_percentile']} percentile for clipping before discretization.")
508
+
405
509
  self.discretize(clip_percentile=dparams['clip_percentile'],
406
510
  num_init=dparams['num_init'])
407
511
 
408
512
  def program_regulator_discovery(self,
409
- program_representation_raw: np.ndarray,
513
+ program_representation: np.ndarray,
410
514
  sample_id_key: str = 'sample_id',
411
515
  params: dict = None):
412
516
  """
@@ -414,7 +518,7 @@ class GeneDSPIN(AbstractDSPIN):
414
518
 
415
519
  Parameters
416
520
  ----------
417
- program_representation_raw : np.ndarray
521
+ program_representation : np.ndarray
418
522
  The gene program representation.
419
523
  sample_id_key : str, optional
420
524
  Column name in `adata.obs` specifying sample identifiers. Default is 'sample_id'.
@@ -422,7 +526,7 @@ class GeneDSPIN(AbstractDSPIN):
422
526
  Additional parameters for regression. Default is None.
423
527
  """
424
528
 
425
- program_states, samp_list = sample_states(self.adata.obs[sample_id_key], program_representation_raw)
529
+ program_states, samp_list = sample_states(self.adata.obs[sample_id_key], program_representation)
426
530
 
427
531
  gene_states, _ = sample_states(self.adata.obs[sample_id_key], self._onmf_rep_tri)
428
532
 
@@ -430,7 +534,9 @@ class GeneDSPIN(AbstractDSPIN):
430
534
  'num_epoch': 500,
431
535
  'stepsz': 0.01,
432
536
  'lambda_l1_interaction': 0.01,
433
- 'rec_gap': 10
537
+ 'rec_gap': 10,
538
+ 'backtrack_gap': 40,
539
+ 'backtrack_tol': 5
434
540
  }
435
541
 
436
542
  if params is not None:
@@ -609,9 +715,9 @@ class ProgramDSPIN(AbstractDSPIN):
609
715
  Parameters for discretization of the oNMF representation. Default is an empty dictionary.
610
716
  """
611
717
 
612
- if mode not in ['compute_summary', 'compute_only', 'summary_only']:
718
+ if mode not in ['compute_summary', 'compute_only', 'summary_only', 'precomputed']:
613
719
  raise ValueError(
614
- 'Mode must be one of compute_summary, compute_only, or summary_only')
720
+ 'Mode must be one of compute_summary, compute_only, summary_only, or precomputed.')
615
721
 
616
722
  adata = self.adata
617
723
  num_spin = self.num_spin
@@ -631,7 +737,7 @@ class ProgramDSPIN(AbstractDSPIN):
631
737
 
632
738
  if onmf_parameters['num_subsample_large'] is None:
633
739
  onmf_parameters['num_subsample_large'] = min(onmf_parameters['num_subsample'] * 5, adata.shape[0])
634
- print(onmf_parameters['num_subsample_large'])
740
+ # print(onmf_parameters['num_subsample_large'])
635
741
 
636
742
  if onmf_parameters['balance_method'] not in ['equal', 'proportional', 'squareroot', None]:
637
743
  raise ValueError('balance_method must be one of equal, proportional, squareroot, or None')
@@ -664,11 +770,11 @@ class ProgramDSPIN(AbstractDSPIN):
664
770
  self.prior_programs_ind = prior_program_ind
665
771
 
666
772
  num_onmf_components = onmf_parameters['num_onmf_components']
773
+
774
+ os.makedirs(self.save_path + 'onmf/', exist_ok=True)
667
775
 
668
776
  if mode == 'compute_summary' or mode == 'compute_only':
669
777
 
670
- os.makedirs(self.save_path + 'onmf/', exist_ok=True)
671
-
672
778
  cur_gene_matrix = adata.X[:, prior_program_mask]
673
779
  cluster_label_raw = adata.obs[cluster_key].values
674
780
  cluster_label = pd.factorize(cluster_label_raw)[0]
@@ -681,7 +787,7 @@ class ProgramDSPIN(AbstractDSPIN):
681
787
 
682
788
  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')
683
789
 
684
- if mode == 'compute_summary' or mode == 'summary_only':
790
+ if mode == 'compute_summary' or mode == 'summary_only' or mode == 'precomputed':
685
791
 
686
792
  cluster_label_raw = adata.obs[cluster_key].values
687
793
  cluster_label = pd.factorize(cluster_label_raw)[0]
@@ -689,20 +795,31 @@ class ProgramDSPIN(AbstractDSPIN):
689
795
  # Perform subsampling and standard deviation clipping on the matrix
690
796
  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)
691
797
  self.matrix_std = std_clipped
692
-
693
- # Summarize the oNMF decompositions
694
- if num_onmf_components > 0:
695
- onmf_summary = self.summarize_onmf_result(sub_large_gene_matrix_normed, num_onmf_components, num_repeat)
798
+
799
+ if mode != 'precomputed':
800
+ # Summarize the oNMF decompositions
801
+ if num_onmf_components > 0:
802
+ onmf_summary = self.summarize_onmf_result(sub_large_gene_matrix_normed, num_onmf_components, num_repeat)
803
+ else:
804
+ onmf_summary = self.compute_onmf_expression(sub_large_gene_matrix_normed, prior_program_ind)
805
+
806
+ onmf_save_path = self.save_path + f'onmf/onmf_summary_total_{num_spin}_onmf_{num_onmf_components}_prior_{len(prior_program_ind)}.npy'
807
+ np.save(onmf_save_path, onmf_summary)
808
+ print(f"Consensus oNMF results saved to {onmf_save_path}")
696
809
  else:
697
- onmf_summary = self.compute_onmf_expression(sub_large_gene_matrix_normed, prior_program_ind)
810
+ assert 'precomputed_onmf_path' in params.keys(), "For precomputed mode, 'precomputed_onmf_path' must be provided in params."
811
+ onmf_summary = np.load(params['precomputed_onmf_path'], allow_pickle=True).item()
698
812
 
699
813
  self._onmf_summary = onmf_summary
700
814
 
701
815
  # Save the gene programs to a CSV file
702
816
  file_path = onmf_to_csv(onmf_summary.components_, adata.var_names, self.save_path + f'gene_programs_total_{num_spin}_onmf_{num_onmf_components}_prior_{len(prior_program_ind)}.csv', thres=0.01)
703
- print('Gene programs saved to {}'.format(file_path))
817
+ print('Gene program information saved to {}'.format(file_path))
704
818
  self.gene_program_csv = file_path
705
819
 
820
+ file_wpath = onmf_to_csv(onmf_summary.components_, adata.var_names, self.save_path + f'gene_programs_total_{num_spin}_onmf_{num_onmf_components}_prior_{len(prior_program_ind)}_weights.csv', thres=0.01, if_write_weights=True)
821
+ # print('Gene programs saved to {}'.format(file_path))
822
+
706
823
  gene_matrix = self.adata.X
707
824
  if issparse(gene_matrix):
708
825
  gene_matrix = np.asarray(gene_matrix.toarray()).astype(np.float64)
@@ -717,6 +834,20 @@ class ProgramDSPIN(AbstractDSPIN):
717
834
  self.discretize(clip_percentile=dparams['clip_percentile'],
718
835
  num_init=dparams['num_init'])
719
836
 
837
+ num_display_gene = 5
838
+ gene_pd = pd.read_csv(self.gene_program_csv, index_col=None)
839
+
840
+ top_gene_list = [gene_pd.iloc[:, ii].tolist()[:num_display_gene] for ii in range(num_spin)]
841
+ # remove nan
842
+ top_gene_list_filtered = []
843
+ for sub_list in top_gene_list:
844
+ sub_list_filtered = [gene for gene in sub_list if str(gene) != 'nan']
845
+ top_gene_list_filtered.append(sub_list_filtered)
846
+
847
+ program_names = [f'P{ii}-' + ','.join(top_gene_list_filtered[ii]) for ii in range(num_spin)]
848
+ self.name_list = program_names
849
+ self.name_list_short = [name.split('-')[0] for name in program_names]
850
+
720
851
 
721
852
  class DSPIN(object):
722
853
  """
@@ -9,19 +9,18 @@ import matplotlib.patheffects as patheffects
9
9
  from sklearn.cluster import KMeans
10
10
  import numpy as np
11
11
  import pandas as pd
12
- import seaborn as sns
13
- import os
14
- import sys
15
12
  import csv
16
13
  import scanpy as sc
17
14
  import igraph as ig
18
15
  import leidenalg as la
19
16
  import networkx as nx
20
17
  from scipy import optimize
21
- from scipy.spatial.distance import pdist
18
+ from scipy.spatial.distance import pdist, squareform
19
+ import matplotlib as mpl
20
+ from typing import List, Dict, Optional, Tuple
22
21
 
23
22
 
24
- def onmf_to_csv(features, gene_name, file_path, thres=0.01):
23
+ def onmf_to_csv(features, gene_name, file_path, thres=0.01, if_write_weights=False):
25
24
  """
26
25
  Extract gene names in ONMF to CSV.
27
26
 
@@ -30,6 +29,7 @@ def onmf_to_csv(features, gene_name, file_path, thres=0.01):
30
29
  gene_name (List[str]): Names of the genes corresponding to feature indices.
31
30
  file_path (str): Path of the file where the CSV will be written.
32
31
  thres (float): Threshold value for filtering features.
32
+ if_write_weights (bool): If True, writes weights to the CSV file.
33
33
 
34
34
  Returns:
35
35
  str: Path of the created CSV file.
@@ -51,6 +51,11 @@ def onmf_to_csv(features, gene_name, file_path, thres=0.01):
51
51
  cur_line.insert(0, spin)
52
52
  write.writerow(cur_line) # Write the current line to the CSV file
53
53
 
54
+ if if_write_weights:
55
+ cur_line = [features[spin, ind].round(6) for ind in gene_ind]
56
+ cur_line.insert(0, f'Weights_{spin}')
57
+ write.writerow(cur_line)
58
+
54
59
  # Read the created CSV, transpose it and save it again
55
60
  pd_csv = pd.read_csv(file_path, names=range(rec_max_gene + 1))
56
61
  pd_csv = pd_csv.transpose()
@@ -551,3 +556,259 @@ def plot_final(
551
556
  ax.set_title(title, fontsize=node_fontsize * 1.5, y = 1.05)
552
557
 
553
558
 
559
+ def plot_module_heatmap(j_mat: np.ndarray,
560
+ modules: List[List[int]],
561
+ spin_name_list: Optional[List[str]] = None,
562
+ fig_folder: Optional[str] = None) -> None:
563
+ """
564
+ Plot a heatmap of the network adjacency matrix with modules highlighted.
565
+
566
+ Parameters
567
+ ----------
568
+ j_mat : np.ndarray
569
+ The network adjacency matrix.
570
+ modules : List[List[int]]
571
+ List of modules, where each module is a list of node indices.
572
+ spin_name_list : List[str], optional
573
+ List of names for the spins/nodes. If None, will use generic names.
574
+ fig_folder : str, optional
575
+ Folder path to save the figure. If None, will not save the figure.
576
+ """
577
+ num_spin = j_mat.shape[0]
578
+ j_mat = j_mat.copy()
579
+ np.fill_diagonal(j_mat, 0)
580
+
581
+ # Create spin order based on modules
582
+ spin_order = [spin for cur_list in modules for spin in cur_list]
583
+ net_class_len = [len(cur_list) for cur_list in modules]
584
+
585
+ # Create custom colormap
586
+ cmap_hvec = mpl.colors.LinearSegmentedColormap.from_list("", ['#E84B23', '#FFFFFF', '#3285CC'])
587
+
588
+ # Plot heatmap
589
+ sc.set_figure_params(figsize=[0.22 * num_spin + 0.5, 0.22 * num_spin])
590
+ plt.imshow(j_mat[:, spin_order][spin_order, :], cmap=cmap_hvec, vmin=-1, vmax=1)
591
+
592
+ # Add module boundaries
593
+ for ii in range(len(net_class_len)):
594
+ plt.axhline(np.sum(net_class_len[:ii]) - 0.5, color='k', linewidth=1)
595
+ plt.axvline(np.sum(net_class_len[:ii]) - 0.5, color='k', linewidth=1)
596
+
597
+ # Set labels
598
+ if spin_name_list is not None:
599
+ plt.xticks(np.arange(len(spin_order)), np.array(spin_name_list)[spin_order], rotation=90, fontsize=10)
600
+ plt.yticks(np.arange(len(spin_order)), np.array(spin_name_list)[spin_order], fontsize=10)
601
+ else:
602
+ plt.xticks(np.arange(len(spin_order)), [f'P{spin}' for spin in spin_order], rotation=90, fontsize=10)
603
+ plt.yticks(np.arange(len(spin_order)), [f'P{spin}' for spin in spin_order], fontsize=10)
604
+
605
+ plt.colorbar(fraction=0.03)
606
+ plt.grid()
607
+ plt.title(f'Network on {num_spin} programs', fontsize=12)
608
+
609
+ # Save figure if folder is provided
610
+ if fig_folder is not None:
611
+ plt.savefig(fig_folder + 'module_heatmap.png', dpi=300, bbox_inches='tight')
612
+
613
+ plt.show()
614
+
615
+
616
+
617
+ def layout_loss_fun(xx, ori_pos, network):
618
+
619
+ stength_module = 12
620
+ strength_repulsion = 12
621
+ strength_spring = 1
622
+
623
+ spring_length = 2
624
+ spring_weight = np.abs(network)
625
+
626
+ module_length = 1
627
+
628
+ xx = xx.reshape(- 1, 2)
629
+ module_dist = np.clip(np.sqrt(((xx - ori_pos) ** 2).sum(axis=1)) - module_length, 0, None)
630
+ module_attract = np.sum(module_dist ** 2)
631
+ repulse = np.sum(1 / pdist(xx))
632
+
633
+ pairwise_distance = squareform(pdist(xx))
634
+ clip_dist = np.clip(pairwise_distance - spring_length, 0, None)
635
+ spring_energy = np.sum(clip_dist ** 2 * spring_weight)
636
+
637
+ return stength_module * module_attract + strength_repulsion * repulse + strength_spring * spring_energy
638
+
639
+ def compute_module_angle(module_size, gap_size):
640
+
641
+ module_plots_size = np.sqrt(module_size)
642
+
643
+ angle_list = []
644
+ cur_angle = 0
645
+
646
+ for ii in range(len(module_plots_size)):
647
+ angle_list.append(cur_angle)
648
+ cur_angle += module_plots_size[ii] + gap_size
649
+
650
+ angle_list = np.array(angle_list)
651
+ angle_list /= (cur_angle / (2 * np.pi))
652
+
653
+ return angle_list
654
+
655
+ def compute_pos_from_module(network, net_class, circle_size=None, module_angle=None, gap_size=0.5):
656
+
657
+ module_size = np.array([len(cur_list) for cur_list in net_class])
658
+ num_spin = np.sum(module_size)
659
+
660
+ if module_angle is None:
661
+ module_angle = compute_module_angle(module_size, gap_size)
662
+
663
+ if circle_size is None:
664
+ circle_size = num_spin ** 0.3
665
+
666
+ module_center = circle_size * np.array([np.cos(module_angle), np.sin(module_angle)]).T
667
+
668
+ pos_module = np.zeros((num_spin, 2))
669
+
670
+ for ii in range(len(module_size)):
671
+ cur_list = net_class[ii]
672
+ pos_module[cur_list, :] = module_center[ii, :]
673
+
674
+ pos_module += np.random.normal(0, 0.05, pos_module.shape)
675
+
676
+ ori_pos = pos_module.copy()
677
+
678
+ opt_res = optimize.minimize(lambda xx : layout_loss_fun(xx, ori_pos, network), pos_module.flatten())
679
+ pos_raw = opt_res.x.reshape(- 1, 2)
680
+
681
+ pos = {ii: pos_raw[ii] for ii in range(num_spin)}
682
+
683
+ return pos
684
+
685
+
686
+ def compute_network_positions(j_mat: np.ndarray,
687
+ modules: List[List[int]])-> Dict[int, Tuple[float, float]]:
688
+ """
689
+ Compute positions for network nodes based on modules.
690
+
691
+ Parameters
692
+ ----------
693
+ j_mat : np.ndarray
694
+ The network adjacency matrix.
695
+ modules : List[List[int]]
696
+ List of modules, where each module is a list of node indices.
697
+
698
+ Returns
699
+ -------
700
+ Dict[int, Tuple[float, float]]
701
+ Dictionary mapping node indices to (x, y) positions.
702
+ """
703
+
704
+ pos = compute_pos_from_module(j_mat, modules)
705
+
706
+ return pos
707
+
708
+
709
+ def plot_network_diagram(j_mat: np.ndarray,
710
+ modules: List[List[int]],
711
+ pos: Optional[Dict[int, Tuple[float, float]]] = None,
712
+ spin_name_list_short: Optional[List[str]] = None,
713
+ spin_name_list: Optional[List[str]] = None,
714
+ fig_folder: Optional[str] = None,
715
+ num_spin: Optional[int] = None) -> None:
716
+ """
717
+ Plot the network diagram with modules highlighted.
718
+
719
+ Parameters
720
+ ----------
721
+ j_mat : np.ndarray
722
+ The network adjacency matrix.
723
+ modules : List[List[int]]
724
+ List of modules, where each module is a list of node indices.
725
+ pos : Dict[int, Tuple[float, float]], optional
726
+ Dictionary mapping node indices to (x, y) positions. If None, positions will be computed automatically.
727
+ spin_name_list_short : List[str], optional
728
+ Short names for the spins/nodes (used for labels). If None, will use generic names.
729
+ spin_name_list : List[str], optional
730
+ Full names for the spins/nodes (used for tooltips). If None, will use generic names.
731
+ fig_folder : str, optional
732
+ Folder path to save the figure. If None, will not save the figure.
733
+ num_spin : int, optional
734
+ Number of spins. If None, will be inferred from j_mat shape.
735
+ """
736
+ if num_spin is None:
737
+ num_spin = j_mat.shape[0]
738
+
739
+ # Filter network for visualization
740
+ thres = 0.2
741
+ j_filt = j_mat.copy()
742
+ j_filt[np.abs(j_mat) < thres] = 0
743
+ np.fill_diagonal(j_filt, 0)
744
+
745
+ # Create networkx graph
746
+ G = nx.from_numpy_array(j_filt)
747
+
748
+ # Compute positions
749
+ if pos is None:
750
+ pos = compute_network_positions(j_mat, modules)
751
+
752
+ # Create labels
753
+ if spin_name_list_short is not None:
754
+ label = {ii: spin_name_list_short[ii] for ii in range(num_spin)}
755
+ else:
756
+ label = {ii: f'P{ii}' for ii in range(num_spin)}
757
+
758
+ # Plot network
759
+ fig_size = 3 * num_spin ** 0.3
760
+ sc.set_figure_params(figsize=[fig_size, fig_size])
761
+ fig, ax = plt.subplots()
762
+
763
+ # Draw edges
764
+ eposi = [(u, v) for (u, v, d) in G.edges(data=True) if d['weight'] > 0]
765
+ wposi = np.array([d['weight'] for (u, v, d) in G.edges(data=True) if d['weight'] > 0])
766
+
767
+ enega = [(u, v) for (u, v, d) in G.edges(data=True) if d['weight'] < 0]
768
+ wnega = np.array([d['weight'] for (u, v, d) in G.edges(data=True) if d['weight'] < 0])
769
+
770
+ # Draw positive edges
771
+ if len(eposi) > 0:
772
+ nx.draw_networkx_edges(G, pos, ax=ax, edgelist=eposi, width=3 * wposi,
773
+ edge_color='#3285CC', alpha=0.7)
774
+
775
+ # Draw negative edges
776
+ if len(enega) > 0:
777
+ nx.draw_networkx_edges(G, pos, ax=ax, edgelist=enega, width=3 * (-wnega),
778
+ edge_color='#E84B23', alpha=0.7)
779
+
780
+ # Draw nodes
781
+ node_colors = ['#f0dab1'] * num_spin
782
+ nx.draw_networkx_nodes(G, pos, ax=ax, node_size=300, node_color=node_colors,
783
+ edgecolors='black', linewidths=1)
784
+
785
+ # Draw labels
786
+ for spin in range(num_spin):
787
+ plt.text(pos[spin][0], pos[spin][1], label[spin], fontsize=10,
788
+ path_effects=[patheffects.withStroke(linewidth=3, foreground='w')],
789
+ ha='center', va='center')
790
+
791
+ ax.set_aspect('equal')
792
+ ax.set_axis_off()
793
+ plt.title('Network Diagram with Modules', fontsize=12)
794
+
795
+ # Print the spin_name_list in a column to the right of the figure
796
+ if spin_name_list is not None:
797
+ # Place the text to the right of the plot
798
+ # Get axis limits to determine placement
799
+ xlim = ax.get_xlim()
800
+ ylim = ax.get_ylim()
801
+ # Place the column at a little to the right of the rightmost node
802
+ x_text = xlim[1] + 0.02 * (xlim[1] - xlim[0])
803
+ y_start = ylim[1] - 0.05 * (ylim[1] - ylim[0])
804
+ line_gap = fig_size / 20
805
+ for idx, name in enumerate(spin_name_list):
806
+ ax.text(x_text, y_start - idx * line_gap, name, fontsize=10, va='top', ha='left')
807
+ # Optionally, expand the xlim to make space for the text
808
+ ax.set_xlim(xlim[0], x_text + 1.5)
809
+
810
+ # Save figure if folder is provided
811
+ if fig_folder is not None:
812
+ plt.savefig(fig_folder + 'network_diagram.png', dpi=300, bbox_inches='tight')
813
+
814
+ plt.show()
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: dspin
3
- Version: 1.3.4
3
+ Version: 1.4.0
4
4
  Summary: Regulatory network models from single-cell perturbation profiling
5
5
  Home-page: https://github.com/JialongJiang/DSPIN
6
6
  Author: Jialong Jiang
@@ -25,6 +25,18 @@ Requires-Dist: scanpy
25
25
  Requires-Dist: tqdm
26
26
  Requires-Dist: igraph
27
27
  Requires-Dist: leidenalg
28
+ Dynamic: author
29
+ Dynamic: author-email
30
+ Dynamic: classifier
31
+ Dynamic: description
32
+ Dynamic: description-content-type
33
+ Dynamic: home-page
34
+ Dynamic: keywords
35
+ Dynamic: license
36
+ Dynamic: license-file
37
+ Dynamic: requires-dist
38
+ Dynamic: requires-python
39
+ Dynamic: summary
28
40
 
29
41
  # D-SPIN: Constructing regulatory network models from scRNA-seq perturbation profiling
30
42
 
@@ -21,7 +21,7 @@ install_requires = [
21
21
  # ]
22
22
 
23
23
  # Managing your version (adjust accordingly)
24
- version = "1.3.4" # You can manage versioning in a more sophisticated way if needed
24
+ version = "1.4.0" # 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
@@ -1,5 +0,0 @@
1
-
2
- from . import annotate
3
- from . import compute
4
- from . import plot
5
- from . import dspin
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes