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.
- {dspin-1.3.4/dspin.egg-info → dspin-1.4.0}/PKG-INFO +14 -2
- dspin-1.4.0/dspin/__init__.py +18 -0
- {dspin-1.3.4 → dspin-1.4.0}/dspin/compute.py +105 -53
- {dspin-1.3.4 → dspin-1.4.0}/dspin/dspin.py +160 -29
- {dspin-1.3.4 → dspin-1.4.0}/dspin/plot.py +266 -5
- {dspin-1.3.4 → dspin-1.4.0/dspin.egg-info}/PKG-INFO +14 -2
- {dspin-1.3.4 → dspin-1.4.0}/setup.py +1 -1
- dspin-1.3.4/dspin/__init__.py +0 -5
- {dspin-1.3.4 → dspin-1.4.0}/LICENSE +0 -0
- {dspin-1.3.4 → dspin-1.4.0}/README.md +0 -0
- {dspin-1.3.4 → dspin-1.4.0}/__pycache__/__init__.cpython-310.pyc +0 -0
- {dspin-1.3.4 → dspin-1.4.0}/__pycache__/__init__.cpython-37.pyc +0 -0
- {dspin-1.3.4 → dspin-1.4.0}/__pycache__/compute.cpython-310.pyc +0 -0
- {dspin-1.3.4 → dspin-1.4.0}/__pycache__/compute.cpython-37.pyc +0 -0
- {dspin-1.3.4 → dspin-1.4.0}/__pycache__/dspin_abstract.cpython-310.pyc +0 -0
- {dspin-1.3.4 → dspin-1.4.0}/__pycache__/plot.cpython-310.pyc +0 -0
- {dspin-1.3.4 → dspin-1.4.0}/__pycache__/plotting.cpython-37.pyc +0 -0
- {dspin-1.3.4 → dspin-1.4.0}/dspin/annotate.py +0 -0
- {dspin-1.3.4 → dspin-1.4.0}/dspin.egg-info/SOURCES.txt +0 -0
- {dspin-1.3.4 → dspin-1.4.0}/dspin.egg-info/dependency_links.txt +0 -0
- {dspin-1.3.4 → dspin-1.4.0}/dspin.egg-info/requires.txt +0 -0
- {dspin-1.3.4 → dspin-1.4.0}/dspin.egg-info/top_level.txt +0 -0
- {dspin-1.3.4 → dspin-1.4.0}/requirements.txt +0 -0
- {dspin-1.3.4 → dspin-1.4.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: dspin
|
|
3
|
-
Version: 1.
|
|
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 %
|
|
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 −
|
|
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
|
-
|
|
832
|
-
|
|
833
|
-
|
|
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
|
-
|
|
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
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
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]:.
|
|
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
|
-
|
|
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.
|
|
205
|
-
|
|
206
|
-
for samp in self.
|
|
207
|
-
self._samp_list = self._samp_list[
|
|
208
|
-
raw_data = raw_data[
|
|
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
|
-
|
|
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
|
-
|
|
260
|
-
List of
|
|
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
|
|
279
|
-
num_sample = len(
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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],
|
|
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
|
|
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
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: dspin
|
|
3
|
-
Version: 1.
|
|
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.
|
|
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
|
dspin-1.3.4/dspin/__init__.py
DELETED
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|