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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dspin
3
- Version: 1.4.0
3
+ Version: 1.4.2
4
4
  Summary: Regulatory network models from single-cell perturbation profiling
5
5
  Home-page: https://github.com/JialongJiang/DSPIN
6
6
  Author: Jialong Jiang
@@ -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. Pratapa, Aditya, et al. "Benchmarking algorithms for gene regulatory network inference from single-cell transcriptomic data." Nature methods 17.2 (2020): 147-154.
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
- 3. Cui, Ang, et al. "Dictionary of immune responses to cytokines at single-cell resolution." Nature 625.7994 (2024): 377-384.
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
- if gene_skew.mean() > 2:
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
- Args:
311
- - j_mat: The adjacency matrix representing connections between spins.
312
-
313
- Returns:
314
- - spin_order: The order of spins determined by the function.
315
- - pert_pos: The positions perturbed for visualization.
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
- thres = 0
320
- j_filt = j_mat.copy()
321
- j_filt[np.abs(j_mat) < thres] = 0
322
- np.fill_diagonal(j_filt, 0)
323
- G = nx.from_numpy_array(j_filt)
324
-
325
- G = ig.Graph.from_networkx(G)
326
- G_pos = G.subgraph_edges(
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
- angle_list_raw = np.linspace(start_angle, end_angle, np.sum(
353
- net_class_len) + gap_size * len(net_class_len) + 1)[: - 1]
354
- angle_list = []
355
- size_group_cum = np.cumsum(net_class_len)
356
- size_group_cum = np.insert(size_group_cum, 0, 0)
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
- angle_list.extend(
360
- angle_list_raw[size_group_cum[ii] + gap_size * ii: size_group_cum[ii + 1] + gap_size * ii])
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
- # Setting default values for parameters of node size and line width.
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
- fig, grid = sc.pl._tools._panel_grid(0.2, 0.2, ncols=2, num_panels=2)
499
-
500
- # Filtering the adjacency matrix.
501
- cur_j_filt = cur_j.copy()
502
- cur_j_filt[np.abs(cur_j_filt) < np.percentile(np.abs(cur_j_filt), adj_matrix_threshold * 100)] = 0
503
-
504
- if spin_order is None:
505
- # Calculating spin orders and perturbed positions for plotting.
506
- spin_order, pert_pos = spin_order_in_cluster(cur_j, resolution_parameter)
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
- # Initializations and adjustments for plotting.
512
- if not cluster:
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 network adjacency matrix with modules highlighted.
381
+ Plot a heatmap of the response matrix with modules highlighted.
565
382
 
566
383
  Parameters
567
384
  ----------
568
- j_mat : np.ndarray
569
- The network adjacency matrix.
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 = j_mat.shape[0]
578
- j_mat = j_mat.copy()
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 modules for spin in cur_list]
583
- net_class_len = [len(cur_list) for cur_list in modules]
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.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)
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=10)
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=10)
603
- plt.yticks(np.arange(len(spin_order)), [f'P{spin}' for spin in spin_order], fontsize=10)
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=0.03)
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
- def compute_pos_from_module(network, net_class, circle_size=None, module_angle=None, gap_size=0.5):
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
- spin_name_list: Optional[List[str]] = None,
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
- if num_spin is None:
737
- num_spin = j_mat.shape[0]
652
+ num_spin = j_mat.shape[0]
738
653
 
739
654
  # Filter network for visualization
740
- thres = 0.2
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
- # Create networkx graph
746
- G = nx.from_numpy_array(j_filt)
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 = 3 * num_spin ** 0.3
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=3 * wposi,
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=3 * (-wnega),
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()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dspin
3
- Version: 1.4.0
3
+ Version: 1.4.2
4
4
  Summary: Regulatory network models from single-cell perturbation profiling
5
5
  Home-page: https://github.com/JialongJiang/DSPIN
6
6
  Author: Jialong Jiang
@@ -21,7 +21,7 @@ install_requires = [
21
21
  # ]
22
22
 
23
23
  # Managing your version (adjust accordingly)
24
- version = "1.4.0" # You can manage versioning in a more sophisticated way if needed
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