dspin 1.4.0__tar.gz → 1.4.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.
- {dspin-1.4.0/dspin.egg-info → dspin-1.4.2}/PKG-INFO +1 -1
- {dspin-1.4.0 → dspin-1.4.2}/README.md +6 -2
- {dspin-1.4.0 → dspin-1.4.2}/dspin/__init__.py +4 -1
- {dspin-1.4.0 → dspin-1.4.2}/dspin/dspin.py +24 -66
- {dspin-1.4.0 → dspin-1.4.2}/dspin/plot.py +280 -318
- {dspin-1.4.0 → dspin-1.4.2/dspin.egg-info}/PKG-INFO +1 -1
- {dspin-1.4.0 → dspin-1.4.2}/setup.py +1 -1
- {dspin-1.4.0 → dspin-1.4.2}/LICENSE +0 -0
- {dspin-1.4.0 → dspin-1.4.2}/__pycache__/__init__.cpython-310.pyc +0 -0
- {dspin-1.4.0 → dspin-1.4.2}/__pycache__/__init__.cpython-37.pyc +0 -0
- {dspin-1.4.0 → dspin-1.4.2}/__pycache__/compute.cpython-310.pyc +0 -0
- {dspin-1.4.0 → dspin-1.4.2}/__pycache__/compute.cpython-37.pyc +0 -0
- {dspin-1.4.0 → dspin-1.4.2}/__pycache__/dspin_abstract.cpython-310.pyc +0 -0
- {dspin-1.4.0 → dspin-1.4.2}/__pycache__/plot.cpython-310.pyc +0 -0
- {dspin-1.4.0 → dspin-1.4.2}/__pycache__/plotting.cpython-37.pyc +0 -0
- {dspin-1.4.0 → dspin-1.4.2}/dspin/annotate.py +0 -0
- {dspin-1.4.0 → dspin-1.4.2}/dspin/compute.py +0 -0
- {dspin-1.4.0 → dspin-1.4.2}/dspin.egg-info/SOURCES.txt +0 -0
- {dspin-1.4.0 → dspin-1.4.2}/dspin.egg-info/dependency_links.txt +0 -0
- {dspin-1.4.0 → dspin-1.4.2}/dspin.egg-info/requires.txt +0 -0
- {dspin-1.4.0 → dspin-1.4.2}/dspin.egg-info/top_level.txt +0 -0
- {dspin-1.4.0 → dspin-1.4.2}/requirements.txt +0 -0
- {dspin-1.4.0 → dspin-1.4.2}/setup.cfg +0 -0
|
@@ -18,6 +18,8 @@ The MATLAB code can be downloaded from the folder "DSPIN_matlab", and directly e
|
|
|
18
18
|
|
|
19
19
|
## Demos
|
|
20
20
|
|
|
21
|
+
A tutorial intended for an invited book chapter is available at [bioRxiv](https://www.biorxiv.org/content/10.1101/2025.07.28.667326).
|
|
22
|
+
|
|
21
23
|
Two demos of D-SPIN are available on Google Colab.
|
|
22
24
|
|
|
23
25
|
The first demo reconstructs the regulatory network of simulated hematopoietic stem cell (HSC) differentiation network with perturbations using the BEELINE framework (Pratapa, Aditya, et al. Nature methods, 2020).
|
|
@@ -170,6 +172,8 @@ model_gene.program_regulator_discovery(
|
|
|
170
172
|
|
|
171
173
|
1. Jiang, Jialong, et al. "D-SPIN constructs gene regulatory network models from multiplexed scRNA-seq data revealing organizing principles of cellular perturbation response." bioRxiv (2023).
|
|
172
174
|
|
|
173
|
-
2.
|
|
175
|
+
2. Jiang, Jialong, and Thomson, Matt. "A guide to D-SPIN: constructing regulatory network models from single-cell RNA-seq perturbation data." bioRxiv (2025).
|
|
176
|
+
|
|
177
|
+
3. Pratapa, Aditya, et al. "Benchmarking algorithms for gene regulatory network inference from single-cell transcriptomic data." Nature methods 17.2 (2020): 147-154.
|
|
174
178
|
|
|
175
|
-
|
|
179
|
+
4. Cui, Ang, et al. "Dictionary of immune responses to cytokines at single-cell resolution." Nature 625.7994 (2024): 377-384.
|
|
@@ -3,6 +3,7 @@ from . import annotate
|
|
|
3
3
|
from . import compute
|
|
4
4
|
from . import plot
|
|
5
5
|
from . import dspin
|
|
6
|
+
from importlib.metadata import version
|
|
6
7
|
|
|
7
8
|
# Import main classes for easy access
|
|
8
9
|
from .dspin import DSPIN, GeneDSPIN, ProgramDSPIN, AbstractDSPIN
|
|
@@ -15,4 +16,6 @@ __all__ = [
|
|
|
15
16
|
'annotate',
|
|
16
17
|
'compute',
|
|
17
18
|
'plot'
|
|
18
|
-
]
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
__version__ = version("DSPIN")
|
|
@@ -109,10 +109,6 @@ class AbstractDSPIN(ABC):
|
|
|
109
109
|
@property
|
|
110
110
|
def sample_list(self):
|
|
111
111
|
return self._samp_list
|
|
112
|
-
|
|
113
|
-
@property
|
|
114
|
-
def modules(self):
|
|
115
|
-
return self._modules
|
|
116
112
|
|
|
117
113
|
@property
|
|
118
114
|
def name_list(self):
|
|
@@ -146,10 +142,6 @@ class AbstractDSPIN(ABC):
|
|
|
146
142
|
def sample_list(self, value):
|
|
147
143
|
self._samp_list = value
|
|
148
144
|
|
|
149
|
-
@modules.setter
|
|
150
|
-
def modules(self, value):
|
|
151
|
-
self._modules = value
|
|
152
|
-
|
|
153
145
|
@name_list.setter
|
|
154
146
|
def name_list(self, value):
|
|
155
147
|
self._name_list = value
|
|
@@ -234,7 +226,7 @@ class AbstractDSPIN(ABC):
|
|
|
234
226
|
self._samp_list = self._samp_list[sample_list_ordered_ind]
|
|
235
227
|
raw_data = raw_data[sample_list_ordered_ind]
|
|
236
228
|
self._raw_data = raw_data
|
|
237
|
-
print("Using a subset of {} samples for network inference.".format(len(self._samp_list)))
|
|
229
|
+
print("Using a ordered subset of {} samples for network inference.".format(len(self._samp_list)))
|
|
238
230
|
|
|
239
231
|
num_sample = len(raw_data)
|
|
240
232
|
params = {'num_epoch': 400,
|
|
@@ -271,6 +263,8 @@ class AbstractDSPIN(ABC):
|
|
|
271
263
|
sample_list_ordered: List[str] = None,
|
|
272
264
|
prior_network: np.ndarray = None,
|
|
273
265
|
perturb_matrix: np.ndarray = None,
|
|
266
|
+
if_control_key: str = 'if_control',
|
|
267
|
+
batch_key: str = 'batch',
|
|
274
268
|
run_with_matlab: bool = False) -> None:
|
|
275
269
|
"""
|
|
276
270
|
Perform network inference using a specified method and parameters.
|
|
@@ -291,6 +285,10 @@ class AbstractDSPIN(ABC):
|
|
|
291
285
|
Binary matrix of preferred network connectivity from prior knowledge. Default is None.
|
|
292
286
|
perturb_matrix : np.ndarray, optional
|
|
293
287
|
Matrix of putative relative responses to control samples. Default is None.
|
|
288
|
+
if_control_key : str, optional
|
|
289
|
+
Column name in `adata.obs` indicating whether a sample is a control. Default is 'if_control'.
|
|
290
|
+
batch_key : str, optional
|
|
291
|
+
Column name in `adata.obs` specifying batch assignment for each sample. Default is 'batch'.
|
|
294
292
|
run_with_matlab : bool, optional
|
|
295
293
|
If True, prepares data for MATLAB execution instead of running inference in Python. Default is False.
|
|
296
294
|
"""
|
|
@@ -338,7 +336,21 @@ class AbstractDSPIN(ABC):
|
|
|
338
336
|
train_dat['lambda_l1_j_prior_mask'] = 0.01
|
|
339
337
|
|
|
340
338
|
if perturb_matrix is not None:
|
|
339
|
+
if if_control_key not in self.adata.obs.columns:
|
|
340
|
+
raise ValueError(f"Column '{if_control_key}' not found in adata.obs.")
|
|
341
|
+
if batch_key not in self.adata.obs.columns:
|
|
342
|
+
raise ValueError(f"Column '{batch_key}' not found in adata.obs.")
|
|
343
|
+
|
|
344
|
+
sample_list = self._samp_list
|
|
345
|
+
sample_to_control = self.adata.obs.groupby(sample_id_key, observed=False)[if_control_key].first().to_dict()
|
|
346
|
+
sample_to_batch = self.adata.obs.groupby(sample_id_key, observed=False)[batch_key].first().to_dict()
|
|
347
|
+
|
|
348
|
+
if_control = np.array([sample_to_control[sample] for sample in sample_list])
|
|
349
|
+
batch_index = np.array([sample_to_batch[sample] for sample in sample_list])
|
|
350
|
+
|
|
341
351
|
train_dat['perturb_matrix'] = perturb_matrix
|
|
352
|
+
train_dat['if_control'] = if_control
|
|
353
|
+
train_dat['batch_index'] = batch_index
|
|
342
354
|
train_dat['lambda_l2_h_rela_prior'] = 0.5
|
|
343
355
|
|
|
344
356
|
train_dat['directed'] = directed
|
|
@@ -389,62 +401,6 @@ class AbstractDSPIN(ABC):
|
|
|
389
401
|
|
|
390
402
|
self._relative_responses = compute_relative_responses(self.responses, if_control, batch_index)
|
|
391
403
|
|
|
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
|
-
|
|
448
404
|
|
|
449
405
|
class GeneDSPIN(AbstractDSPIN):
|
|
450
406
|
"""
|
|
@@ -488,7 +444,9 @@ class GeneDSPIN(AbstractDSPIN):
|
|
|
488
444
|
cur_gene = self._onmf_rep_ori[:, ii]
|
|
489
445
|
gene_skew[ii] = skew(cur_gene, bias=False)
|
|
490
446
|
|
|
491
|
-
|
|
447
|
+
print(f'Skewness of the gene expression: {gene_skew.mean().round(3)}')
|
|
448
|
+
|
|
449
|
+
if gene_skew.mean() >= 1.5:
|
|
492
450
|
default_clip = 95
|
|
493
451
|
else:
|
|
494
452
|
default_clip = 100
|
|
@@ -16,6 +16,7 @@ import leidenalg as la
|
|
|
16
16
|
import networkx as nx
|
|
17
17
|
from scipy import optimize
|
|
18
18
|
from scipy.spatial.distance import pdist, squareform
|
|
19
|
+
from scipy.cluster.hierarchy import linkage, leaves_list
|
|
19
20
|
import matplotlib as mpl
|
|
20
21
|
from typing import List, Dict, Optional, Tuple
|
|
21
22
|
|
|
@@ -146,6 +147,7 @@ def assign_program_position(onmf_rep_ori, umap_all, repulsion=2):
|
|
|
146
147
|
|
|
147
148
|
return program_umap_pos
|
|
148
149
|
|
|
150
|
+
|
|
149
151
|
def gene_program_on_umap(onmf_rep, umap_all, program_umap_pos, fig_folder=None, subsample=True):
|
|
150
152
|
"""
|
|
151
153
|
Plot gene programs on the UMAP plot.
|
|
@@ -265,6 +267,7 @@ def visualize_program_expression(onmf_summary, spin_name, gene_matrix, onmf_rep_
|
|
|
265
267
|
plt.savefig(fig_folder + 'gene_program_decomposition.png', bbox_inches='tight')
|
|
266
268
|
return fig_folder + 'gene_program_decomposition.png'
|
|
267
269
|
|
|
270
|
+
|
|
268
271
|
def format_label(label):
|
|
269
272
|
"""
|
|
270
273
|
Format the label string by inserting a newline character after every second word.
|
|
@@ -286,6 +289,7 @@ def format_label(label):
|
|
|
286
289
|
|
|
287
290
|
return '_'.join(parts)
|
|
288
291
|
|
|
292
|
+
|
|
289
293
|
def temporary_spin_name(csv_file, num_gene: int = 4):
|
|
290
294
|
"""
|
|
291
295
|
Create temporary spin names based on the CSV file content.
|
|
@@ -304,307 +308,122 @@ def temporary_spin_name(csv_file, num_gene: int = 4):
|
|
|
304
308
|
spin_names_formatted = [format_label(name) for name in spin_names]
|
|
305
309
|
return spin_names_formatted
|
|
306
310
|
|
|
307
|
-
def spin_order_in_cluster(j_mat, resolution_parameter: float = 2):
|
|
308
|
-
""" Determine the order of spins in the cluster.
|
|
309
311
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
312
|
+
def plot_network_heatmap(j_mat: np.ndarray,
|
|
313
|
+
module_list: List[List[int]] = None,
|
|
314
|
+
spin_name_list: Optional[List[str]] = None,
|
|
315
|
+
fig_folder: Optional[str] = None) -> None:
|
|
316
|
+
"""
|
|
317
|
+
Plot a heatmap of the network adjacency matrix with modules highlighted.
|
|
318
|
+
|
|
319
|
+
Parameters
|
|
320
|
+
----------
|
|
321
|
+
j_mat : np.ndarray
|
|
322
|
+
The network adjacency matrix.
|
|
323
|
+
modules : List[List[int]]
|
|
324
|
+
List of modules, where each module is a list of node indices.
|
|
325
|
+
spin_name_list : List[str], optional
|
|
326
|
+
List of names for the spins/nodes. If None, will use generic names.
|
|
327
|
+
fig_folder : str, optional
|
|
328
|
+
Folder path to save the figure. If None, will not save the figure.
|
|
316
329
|
"""
|
|
330
|
+
num_spin = j_mat.shape[0]
|
|
331
|
+
j_mat = j_mat.copy()
|
|
317
332
|
np.fill_diagonal(j_mat, 0)
|
|
333
|
+
|
|
334
|
+
if module_list is None:
|
|
335
|
+
module_list = [list(np.arange(num_spin))]
|
|
318
336
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
G.es.select(
|
|
328
|
-
weight_gt=0),
|
|
329
|
-
delete_vertices=False)
|
|
330
|
-
G_neg = G.subgraph_edges(
|
|
331
|
-
G.es.select(
|
|
332
|
-
weight_lt=0),
|
|
333
|
-
delete_vertices=False)
|
|
334
|
-
G_neg.es['weight'] = [-w for w in G_neg.es['weight']]
|
|
335
|
-
|
|
336
|
-
part_pos = la.RBConfigurationVertexPartition(
|
|
337
|
-
G_pos, weights='weight', resolution_parameter=resolution_parameter)
|
|
338
|
-
part_neg = la.RBConfigurationVertexPartition(
|
|
339
|
-
G_neg, weights='weight', resolution_parameter=resolution_parameter)
|
|
340
|
-
optimiser = la.Optimiser()
|
|
341
|
-
diff = optimiser.optimise_partition_multiplex(
|
|
342
|
-
[part_pos, part_neg], layer_weights=[1, -1])
|
|
343
|
-
|
|
344
|
-
net_class = list(part_pos)
|
|
345
|
-
spin_order = [spin for cur_list in net_class for spin in cur_list]
|
|
346
|
-
net_class_len = [len(cur_list) for cur_list in net_class]
|
|
347
|
-
|
|
348
|
-
start_angle = 0 * np.pi
|
|
349
|
-
end_angle = 2 * np.pi
|
|
350
|
-
gap_size = 2
|
|
337
|
+
# Create spin order based on modules
|
|
338
|
+
spin_order = [spin for cur_list in module_list for spin in cur_list]
|
|
339
|
+
net_class_len = [len(cur_list) for cur_list in module_list]
|
|
340
|
+
|
|
341
|
+
# Create custom colormap
|
|
342
|
+
cmap_hvec = mpl.colors.LinearSegmentedColormap.from_list("", ['#E84B23', '#FFFFFF', '#3285CC'])
|
|
343
|
+
|
|
344
|
+
color_thres = np.percentile(np.abs(j_mat[j_mat != 0]), 95)
|
|
351
345
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
# angle_list = np.linspace(start_angle, end_angle, len(leiden_list) + 1)
|
|
346
|
+
# Plot heatmap
|
|
347
|
+
sc.set_figure_params(figsize=[0.18 * num_spin + 0.5, 0.18 * num_spin])
|
|
348
|
+
plt.imshow(j_mat[:, spin_order][spin_order, :], cmap=cmap_hvec, vmin=- color_thres, vmax=color_thres)
|
|
349
|
+
|
|
350
|
+
# Add module boundaries
|
|
358
351
|
for ii in range(len(net_class_len)):
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
pert_dist = 3
|
|
363
|
-
|
|
364
|
-
pert_pos = np.array(
|
|
365
|
-
[- pert_dist * np.cos(angle_list), pert_dist * np.sin(angle_list)]).T
|
|
366
|
-
|
|
367
|
-
return spin_order, pert_pos
|
|
368
|
-
|
|
369
|
-
def plot_network(
|
|
370
|
-
G,
|
|
371
|
-
j_mat,
|
|
372
|
-
ax,
|
|
373
|
-
pos: None,
|
|
374
|
-
nodesz=1,
|
|
375
|
-
linewz=1,
|
|
376
|
-
node_color='k'):
|
|
377
|
-
"""
|
|
378
|
-
Plot the network.
|
|
379
|
-
|
|
380
|
-
Args:
|
|
381
|
-
- G: The networkx graph object.
|
|
382
|
-
- j_mat: The adjacency matrix representing connections between spins.
|
|
383
|
-
- ax: The axis object to plot the network.
|
|
384
|
-
- nodesz: The size of nodes in the network.
|
|
385
|
-
- linewz: The width of edges in the network.
|
|
386
|
-
- node_color: The color of nodes in the network.
|
|
387
|
-
- pos: The positions of nodes in the network.
|
|
388
|
-
"""
|
|
389
|
-
|
|
390
|
-
self_loops = [(u, v) for u, v in G.edges() if u == v]
|
|
391
|
-
G.remove_edges_from(self_loops)
|
|
392
|
-
|
|
393
|
-
eposi = [(u, v) for (u, v, d) in G.edges(data=True) if d['weight'] > 0]
|
|
394
|
-
wposi = np.array([d['weight']
|
|
395
|
-
for (u, v, d) in G.edges(data=True) if d['weight'] > 0])
|
|
396
|
-
|
|
397
|
-
enega = [(u, v) for (u, v, d) in G.edges(data=True) if d['weight'] < 0]
|
|
398
|
-
wnega = np.array([d['weight']
|
|
399
|
-
for (u, v, d) in G.edges(data=True) if d['weight'] < 0])
|
|
400
|
-
|
|
401
|
-
col1 = '#f0dab1'
|
|
402
|
-
|
|
403
|
-
nx.draw_networkx_nodes(
|
|
404
|
-
G,
|
|
405
|
-
pos,
|
|
406
|
-
ax=ax,
|
|
407
|
-
node_size=61.8 *
|
|
408
|
-
nodesz,
|
|
409
|
-
node_color=node_color,
|
|
410
|
-
edgecolors='k')
|
|
411
|
-
|
|
412
|
-
def sig_fun(xx): return (1 / (1 + np.exp(- 5 * (xx + cc))))
|
|
413
|
-
cc = np.max(np.abs(j_mat)) / 10
|
|
414
|
-
|
|
415
|
-
# edges
|
|
416
|
-
nx.draw_networkx_edges(
|
|
417
|
-
G,
|
|
418
|
-
pos,
|
|
419
|
-
ax=ax,
|
|
420
|
-
edgelist=eposi,
|
|
421
|
-
width=linewz * wposi,
|
|
422
|
-
edge_color='#3285CC',
|
|
423
|
-
alpha=sig_fun(wposi))
|
|
424
|
-
|
|
425
|
-
nx.draw_networkx_edges(G,
|
|
426
|
-
pos,
|
|
427
|
-
ax=ax,
|
|
428
|
-
edgelist=enega,
|
|
429
|
-
width=- linewz * wnega,
|
|
430
|
-
edge_color='#E84B23',
|
|
431
|
-
alpha=sig_fun(- wnega))
|
|
432
|
-
|
|
433
|
-
margin = 0.2
|
|
434
|
-
plt.margins(x=0.1, y=0.1)
|
|
435
|
-
|
|
436
|
-
ax.set_axis_off()
|
|
437
|
-
ax.set_aspect('equal')
|
|
438
|
-
return ax
|
|
439
|
-
|
|
440
|
-
def adjust_label_position(pos, offset=0.1):
|
|
441
|
-
"""
|
|
442
|
-
Adjust the label positions radially outward from the center.
|
|
443
|
-
|
|
444
|
-
Args:
|
|
445
|
-
- pos: The original positions of the labels.
|
|
446
|
-
- offset: The radial distance by which to adjust the label positions.
|
|
447
|
-
|
|
448
|
-
Returns:
|
|
449
|
-
- adjusted_pos: The adjusted positions of the labels.
|
|
450
|
-
"""
|
|
451
|
-
adjusted_pos = {}
|
|
452
|
-
for node, coordinates in enumerate(pos):
|
|
453
|
-
theta = np.arctan2(coordinates[1], coordinates[0])
|
|
454
|
-
radius = np.sqrt(coordinates[0]**2 + coordinates[1]**2)
|
|
455
|
-
adjusted_pos[node] = (
|
|
456
|
-
coordinates[0] + np.cos(theta) * offset,
|
|
457
|
-
coordinates[1] + np.sin(theta) * offset)
|
|
458
|
-
return adjusted_pos
|
|
459
|
-
|
|
460
|
-
def plot_final(
|
|
461
|
-
cur_j,
|
|
462
|
-
gene_program_name,
|
|
463
|
-
cluster:bool = True,
|
|
464
|
-
title: str = "Gene Regulatory Network Reconstructed by D-SPIN",
|
|
465
|
-
adj_matrix_threshold: float = 0.4,
|
|
466
|
-
resolution_parameter: float = 2,
|
|
467
|
-
nodesz: float = 3,
|
|
468
|
-
linewz: float = 2,
|
|
469
|
-
node_color: str = 'k',
|
|
470
|
-
figsize=[20, 20],
|
|
471
|
-
pos=None,
|
|
472
|
-
spin_order=None,
|
|
473
|
-
node_fontsize=None):
|
|
474
|
-
"""
|
|
475
|
-
Plot the final gene regulatory network.
|
|
476
|
-
|
|
477
|
-
Args:
|
|
478
|
-
- gene_program_name: The names of gene programs.
|
|
479
|
-
- cur_j: The adjacency matrix representing connections between gene programs.
|
|
480
|
-
- nodesz: The size of nodes in the network.
|
|
481
|
-
- linewz: The width of edges in the network.
|
|
482
|
-
- node_color: The color of nodes in the network.
|
|
483
|
-
- pos: The positions of nodes in the network.
|
|
484
|
-
"""
|
|
485
|
-
sc.set_figure_params(figsize=figsize)
|
|
486
|
-
|
|
487
|
-
num_spin = cur_j.shape[0]
|
|
352
|
+
plt.axhline(np.sum(net_class_len[:ii]) - 0.5, color='k', linewidth=1)
|
|
353
|
+
plt.axvline(np.sum(net_class_len[:ii]) - 0.5, color='k', linewidth=1)
|
|
488
354
|
|
|
489
|
-
|
|
490
|
-
if not nodesz:
|
|
491
|
-
nodesz = np.sqrt(100 / num_spin)
|
|
492
|
-
if not linewz:
|
|
493
|
-
linewz = np.sqrt(100 / num_spin)
|
|
494
|
-
if node_fontsize is None:
|
|
495
|
-
node_fontsize = figsize[0] * 20/ num_spin
|
|
496
|
-
|
|
355
|
+
fsize = 9
|
|
497
356
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
357
|
+
# Set labels
|
|
358
|
+
if spin_name_list is not None:
|
|
359
|
+
plt.xticks(np.arange(len(spin_order)), np.array(spin_name_list)[spin_order], rotation=90, fontsize=fsize)
|
|
360
|
+
plt.yticks(np.arange(len(spin_order)), np.array(spin_name_list)[spin_order], fontsize=fsize)
|
|
361
|
+
else:
|
|
362
|
+
plt.xticks(np.arange(len(spin_order)), [f'P{spin}' for spin in spin_order], rotation=90, fontsize=fsize)
|
|
363
|
+
plt.yticks(np.arange(len(spin_order)), [f'P{spin}' for spin in spin_order], fontsize=fsize)
|
|
364
|
+
|
|
365
|
+
plt.colorbar(fraction=0.6 / num_spin)
|
|
366
|
+
plt.grid()
|
|
367
|
+
|
|
368
|
+
# Save figure if folder is provided
|
|
369
|
+
if fig_folder is not None:
|
|
370
|
+
plt.savefig(fig_folder + 'network_heatmap.png', dpi=300, bbox_inches='tight')
|
|
371
|
+
|
|
372
|
+
plt.show()
|
|
507
373
|
|
|
508
|
-
# Creating a graph from the filtered adjacency matrix and ordering spins.
|
|
509
|
-
G = nx.convert_matrix.from_numpy_array(cur_j_filt[spin_order, :][:, spin_order])
|
|
510
374
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
pos = nx.circular_layout(G)
|
|
514
|
-
else:
|
|
515
|
-
pos = pert_pos
|
|
516
|
-
node_color = ['#f0dab1'] * num_spin
|
|
517
|
-
node_label = np.array(gene_program_name)[spin_order]
|
|
518
|
-
# node_label = np.array([format_label(label) for label in gene_list])
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
ax = plt.subplot(grid[1])
|
|
522
|
-
ax = plot_network(
|
|
523
|
-
G,
|
|
524
|
-
cur_j_filt,
|
|
525
|
-
ax,
|
|
526
|
-
nodesz=nodesz,
|
|
527
|
-
linewz=linewz,
|
|
528
|
-
node_color=node_color,
|
|
529
|
-
pos=pos)
|
|
530
|
-
|
|
531
|
-
# Adding labels with path effects to nodes in the network.
|
|
532
|
-
path_effect = [patheffects.withStroke(linewidth=3, foreground='w')]
|
|
533
|
-
|
|
534
|
-
if cluster:
|
|
535
|
-
adjusted_positions = adjust_label_position(pos, 0.5)
|
|
536
|
-
else:
|
|
537
|
-
adjusted_positions = pos
|
|
538
|
-
for ii in range(num_spin):
|
|
539
|
-
x, y = adjusted_positions[ii]
|
|
540
|
-
text = plt.text(
|
|
541
|
-
x,
|
|
542
|
-
y,
|
|
543
|
-
node_label[ii],
|
|
544
|
-
fontsize= node_fontsize,
|
|
545
|
-
color='k',
|
|
546
|
-
ha='center',
|
|
547
|
-
va='center',
|
|
548
|
-
rotation=np.arctan(
|
|
549
|
-
pos[ii][1] /
|
|
550
|
-
pos[ii][0]) /
|
|
551
|
-
np.pi *
|
|
552
|
-
180)
|
|
553
|
-
text.set_path_effects(path_effect)
|
|
554
|
-
if not title:
|
|
555
|
-
title = 'Gene Regulatory Network Reconstructed by D-SPIN'
|
|
556
|
-
ax.set_title(title, fontsize=node_fontsize * 1.5, y = 1.05)
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
def plot_module_heatmap(j_mat: np.ndarray,
|
|
560
|
-
modules: List[List[int]],
|
|
375
|
+
def plot_response_heatmap(h_vec: np.ndarray,
|
|
376
|
+
module_list: List[List[int]]=None,
|
|
561
377
|
spin_name_list: Optional[List[str]] = None,
|
|
378
|
+
sample_list: Optional[List[str]] = None,
|
|
562
379
|
fig_folder: Optional[str] = None) -> None:
|
|
563
380
|
"""
|
|
564
|
-
Plot a heatmap of the
|
|
381
|
+
Plot a heatmap of the response matrix with modules highlighted.
|
|
565
382
|
|
|
566
383
|
Parameters
|
|
567
384
|
----------
|
|
568
|
-
|
|
569
|
-
The
|
|
385
|
+
h_vec : np.ndarray
|
|
386
|
+
The response vector.
|
|
570
387
|
modules : List[List[int]]
|
|
571
388
|
List of modules, where each module is a list of node indices.
|
|
572
389
|
spin_name_list : List[str], optional
|
|
573
390
|
List of names for the spins/nodes. If None, will use generic names.
|
|
391
|
+
sample_list : List[str], optional
|
|
392
|
+
List of names for the samples. If None, will use generic names.
|
|
574
393
|
fig_folder : str, optional
|
|
575
394
|
Folder path to save the figure. If None, will not save the figure.
|
|
576
395
|
"""
|
|
577
|
-
num_spin =
|
|
578
|
-
|
|
579
|
-
np.fill_diagonal(j_mat, 0)
|
|
396
|
+
num_spin, num_sample = h_vec.shape
|
|
397
|
+
h_vec = h_vec.copy()
|
|
580
398
|
|
|
399
|
+
if module_list is None:
|
|
400
|
+
module_list = [list(leaves_list(linkage(h_vec, method='ward')))]
|
|
401
|
+
|
|
581
402
|
# Create spin order based on modules
|
|
582
|
-
spin_order = [spin for cur_list in
|
|
583
|
-
net_class_len = [len(cur_list) for cur_list in
|
|
403
|
+
spin_order = [spin for cur_list in module_list for spin in cur_list]
|
|
404
|
+
net_class_len = [len(cur_list) for cur_list in module_list]
|
|
405
|
+
sample_order = leaves_list(linkage(h_vec.T, method='ward'))
|
|
584
406
|
|
|
585
|
-
# Create custom colormap
|
|
586
407
|
cmap_hvec = mpl.colors.LinearSegmentedColormap.from_list("", ['#E84B23', '#FFFFFF', '#3285CC'])
|
|
587
408
|
|
|
588
409
|
# Plot heatmap
|
|
589
|
-
sc.set_figure_params(figsize=[0.
|
|
590
|
-
plt.imshow(
|
|
410
|
+
sc.set_figure_params(figsize=[0.16 * num_spin + 0.5, 0.16 * num_sample])
|
|
411
|
+
plt.imshow(h_vec.T[sample_order, :][:, spin_order], cmap=cmap_hvec, vmin=- 1, vmax=1, aspect='auto')
|
|
591
412
|
|
|
592
413
|
# Add module boundaries
|
|
593
414
|
for ii in range(len(net_class_len)):
|
|
594
|
-
plt.axhline(np.sum(net_class_len[:ii]) - 0.5, color='k', linewidth=1)
|
|
595
415
|
plt.axvline(np.sum(net_class_len[:ii]) - 0.5, color='k', linewidth=1)
|
|
596
416
|
|
|
597
417
|
# Set labels
|
|
598
418
|
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=
|
|
600
|
-
plt.yticks(np.arange(len(spin_order)), np.array(spin_name_list)[spin_order], fontsize=10)
|
|
419
|
+
plt.xticks(np.arange(len(spin_order)), np.array(spin_name_list)[spin_order], rotation=90, fontsize=8)
|
|
601
420
|
else:
|
|
602
|
-
plt.xticks(np.arange(len(spin_order)), [f'P{spin}' for spin in spin_order], rotation=90, fontsize=
|
|
603
|
-
|
|
421
|
+
plt.xticks(np.arange(len(spin_order)), [f'P{spin}' for spin in spin_order], rotation=90, fontsize=8)
|
|
422
|
+
|
|
423
|
+
plt.yticks(np.arange(len(sample_order)), np.array(sample_list)[sample_order], fontsize=8)
|
|
604
424
|
|
|
605
|
-
plt.colorbar(fraction=
|
|
425
|
+
plt.colorbar(fraction=1 / num_spin)
|
|
606
426
|
plt.grid()
|
|
607
|
-
plt.title(f'Network on {num_spin} programs', fontsize=12)
|
|
608
427
|
|
|
609
428
|
# Save figure if folder is provided
|
|
610
429
|
if fig_folder is not None:
|
|
@@ -613,6 +432,46 @@ def plot_module_heatmap(j_mat: np.ndarray,
|
|
|
613
432
|
plt.show()
|
|
614
433
|
|
|
615
434
|
|
|
435
|
+
def create_undirected_network(j_mat, node_names, thres_strength=0.05):
|
|
436
|
+
|
|
437
|
+
np.fill_diagonal(j_mat, 0)
|
|
438
|
+
|
|
439
|
+
# Filter weak connections
|
|
440
|
+
j_filt = j_mat.copy()
|
|
441
|
+
j_filt[np.abs(j_mat) < thres_strength] = 0
|
|
442
|
+
np.fill_diagonal(j_filt, 0)
|
|
443
|
+
|
|
444
|
+
# Create networkx graph
|
|
445
|
+
G = nx.from_numpy_array(j_filt)
|
|
446
|
+
G = nx.relabel_nodes(G, {ii: node_names[ii] for ii in range(len(node_names))})
|
|
447
|
+
|
|
448
|
+
return G, j_filt
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def create_directed_network(j_mat, node_names,thres_strength=0.05, thres_direction=0.05):
|
|
452
|
+
|
|
453
|
+
np.fill_diagonal(j_mat, 0)
|
|
454
|
+
|
|
455
|
+
num_spin = j_mat.shape[0]
|
|
456
|
+
j_mat_directed = np.zeros(j_mat.shape)
|
|
457
|
+
|
|
458
|
+
for ind1 in range(num_spin):
|
|
459
|
+
for ind2 in range(num_spin):
|
|
460
|
+
if np.abs(j_mat[ind1, ind2]) > (1 + thres_direction) * np.abs(j_mat[ind2, ind1]):
|
|
461
|
+
j_mat_directed[ind1, ind2] = j_mat[ind1, ind2]
|
|
462
|
+
elif np.abs(j_mat[ind2, ind1]) > (1 + thres_direction) * np.abs(j_mat[ind1, ind2]):
|
|
463
|
+
j_mat_directed[ind2, ind1] = j_mat[ind2, ind1]
|
|
464
|
+
else:
|
|
465
|
+
j_mat_directed[ind1, ind2] = (j_mat[ind1, ind2] + j_mat[ind2, ind1]) / 2
|
|
466
|
+
j_mat_directed[ind2, ind1] = j_mat_directed[ind1, ind2]
|
|
467
|
+
|
|
468
|
+
j_mat_directed[np.abs(j_mat_directed) < thres_strength] = 0
|
|
469
|
+
|
|
470
|
+
G = nx.from_numpy_array(j_mat_directed.T, create_using=nx.DiGraph)
|
|
471
|
+
G = nx.relabel_nodes(G, {ii: node_names[ii] for ii in range(len(node_names))})
|
|
472
|
+
|
|
473
|
+
return G, j_mat_directed
|
|
474
|
+
|
|
616
475
|
|
|
617
476
|
def layout_loss_fun(xx, ori_pos, network):
|
|
618
477
|
|
|
@@ -636,6 +495,85 @@ def layout_loss_fun(xx, ori_pos, network):
|
|
|
636
495
|
|
|
637
496
|
return stength_module * module_attract + strength_repulsion * repulse + strength_spring * spring_energy
|
|
638
497
|
|
|
498
|
+
|
|
499
|
+
def remove_peripheral_nodes(G, min_in_degree=1, min_out_degree=1, max_iter=100):
|
|
500
|
+
|
|
501
|
+
cur_size = len(G.nodes)
|
|
502
|
+
|
|
503
|
+
for repeat in range(max_iter):
|
|
504
|
+
|
|
505
|
+
remove_nodes1 = [node for node, out_degree in G.out_degree() if out_degree <= min_out_degree]
|
|
506
|
+
remove_nodes2 = [node for node, in_degree in G.in_degree() if in_degree <= min_in_degree]
|
|
507
|
+
remove_nodes = list(set(remove_nodes1 + remove_nodes2))
|
|
508
|
+
G.remove_nodes_from(remove_nodes)
|
|
509
|
+
|
|
510
|
+
if cur_size == len(G.nodes):
|
|
511
|
+
break
|
|
512
|
+
else:
|
|
513
|
+
cur_size = len(G.nodes)
|
|
514
|
+
|
|
515
|
+
print(f'{len(G.nodes)} nodes after filtering ')
|
|
516
|
+
|
|
517
|
+
return G
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def compute_modules(G_nx, resolution: float = 1.0,
|
|
521
|
+
seed: int = 1) -> List[List[int]]:
|
|
522
|
+
"""
|
|
523
|
+
Compute network modules using Leiden community detection.
|
|
524
|
+
|
|
525
|
+
Parameters
|
|
526
|
+
----------
|
|
527
|
+
resolution : float, optional
|
|
528
|
+
Resolution parameter for Leiden community detection. Higher values lead to more modules. Default is 1.0.
|
|
529
|
+
seed : int, optional
|
|
530
|
+
Random seed for Leiden community detection. Default is 0.
|
|
531
|
+
thres : float, optional
|
|
532
|
+
Threshold for filtering weak connections. Default is 0.
|
|
533
|
+
|
|
534
|
+
Returns
|
|
535
|
+
-------
|
|
536
|
+
List[List[int]]
|
|
537
|
+
List of modules, where each module is a list of node indices.
|
|
538
|
+
"""
|
|
539
|
+
|
|
540
|
+
adj_matrix = nx.to_numpy_array(G_nx, weight='weight')
|
|
541
|
+
sub_gene_list = list(G_nx.nodes)
|
|
542
|
+
|
|
543
|
+
G = nx.from_numpy_array(adj_matrix)
|
|
544
|
+
|
|
545
|
+
G = ig.Graph.from_networkx(G)
|
|
546
|
+
|
|
547
|
+
# Separate positive and negative edges
|
|
548
|
+
G_pos = G.subgraph_edges(G.es.select(weight_gt=0), delete_vertices=False)
|
|
549
|
+
G_neg = G.subgraph_edges(G.es.select(weight_lt=0), delete_vertices=False)
|
|
550
|
+
G_neg.es['weight'] = [-w for w in G_neg.es['weight']]
|
|
551
|
+
|
|
552
|
+
# Apply Leiden community detection
|
|
553
|
+
part_pos = la.RBConfigurationVertexPartition(G_pos, weights='weight', resolution_parameter=resolution)
|
|
554
|
+
part_neg = la.RBConfigurationVertexPartition(G_neg, weights='weight', resolution_parameter=resolution)
|
|
555
|
+
|
|
556
|
+
optimiser = la.Optimiser()
|
|
557
|
+
optimiser.set_rng_seed(seed)
|
|
558
|
+
|
|
559
|
+
diff = optimiser.optimise_partition_multiplex([part_pos, part_neg], layer_weights=[1, -1])
|
|
560
|
+
|
|
561
|
+
# Extract modules
|
|
562
|
+
net_class = list(part_pos)
|
|
563
|
+
|
|
564
|
+
print(f'Identified {len(net_class)} modules with sizes {np.array([len(cur_list) for cur_list in net_class])}')
|
|
565
|
+
|
|
566
|
+
abs_degree = np.abs(adj_matrix).sum(axis=1)
|
|
567
|
+
|
|
568
|
+
# rank node inside each module by degree
|
|
569
|
+
for ii in range(len(net_class)):
|
|
570
|
+
cur_list = net_class[ii]
|
|
571
|
+
cur_list = sorted(cur_list, key=lambda xx: abs_degree[xx], reverse=True)
|
|
572
|
+
net_class[ii] = cur_list
|
|
573
|
+
|
|
574
|
+
return net_class
|
|
575
|
+
|
|
576
|
+
|
|
639
577
|
def compute_module_angle(module_size, gap_size):
|
|
640
578
|
|
|
641
579
|
module_plots_size = np.sqrt(module_size)
|
|
@@ -652,7 +590,8 @@ def compute_module_angle(module_size, gap_size):
|
|
|
652
590
|
|
|
653
591
|
return angle_list
|
|
654
592
|
|
|
655
|
-
|
|
593
|
+
|
|
594
|
+
def compute_network_positions(network, net_class, circle_size=None, module_angle=None, gap_size=0.5):
|
|
656
595
|
|
|
657
596
|
module_size = np.array([len(cur_list) for cur_list in net_class])
|
|
658
597
|
num_spin = np.sum(module_size)
|
|
@@ -683,36 +622,13 @@ def compute_pos_from_module(network, net_class, circle_size=None, module_angle=N
|
|
|
683
622
|
return pos
|
|
684
623
|
|
|
685
624
|
|
|
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
625
|
def plot_network_diagram(j_mat: np.ndarray,
|
|
710
626
|
modules: List[List[int]],
|
|
627
|
+
directed: bool = False,
|
|
711
628
|
pos: Optional[Dict[int, Tuple[float, float]]] = None,
|
|
629
|
+
weight_thres: float = None,
|
|
712
630
|
spin_name_list_short: Optional[List[str]] = None,
|
|
713
|
-
|
|
714
|
-
fig_folder: Optional[str] = None,
|
|
715
|
-
num_spin: Optional[int] = None) -> None:
|
|
631
|
+
fig_folder: Optional[str] = None) -> None:
|
|
716
632
|
"""
|
|
717
633
|
Plot the network diagram with modules highlighted.
|
|
718
634
|
|
|
@@ -722,28 +638,33 @@ def plot_network_diagram(j_mat: np.ndarray,
|
|
|
722
638
|
The network adjacency matrix.
|
|
723
639
|
modules : List[List[int]]
|
|
724
640
|
List of modules, where each module is a list of node indices.
|
|
641
|
+
directed : bool, optional
|
|
642
|
+
Whether the network is directed. If True, will plot the network as a directed network.
|
|
725
643
|
pos : Dict[int, Tuple[float, float]], optional
|
|
726
644
|
Dictionary mapping node indices to (x, y) positions. If None, positions will be computed automatically.
|
|
645
|
+
weight_thres : float, optional
|
|
646
|
+
Threshold for filtering weak connections. If None, will filter by the 10th percentile of the absolute nonzero values of the network adjacency matrix.
|
|
727
647
|
spin_name_list_short : List[str], optional
|
|
728
648
|
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
649
|
fig_folder : str, optional
|
|
732
650
|
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
651
|
"""
|
|
736
|
-
|
|
737
|
-
num_spin = j_mat.shape[0]
|
|
652
|
+
num_spin = j_mat.shape[0]
|
|
738
653
|
|
|
739
654
|
# Filter network for visualization
|
|
740
|
-
|
|
655
|
+
if weight_thres is None:
|
|
656
|
+
thres = np.percentile(np.abs(j_mat[j_mat != 0]), 10)
|
|
657
|
+
else:
|
|
658
|
+
thres = weight_thres
|
|
659
|
+
|
|
741
660
|
j_filt = j_mat.copy()
|
|
742
661
|
j_filt[np.abs(j_mat) < thres] = 0
|
|
743
662
|
np.fill_diagonal(j_filt, 0)
|
|
744
663
|
|
|
745
|
-
|
|
746
|
-
|
|
664
|
+
if directed:
|
|
665
|
+
G = nx.from_numpy_array(j_filt, create_using=nx.DiGraph)
|
|
666
|
+
else:
|
|
667
|
+
G = nx.from_numpy_array(j_filt)
|
|
747
668
|
|
|
748
669
|
# Compute positions
|
|
749
670
|
if pos is None:
|
|
@@ -756,7 +677,7 @@ def plot_network_diagram(j_mat: np.ndarray,
|
|
|
756
677
|
label = {ii: f'P{ii}' for ii in range(num_spin)}
|
|
757
678
|
|
|
758
679
|
# Plot network
|
|
759
|
-
fig_size =
|
|
680
|
+
fig_size = 4 * num_spin ** 0.25
|
|
760
681
|
sc.set_figure_params(figsize=[fig_size, fig_size])
|
|
761
682
|
fig, ax = plt.subplots()
|
|
762
683
|
|
|
@@ -767,14 +688,16 @@ def plot_network_diagram(j_mat: np.ndarray,
|
|
|
767
688
|
enega = [(u, v) for (u, v, d) in G.edges(data=True) if d['weight'] < 0]
|
|
768
689
|
wnega = np.array([d['weight'] for (u, v, d) in G.edges(data=True) if d['weight'] < 0])
|
|
769
690
|
|
|
691
|
+
linewz = 0.5 / thres
|
|
692
|
+
|
|
770
693
|
# Draw positive edges
|
|
771
694
|
if len(eposi) > 0:
|
|
772
|
-
nx.draw_networkx_edges(G, pos, ax=ax, edgelist=eposi, width=
|
|
695
|
+
nx.draw_networkx_edges(G, pos, ax=ax, edgelist=eposi, width=linewz * wposi,
|
|
773
696
|
edge_color='#3285CC', alpha=0.7)
|
|
774
697
|
|
|
775
698
|
# Draw negative edges
|
|
776
699
|
if len(enega) > 0:
|
|
777
|
-
nx.draw_networkx_edges(G, pos, ax=ax, edgelist=enega, width=
|
|
700
|
+
nx.draw_networkx_edges(G, pos, ax=ax, edgelist=enega, width=linewz * (-wnega),
|
|
778
701
|
edge_color='#E84B23', alpha=0.7)
|
|
779
702
|
|
|
780
703
|
# Draw nodes
|
|
@@ -790,25 +713,64 @@ def plot_network_diagram(j_mat: np.ndarray,
|
|
|
790
713
|
|
|
791
714
|
ax.set_aspect('equal')
|
|
792
715
|
ax.set_axis_off()
|
|
793
|
-
plt.title('Network Diagram with Modules', fontsize=12)
|
|
794
716
|
|
|
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
717
|
# Save figure if folder is provided
|
|
811
718
|
if fig_folder is not None:
|
|
812
719
|
plt.savefig(fig_folder + 'network_diagram.png', dpi=300, bbox_inches='tight')
|
|
813
720
|
|
|
814
|
-
plt.show()
|
|
721
|
+
plt.show()
|
|
722
|
+
|
|
723
|
+
def plot_top_regulators(program_interactions: np.ndarray,
|
|
724
|
+
program_index: int,
|
|
725
|
+
gene_list: np.array,
|
|
726
|
+
program_name: Optional[str] = None,
|
|
727
|
+
thres: Optional[float] = None,
|
|
728
|
+
fig_folder: Optional[str] = None) -> None:
|
|
729
|
+
|
|
730
|
+
"""
|
|
731
|
+
Plot the top regulators of a program.
|
|
732
|
+
|
|
733
|
+
Parameters
|
|
734
|
+
----------
|
|
735
|
+
program_interactions : np.ndarray
|
|
736
|
+
The program interactions.
|
|
737
|
+
program_index : int
|
|
738
|
+
The index of the program.
|
|
739
|
+
gene_list : np.array
|
|
740
|
+
The list of genes.
|
|
741
|
+
program_name : Optional[str], optional
|
|
742
|
+
The name of the program.
|
|
743
|
+
thres : Optional[float], optional
|
|
744
|
+
The threshold for the regulators.
|
|
745
|
+
fig_folder : Optional[str], optional
|
|
746
|
+
The folder to save the figure.
|
|
747
|
+
"""
|
|
748
|
+
|
|
749
|
+
cur_regulators = program_interactions[:, program_index]
|
|
750
|
+
if thres is None:
|
|
751
|
+
thres = np.sort(np.abs(cur_regulators))[::-1][20]
|
|
752
|
+
|
|
753
|
+
num_gene_show_pos = np.sum(cur_regulators >= thres)
|
|
754
|
+
num_gene_show_neg = np.sum(cur_regulators <= -thres)
|
|
755
|
+
num_gene_show = num_gene_show_pos + num_gene_show_neg
|
|
756
|
+
|
|
757
|
+
plot_ind_pos = np.argsort(cur_regulators)[::-1][:num_gene_show_pos]
|
|
758
|
+
plot_ind_neg = np.argsort(cur_regulators)[:num_gene_show_neg]
|
|
759
|
+
|
|
760
|
+
plot_ind = np.concatenate([plot_ind_pos, plot_ind_neg])
|
|
761
|
+
|
|
762
|
+
sc.set_figure_params(figsize=(num_gene_show * 0.17, 2))
|
|
763
|
+
|
|
764
|
+
plt.scatter(np.arange(num_gene_show), cur_regulators[plot_ind], c=np.where(cur_regulators[plot_ind] > 0,'#3285CC', '#E84B23'), zorder=10)
|
|
765
|
+
plt.xticks(np.arange(num_gene_show), np.array(gene_list)[plot_ind], rotation=90, fontsize=9);
|
|
766
|
+
plt.yticks(fontsize=10)
|
|
767
|
+
plt.ylabel('Regulator strength', fontsize=10)
|
|
768
|
+
plt.xlabel('Regulator', fontsize=10)
|
|
769
|
+
|
|
770
|
+
if program_name is not None:
|
|
771
|
+
plt.title(program_name, fontsize=10)
|
|
772
|
+
|
|
773
|
+
if fig_folder is not None:
|
|
774
|
+
plt.savefig(fig_folder + 'top_regulators.png')
|
|
775
|
+
|
|
776
|
+
plt.show()
|
|
@@ -21,7 +21,7 @@ install_requires = [
|
|
|
21
21
|
# ]
|
|
22
22
|
|
|
23
23
|
# Managing your version (adjust accordingly)
|
|
24
|
-
version = "1.4.
|
|
24
|
+
version = "1.4.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|