nettracer3d 0.8.9__py3-none-any.whl → 0.9.0__py3-none-any.whl

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.
@@ -102,7 +102,7 @@ class ImageViewerWindow(QMainWindow):
102
102
  "WHITE": (1, 1, 1),
103
103
  "GRAY": (0.5, 0.5, 0.5),
104
104
  "LIGHT_GRAY": (0.8, 0.8, 0.8),
105
- "DARK_GRAY": (0.2, 0.2, 0.2)
105
+ "DARK_GRAY": (0.2, 0.2, 0.2),
106
106
  }
107
107
 
108
108
  self.base_colors = [ #Channel colors
@@ -3471,12 +3471,10 @@ class ImageViewerWindow(QMainWindow):
3471
3471
  stats_menu = analysis_menu.addMenu("Stats")
3472
3472
  allstats_action = stats_menu.addAction("Calculate Generic Network Stats")
3473
3473
  allstats_action.triggered.connect(self.stats)
3474
- histos_action = stats_menu.addAction("Calculate Generic Network Histograms")
3474
+ histos_action = stats_menu.addAction("Network Statistic Histograms")
3475
3475
  histos_action.triggered.connect(self.histos)
3476
3476
  radial_action = stats_menu.addAction("Radial Distribution Analysis")
3477
3477
  radial_action.triggered.connect(self.show_radial_dialog)
3478
- degree_dist_action = stats_menu.addAction("Degree Distribution Analysis")
3479
- degree_dist_action.triggered.connect(self.show_degree_dist_dialog)
3480
3478
  neighbor_id_action = stats_menu.addAction("Identity Distribution of Neighbors")
3481
3479
  neighbor_id_action.triggered.connect(self.show_neighbor_id_dialog)
3482
3480
  ripley_action = stats_menu.addAction("Ripley Clustering Analysis")
@@ -3515,7 +3513,7 @@ class ImageViewerWindow(QMainWindow):
3515
3513
 
3516
3514
  # Process menu
3517
3515
  process_menu = menubar.addMenu("Process")
3518
- calculate_menu = process_menu.addMenu("Calculate")
3516
+ calculate_menu = process_menu.addMenu("Calculate Network")
3519
3517
  calc_all_action = calculate_menu.addAction("Calculate Connectivity Network (Find Node-Edge-Node Network)")
3520
3518
  calc_all_action.triggered.connect(self.show_calc_all_dialog)
3521
3519
  calc_prox_action = calculate_menu.addAction("Calculate Proximity Network (connect nodes by distance)")
@@ -3615,46 +3613,75 @@ class ImageViewerWindow(QMainWindow):
3615
3613
  menubar.setCornerWidget(cam_button, Qt.Corner.TopRightCorner)
3616
3614
 
3617
3615
  def snap(self):
3618
-
3619
3616
  try:
3620
-
3617
+ # Check if we have any data to save
3618
+ data = False
3621
3619
  for thing in self.channel_data:
3622
3620
  if thing is not None:
3623
3621
  data = True
3622
+ break
3624
3623
  if not data:
3625
3624
  return
3626
-
3627
- snap = self.create_composite_for_pan()
3628
-
3625
+
3626
+ # Get filename from user
3629
3627
  filename, _ = QFileDialog.getSaveFileName(
3630
3628
  self,
3631
3629
  f"Save Image As",
3632
- "", # Default directory
3633
- "TIFF Files (*.tif *.tiff);;All Files (*)" # File type filter
3630
+ "",
3631
+ "PNG Files (*.png);;TIFF Files (*.tif *.tiff);;JPEG Files (*.jpg *.jpeg);;All Files (*)"
3634
3632
  )
3635
3633
 
3636
- if filename: # Only proceed if user didn't cancel
3637
- # If user didn't type an extension, add .tif
3638
- if not filename.endswith(('.tif', '.tiff')):
3639
- filename += '.tif'
3640
-
3641
- import tifffile
3642
- tifffile.imwrite(filename, snap)
3643
-
3644
- except:
3645
- pass
3634
+ if filename:
3635
+ # Determine file extension
3636
+ if filename.lower().endswith(('.tif', '.tiff')):
3637
+ format_type = 'tiff'
3638
+ elif filename.lower().endswith(('.jpg', '.jpeg')):
3639
+ format_type = 'jpeg'
3640
+ elif filename.lower().endswith('.png'):
3641
+ format_type = 'png'
3642
+ else:
3643
+ filename += '.png'
3644
+ format_type = 'png'
3645
+
3646
+ # Method 1: Save with axes bbox (recommended)
3647
+ bbox = self.ax.get_window_extent().transformed(self.figure.dpi_scale_trans.inverted())
3648
+ self.figure.savefig(filename,
3649
+ dpi=300,
3650
+ bbox_inches=bbox,
3651
+ facecolor='black',
3652
+ edgecolor='none',
3653
+ format=format_type,
3654
+ pad_inches=0)
3655
+
3656
+ print(f"Axes snapshot saved: {filename}")
3657
+
3658
+ except Exception as e:
3659
+ print(f"Error saving snapshot: {e}")
3646
3660
 
3647
3661
 
3648
3662
  def open_cellpose(self):
3649
3663
 
3664
+ try:
3665
+ if self.shape[0] == 1:
3666
+ use_3d = False
3667
+ print("Launching 2D cellpose GUI")
3668
+ else:
3669
+ use_3d = True
3670
+ print("Launching 3D cellpose GUI")
3671
+ except:
3672
+ use_3d = True
3673
+ print("Launching 3D cellpose GUI")
3674
+
3650
3675
  try:
3651
3676
 
3652
3677
  from . import cellpose_manager
3653
3678
  self.cellpose_launcher = cellpose_manager.CellposeGUILauncher(parent_widget=self)
3654
3679
 
3655
- self.cellpose_launcher.launch_cellpose_gui()
3680
+ self.cellpose_launcher.launch_cellpose_gui(use_3d = use_3d)
3656
3681
 
3657
3682
  except:
3683
+ import traceback
3684
+ print(traceback.format_exc())
3658
3685
  pass
3659
3686
 
3660
3687
 
@@ -3680,106 +3707,26 @@ class ImageViewerWindow(QMainWindow):
3680
3707
  print(f"Error finding stats: {e}")
3681
3708
 
3682
3709
  def histos(self):
3683
-
3684
- """from networkx documentation"""
3685
-
3710
+ """
3711
+ Show a PyQt6 window with buttons to select which histogram to generate.
3712
+ Only calculates the histogram that the user selects.
3713
+ """
3686
3714
  try:
3687
-
3688
- G = my_network.network
3689
-
3690
- shortest_path_lengths = dict(nx.all_pairs_shortest_path_length(G))
3691
- diameter = max(nx.eccentricity(G, sp=shortest_path_lengths).values())
3692
- # We know the maximum shortest path length (the diameter), so create an array
3693
- # to store values from 0 up to (and including) diameter
3694
- path_lengths = np.zeros(diameter + 1, dtype=int)
3695
-
3696
-
3697
-
3698
- # Extract the frequency of shortest path lengths between two nodes
3699
- for pls in shortest_path_lengths.values():
3700
- pl, cnts = np.unique(list(pls.values()), return_counts=True)
3701
- path_lengths[pl] += cnts
3702
-
3703
- # Express frequency distribution as a percentage (ignoring path lengths of 0)
3704
- freq_percent = 100 * path_lengths[1:] / path_lengths[1:].sum()
3705
-
3706
- # Plot the frequency distribution (ignoring path lengths of 0) as a percentage
3707
- fig, ax = plt.subplots(figsize=(15, 8))
3708
- ax.bar(np.arange(1, diameter + 1), height=freq_percent)
3709
- ax.set_title(
3710
- "Distribution of shortest path length in G", fontdict={"size": 35}, loc="center"
3711
- )
3712
- ax.set_xlabel("Shortest Path Length", fontdict={"size": 22})
3713
- ax.set_ylabel("Frequency (%)", fontdict={"size": 22})
3714
-
3715
- plt.show()
3716
- freq_dict = {freq: length for length, freq in enumerate(freq_percent, start=1)}
3717
- self.format_for_upperright_table(freq_dict, metric='Frequency (%)', value='Shortest Path Length', title="Distribution of shortest path length in G")
3718
-
3719
- degree_centrality = nx.centrality.degree_centrality(G)
3720
- plt.figure(figsize=(15, 8))
3721
- plt.hist(degree_centrality.values(), bins=25)
3722
- plt.xticks(ticks=[0, 0.025, 0.05, 0.1, 0.15, 0.2]) # set the x axis ticks
3723
- plt.title("Degree Centrality Histogram ", fontdict={"size": 35}, loc="center")
3724
- plt.xlabel("Degree Centrality", fontdict={"size": 20})
3725
- plt.ylabel("Counts", fontdict={"size": 20})
3726
- plt.show()
3727
- self.format_for_upperright_table(degree_centrality, metric='Node', value='Degree Centrality', title="Degree Centrality Table")
3728
-
3729
-
3730
- betweenness_centrality = nx.centrality.betweenness_centrality(
3731
- G
3732
- )
3733
- plt.figure(figsize=(15, 8))
3734
- plt.hist(betweenness_centrality.values(), bins=100)
3735
- plt.xticks(ticks=[0, 0.02, 0.1, 0.2, 0.3, 0.4, 0.5]) # set the x axis ticks
3736
- plt.title("Betweenness Centrality Histogram ", fontdict={"size": 35}, loc="center")
3737
- plt.xlabel("Betweenness Centrality", fontdict={"size": 20})
3738
- plt.ylabel("Counts", fontdict={"size": 20})
3739
- plt.show()
3740
- self.format_for_upperright_table(betweenness_centrality, metric='Node', value='Betweenness Centrality', title="Betweenness Centrality Table")
3741
-
3742
-
3743
- closeness_centrality = nx.centrality.closeness_centrality(
3744
- G
3745
- )
3746
- plt.figure(figsize=(15, 8))
3747
- plt.hist(closeness_centrality.values(), bins=60)
3748
- plt.title("Closeness Centrality Histogram ", fontdict={"size": 35}, loc="center")
3749
- plt.xlabel("Closeness Centrality", fontdict={"size": 20})
3750
- plt.ylabel("Counts", fontdict={"size": 20})
3751
- plt.show()
3752
- self.format_for_upperright_table(closeness_centrality, metric='Node', value='Closeness Centrality', title="Closeness Centrality Table")
3753
-
3754
-
3755
- eigenvector_centrality = nx.centrality.eigenvector_centrality(
3756
- G
3757
- )
3758
- plt.figure(figsize=(15, 8))
3759
- plt.hist(eigenvector_centrality.values(), bins=60)
3760
- plt.xticks(ticks=[0, 0.01, 0.02, 0.04, 0.06, 0.08]) # set the x axis ticks
3761
- plt.title("Eigenvector Centrality Histogram ", fontdict={"size": 35}, loc="center")
3762
- plt.xlabel("Eigenvector Centrality", fontdict={"size": 20})
3763
- plt.ylabel("Counts", fontdict={"size": 20})
3764
- plt.show()
3765
- self.format_for_upperright_table(eigenvector_centrality, metric='Node', value='Eigenvector Centrality', title="Eigenvector Centrality Table")
3766
-
3767
-
3768
-
3769
- clusters = nx.clustering(G)
3770
- plt.figure(figsize=(15, 8))
3771
- plt.hist(clusters.values(), bins=50)
3772
- plt.title("Clustering Coefficient Histogram ", fontdict={"size": 35}, loc="center")
3773
- plt.xlabel("Clustering Coefficient", fontdict={"size": 20})
3774
- plt.ylabel("Counts", fontdict={"size": 20})
3775
- plt.show()
3776
- self.format_for_upperright_table(clusters, metric='Node', value='Clustering Coefficient', title="Clustering Coefficient Table")
3777
-
3778
- bridges = list(nx.bridges(G))
3779
- self.format_for_upperright_table(bridges, metric = 'Node Pair', title="Bridges")
3780
-
3715
+ # Create QApplication if it doesn't exist
3716
+ app = QApplication.instance()
3717
+ if app is None:
3718
+ app = QApplication(sys.argv)
3719
+
3720
+ # Create and show the histogram selector window
3721
+ self.histogram_selector = HistogramSelector(self)
3722
+ self.histogram_selector.show()
3723
+
3724
+ # Keep the window open (you might want to handle this differently based on your application structure)
3725
+ if not app.exec():
3726
+ pass # Window was closed
3727
+
3781
3728
  except Exception as e:
3782
- print(f"Error generating histograms: {e}")
3729
+ print(f"Error creating histogram selector: {e}")
3783
3730
 
3784
3731
  def volumes(self):
3785
3732
 
@@ -5271,21 +5218,38 @@ class ImageViewerWindow(QMainWindow):
5271
5218
  else:
5272
5219
  current_image = self.channel_data[channel]
5273
5220
 
5274
- if is_rgb and self.channel_data[channel].shape[-1] == 3:
5275
- # For RGB images, just display directly without colormap
5276
- self.ax.imshow(current_image,
5277
- alpha=0.7)
5278
- elif is_rgb and self.channel_data[channel].shape[-1] == 4:
5279
- self.ax.imshow(current_image) #For images that already have an alpha value and RGB, don't update alpha
5280
-
5221
+ if is_rgb and self.channel_data[channel].shape[-1] in [3, 4]:
5222
+ # For RGB/RGBA images, use brightness/contrast to control alpha instead
5223
+
5224
+ # Calculate alpha based on brightness settings
5225
+ brightness_min = self.channel_brightness[channel]['min']
5226
+ brightness_max = self.channel_brightness[channel]['max']
5227
+
5228
+ # Map brightness range to alpha range (0.0 to 1.0)
5229
+ # brightness_min controls minimum alpha, brightness_max controls maximum alpha
5230
+ alpha_range = brightness_max - brightness_min
5231
+ base_alpha = brightness_min
5232
+ # You can adjust these multipliers to control the alpha range
5233
+ final_alpha = base_alpha + alpha_range # Scale to reasonable alpha range
5234
+ final_alpha = np.clip(final_alpha, 0.0, 1.0) # Ensure valid alpha range
5235
+
5236
+ # Display the image with brightness-controlled alpha
5237
+ if current_image.shape[-1] == 4:
5238
+ # For RGBA, multiply existing alpha by our brightness-controlled alpha
5239
+ img_with_alpha = current_image.copy()
5240
+ img_with_alpha[..., 3] = img_with_alpha[..., 3] * final_alpha
5241
+ self.ax.imshow(img_with_alpha)
5242
+ else:
5243
+ # For RGB, apply brightness-controlled alpha directly
5244
+ self.ax.imshow(current_image, alpha=final_alpha)
5245
+
5281
5246
  else:
5282
- # Regular channel processing with colormap
5247
+ # Regular channel processing with colormap (your existing code)
5283
5248
  # Calculate brightness/contrast limits from entire volume
5284
- if self.min_max[channel][0] == None:
5285
- self.min_max[channel][0] = np.min(channel)
5286
- if self.min_max[channel][1] == None:
5287
- self.min_max[channel][1] = np.max(channel)
5288
-
5249
+ if self.min_max[channel][0] is None:
5250
+ self.min_max[channel][0] = np.min(self.channel_data[channel])
5251
+ if self.min_max[channel][1] is None:
5252
+ self.min_max[channel][1] = np.max(self.channel_data[channel])
5289
5253
  img_min = self.min_max[channel][0]
5290
5254
  img_max = self.min_max[channel][1]
5291
5255
 
@@ -5344,7 +5308,7 @@ class ImageViewerWindow(QMainWindow):
5344
5308
  )
5345
5309
  self.ax.imshow(self.mini_overlay_data,
5346
5310
  cmap=highlight_cmap,
5347
- alpha=0.5)
5311
+ alpha=0.8)
5348
5312
  elif self.highlight_overlay is not None and self.highlight and self.machine_window is None:
5349
5313
  highlight_slice = self.highlight_overlay[self.current_slice]
5350
5314
  highlight_cmap = LinearSegmentedColormap.from_list(
@@ -5353,7 +5317,7 @@ class ImageViewerWindow(QMainWindow):
5353
5317
  )
5354
5318
  self.ax.imshow(highlight_slice,
5355
5319
  cmap=highlight_cmap,
5356
- alpha=0.5)
5320
+ alpha=0.8)
5357
5321
  elif self.highlight_overlay is not None and self.highlight:
5358
5322
  highlight_slice = self.highlight_overlay[self.current_slice]
5359
5323
  highlight_cmap = LinearSegmentedColormap.from_list(
@@ -5366,7 +5330,7 @@ class ImageViewerWindow(QMainWindow):
5366
5330
  cmap=highlight_cmap,
5367
5331
  vmin=0,
5368
5332
  vmax=2, # Important: set vmax to 2 to accommodate both values
5369
- alpha=0.5)
5333
+ alpha=0.3)
5370
5334
 
5371
5335
  if self.channel_data[4] is not None:
5372
5336
 
@@ -5534,10 +5498,6 @@ class ImageViewerWindow(QMainWindow):
5534
5498
  dialog = RadialDialog(self)
5535
5499
  dialog.exec()
5536
5500
 
5537
- def show_degree_dist_dialog(self):
5538
- dialog = DegreeDistDialog(self)
5539
- dialog.exec()
5540
-
5541
5501
  def show_neighbor_id_dialog(self):
5542
5502
  dialog = NeighborIdentityDialog(self)
5543
5503
  dialog.exec()
@@ -7122,6 +7082,15 @@ class ColorOverlayDialog(QDialog):
7122
7082
 
7123
7083
  layout = QFormLayout(self)
7124
7084
 
7085
+ # Add mode selection dropdown
7086
+ self.mode_selector = QComboBox()
7087
+ self.mode_selector.addItems(["Nodes", "Edges"])
7088
+ if self.parent().active_channel == 0 and self.parent().channel_data[0] is not None:
7089
+ self.mode_selector.setCurrentIndex(0) # Default to Mode 1
7090
+ else:
7091
+ self.mode_selector.setCurrentIndex(1) # Default to Mode 1
7092
+ layout.addRow("Execution Mode:", self.mode_selector)
7093
+
7125
7094
  self.down_factor = QLineEdit("")
7126
7095
  layout.addRow("down_factor (for speeding up overlay generation - optional):", self.down_factor)
7127
7096
 
@@ -7136,11 +7105,10 @@ class ColorOverlayDialog(QDialog):
7136
7105
 
7137
7106
  down_factor = float(self.down_factor.text()) if self.down_factor.text().strip() else None
7138
7107
 
7139
- if self.parent().active_channel == 0:
7140
- mode = 0
7108
+ mode = self.mode_selector.currentIndex()
7109
+ if mode == 0:
7141
7110
  self.sort = 'Node'
7142
7111
  else:
7143
- mode = 1
7144
7112
  self.sort = 'Edge'
7145
7113
 
7146
7114
 
@@ -7658,39 +7626,6 @@ class RadialDialog(QDialog):
7658
7626
  except Exception as e:
7659
7627
  print(f"An error occurred: {e}")
7660
7628
 
7661
- class DegreeDistDialog(QDialog):
7662
-
7663
- def __init__(self, parent=None):
7664
-
7665
- super().__init__(parent)
7666
- self.setWindowTitle("Degree Distribution Parameters")
7667
- self.setModal(True)
7668
-
7669
- layout = QFormLayout(self)
7670
-
7671
- self.directory = QLineEdit("")
7672
- layout.addRow("Output Directory:", self.directory)
7673
-
7674
- # Add Run button
7675
- run_button = QPushButton("Get Degree Distribution")
7676
- run_button.clicked.connect(self.degreedist)
7677
- layout.addWidget(run_button)
7678
-
7679
- def degreedist(self):
7680
-
7681
- try:
7682
-
7683
- directory = str(self.distance.text()) if self.directory.text().strip() else None
7684
-
7685
- degrees = my_network.degree_distribution(directory = directory)
7686
-
7687
-
7688
- self.parent().format_for_upperright_table(degrees, 'Degree (k)', 'Proportion of nodes with degree (p(k))', title = 'Degree Distribution Analysis')
7689
-
7690
- self.accept()
7691
-
7692
- except Exception as e:
7693
- print(f"An error occurred: {e}")
7694
7629
 
7695
7630
  class NearNeighDialog(QDialog):
7696
7631
  def __init__(self, parent=None):
@@ -7725,9 +7660,12 @@ class NearNeighDialog(QDialog):
7725
7660
  self.num = QLineEdit("1")
7726
7661
  identities_layout.addRow("Number of Nearest Neighbors to Evaluate Per Node?:", self.num)
7727
7662
 
7728
-
7729
- main_layout.addWidget(identities_group)
7663
+ self.centroids = QPushButton("Centroids")
7664
+ self.centroids.setCheckable(True)
7665
+ self.centroids.setChecked(True)
7666
+ identities_layout.addRow("Use Centroids? (Recommended for spheroids) Deselecting finds true nearest neighbors for mask but will be slower, and will only support a single nearest neighbor calculation for each root (rather than an avg)", self.centroids)
7730
7667
 
7668
+ main_layout.addWidget(identities_group)
7731
7669
 
7732
7670
  # Optional Heatmap group box
7733
7671
  heatmap_group = QGroupBox("Optional Heatmap")
@@ -7761,39 +7699,71 @@ class NearNeighDialog(QDialog):
7761
7699
 
7762
7700
  main_layout.addWidget(quant_group)
7763
7701
 
7764
- # Get Distribution group box
7702
+ # Get Distribution group box - ENHANCED STYLING
7765
7703
  distribution_group = QGroupBox("Get Distribution")
7766
7704
  distribution_layout = QVBoxLayout(distribution_group)
7767
7705
 
7768
- run_button = QPushButton("Get Average Nearest Neighbor (Plus Distribution)")
7706
+ run_button = QPushButton("🔍 Get Average Nearest Neighbor (Plus Distribution)")
7707
+ # Style for primary action - blue with larger font
7708
+ run_button.setStyleSheet("""
7709
+ QPushButton {
7710
+ background-color: #2196F3;
7711
+ color: white;
7712
+ border: none;
7713
+ padding: 12px 20px;
7714
+ font-size: 14px;
7715
+ font-weight: bold;
7716
+ border-radius: 6px;
7717
+ }
7718
+ QPushButton:hover {
7719
+ background-color: #1976D2;
7720
+ }
7721
+ QPushButton:pressed {
7722
+ background-color: #0D47A1;
7723
+ }
7724
+ """)
7769
7725
  run_button.clicked.connect(self.run)
7770
7726
  distribution_layout.addWidget(run_button)
7771
7727
 
7772
7728
  main_layout.addWidget(distribution_group)
7773
7729
 
7774
- # Get All Averages group box (only if node_identities exists)
7730
+ # Get All Averages group box - ENHANCED STYLING (only if node_identities exists)
7775
7731
  if my_network.node_identities is not None:
7776
7732
  averages_group = QGroupBox("Get All Averages")
7777
7733
  averages_layout = QVBoxLayout(averages_group)
7778
7734
 
7779
- run_button2 = QPushButton("Get Average Nearest All ID Combinations (No Distribution, No Heatmap)")
7735
+ run_button2 = QPushButton("📊 Get Average Nearest All ID Combinations (No Distribution, No Heatmap)")
7736
+ # Style for secondary action - green with different styling
7737
+ run_button2.setStyleSheet("""
7738
+ QPushButton {
7739
+ background-color: #4CAF50;
7740
+ color: white;
7741
+ border: 2px solid #45a049;
7742
+ padding: 10px 16px;
7743
+ font-size: 13px;
7744
+ font-weight: normal;
7745
+ border-radius: 8px;
7746
+ }
7747
+ QPushButton:hover {
7748
+ background-color: #45a049;
7749
+ border-color: #3d8b40;
7750
+ }
7751
+ QPushButton:pressed {
7752
+ background-color: #3d8b40;
7753
+ }
7754
+ """)
7780
7755
  run_button2.clicked.connect(self.run2)
7781
7756
  averages_layout.addWidget(run_button2)
7782
7757
 
7783
7758
  main_layout.addWidget(averages_group)
7784
7759
 
7785
7760
  def toggle_map(self):
7786
-
7787
7761
  if self.numpy.isChecked():
7788
-
7789
7762
  if not self.map.isChecked():
7790
-
7791
7763
  self.map.click()
7792
7764
 
7793
7765
  def run(self):
7794
-
7795
7766
  try:
7796
-
7797
7767
  try:
7798
7768
  root = self.root.currentText()
7799
7769
  except:
@@ -7808,6 +7778,9 @@ class NearNeighDialog(QDialog):
7808
7778
  numpy = self.numpy.isChecked()
7809
7779
  num = int(self.num.text()) if self.num.text().strip() else 1
7810
7780
  quant = self.quant.isChecked()
7781
+ centroids = self.centroids.isChecked()
7782
+ if not centroids:
7783
+ num = 1
7811
7784
 
7812
7785
  if root is not None and targ is not None:
7813
7786
  title = f"Nearest {num} Neighbor(s) Distance of {targ} from {root}"
@@ -7818,15 +7791,15 @@ class NearNeighDialog(QDialog):
7818
7791
  header = f"Shortest Distance to Closest {num} Nodes"
7819
7792
  header2 = "Root Node ID"
7820
7793
 
7821
- if my_network.node_centroids is None:
7794
+ if centroids and my_network.node_centroids is None:
7822
7795
  self.parent().show_centroid_dialog()
7823
7796
  if my_network.node_centroids is None:
7824
7797
  return
7825
7798
 
7826
7799
  if not numpy:
7827
- avg, output, quant_overlay = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num, heatmap = heatmap, threed = threed, quant = quant)
7800
+ avg, output, quant_overlay = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num, heatmap = heatmap, threed = threed, quant = quant, centroids = centroids)
7828
7801
  else:
7829
- avg, output, overlay, quant_overlay = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num, heatmap = heatmap, threed = threed, numpy = True, quant = quant)
7802
+ avg, output, overlay, quant_overlay = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num, heatmap = heatmap, threed = threed, numpy = True, quant = quant, centroids = centroids)
7830
7803
  self.parent().load_channel(3, overlay, data = True)
7831
7804
 
7832
7805
  if quant_overlay is not None:
@@ -7840,27 +7813,24 @@ class NearNeighDialog(QDialog):
7840
7813
  except Exception as e:
7841
7814
  import traceback
7842
7815
  print(traceback.format_exc())
7843
-
7844
7816
  print(f"Error: {e}")
7845
7817
 
7846
7818
  def run2(self):
7847
-
7848
7819
  try:
7849
-
7850
7820
  available = list(set(my_network.node_identities.values()))
7851
-
7852
7821
  num = int(self.num.text()) if self.num.text().strip() else 1
7853
7822
 
7823
+ centroids = self.centroids.isChecked()
7824
+ if not centroids:
7825
+ num = 1
7826
+
7854
7827
  output_dict = {}
7855
7828
 
7856
7829
  while len(available) > 1:
7857
-
7858
7830
  root = available[0]
7859
7831
 
7860
7832
  for targ in available:
7861
-
7862
- avg, _, _ = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num)
7863
-
7833
+ avg, _, _ = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num, centroids = centroids)
7864
7834
  output_dict[f"{root} vs {targ}"] = avg
7865
7835
 
7866
7836
  del available[0]
@@ -7870,7 +7840,6 @@ class NearNeighDialog(QDialog):
7870
7840
  self.accept()
7871
7841
 
7872
7842
  except Exception as e:
7873
-
7874
7843
  print(f"Error: {e}")
7875
7844
 
7876
7845
 
@@ -10264,7 +10233,14 @@ class SegmentationWorker(QThread):
10264
10233
  self.chunks_since_update = 0
10265
10234
  self.last_update = current_time
10266
10235
 
10236
+ current_xlim = self.machine_window.parent().ax.get_xlim()
10237
+
10238
+ current_ylim = self.machine_window.parent().ax.get_ylim()
10239
+
10240
+ self.machine_window.parent().update_display(preserve_zoom=(current_xlim, current_ylim))
10241
+
10267
10242
  self.finished.emit()
10243
+
10268
10244
 
10269
10245
  except Exception as e:
10270
10246
  print(f"Error in segmentation: {e}")
@@ -11331,7 +11307,7 @@ class SkeletonizeDialog(QDialog):
11331
11307
  )
11332
11308
 
11333
11309
  if remove > 0:
11334
- result = n3d.remove_branches(result, remove)
11310
+ result = n3d.remove_branches_new(result, remove)
11335
11311
 
11336
11312
 
11337
11313
  # Update both the display data and the network object
@@ -13128,6 +13104,409 @@ class ProxDialog(QDialog):
13128
13104
  print(traceback.format_exc())
13129
13105
 
13130
13106
 
13107
+ class HistogramSelector(QWidget):
13108
+ def __init__(self, network_analysis_instance):
13109
+ super().__init__()
13110
+ self.network_analysis = network_analysis_instance
13111
+ self.G = my_network.network
13112
+ self.init_ui()
13113
+
13114
+ def init_ui(self):
13115
+ self.setWindowTitle('Network Analysis - Histogram Selector')
13116
+ self.setGeometry(300, 300, 400, 700) # Increased height for more buttons
13117
+
13118
+ layout = QVBoxLayout()
13119
+
13120
+ # Title label
13121
+ title_label = QLabel('Select Histogram to Generate:')
13122
+ title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
13123
+ title_label.setStyleSheet("font-size: 16px; font-weight: bold; margin: 10px;")
13124
+ layout.addWidget(title_label)
13125
+
13126
+ # Create buttons for each histogram type
13127
+ self.create_button(layout, "Shortest Path Length Distribution", self.shortest_path_histogram)
13128
+ self.create_button(layout, "Degree Centrality", self.degree_centrality_histogram)
13129
+ self.create_button(layout, "Betweenness Centrality", self.betweenness_centrality_histogram)
13130
+ self.create_button(layout, "Closeness Centrality", self.closeness_centrality_histogram)
13131
+ self.create_button(layout, "Eigenvector Centrality", self.eigenvector_centrality_histogram)
13132
+ self.create_button(layout, "Clustering Coefficient", self.clustering_coefficient_histogram)
13133
+ self.create_button(layout, "Degree Distribution", self.degree_distribution_histogram)
13134
+ self.create_button(layout, "Node Connectivity", self.node_connectivity_histogram)
13135
+ self.create_button(layout, "Eccentricity", self.eccentricity_histogram)
13136
+ self.create_button(layout, "K-Core Decomposition", self.kcore_histogram)
13137
+ self.create_button(layout, "Triangle Count", self.triangle_count_histogram)
13138
+ self.create_button(layout, "Load Centrality", self.load_centrality_histogram)
13139
+ self.create_button(layout, "Communicability Betweenness Centrality", self.communicability_centrality_histogram)
13140
+ self.create_button(layout, "Harmonic Centrality", self.harmonic_centrality_histogram)
13141
+ self.create_button(layout, "Current Flow Betweenness", self.current_flow_betweenness_histogram)
13142
+ self.create_button(layout, "Dispersion", self.dispersion_histogram)
13143
+ self.create_button(layout, "Network Bridges", self.bridges_analysis)
13144
+
13145
+ # Close button
13146
+ close_button = QPushButton('Close')
13147
+ close_button.clicked.connect(self.close)
13148
+ close_button.setStyleSheet("QPushButton { background-color: #f44336; color: white; font-weight: bold; }")
13149
+ layout.addWidget(close_button)
13150
+
13151
+ self.setLayout(layout)
13152
+
13153
+ def create_button(self, layout, text, callback):
13154
+ button = QPushButton(text)
13155
+ button.clicked.connect(callback)
13156
+ button.setMinimumHeight(40)
13157
+ button.setStyleSheet("""
13158
+ QPushButton {
13159
+ background-color: #4CAF50;
13160
+ color: white;
13161
+ border: none;
13162
+ padding: 10px;
13163
+ font-size: 14px;
13164
+ font-weight: bold;
13165
+ border-radius: 5px;
13166
+ }
13167
+ QPushButton:hover {
13168
+ background-color: #45a049;
13169
+ }
13170
+ QPushButton:pressed {
13171
+ background-color: #3d8b40;
13172
+ }
13173
+ """)
13174
+ layout.addWidget(button)
13175
+
13176
+ def shortest_path_histogram(self):
13177
+ try:
13178
+ shortest_path_lengths = dict(nx.all_pairs_shortest_path_length(self.G))
13179
+ diameter = max(nx.eccentricity(self.G, sp=shortest_path_lengths).values())
13180
+ path_lengths = np.zeros(diameter + 1, dtype=int)
13181
+
13182
+ for pls in shortest_path_lengths.values():
13183
+ pl, cnts = np.unique(list(pls.values()), return_counts=True)
13184
+ path_lengths[pl] += cnts
13185
+
13186
+ freq_percent = 100 * path_lengths[1:] / path_lengths[1:].sum()
13187
+
13188
+ fig, ax = plt.subplots(figsize=(15, 8))
13189
+ ax.bar(np.arange(1, diameter + 1), height=freq_percent)
13190
+ ax.set_title(
13191
+ "Distribution of shortest path length in G", fontdict={"size": 35}, loc="center"
13192
+ )
13193
+ ax.set_xlabel("Shortest Path Length", fontdict={"size": 22})
13194
+ ax.set_ylabel("Frequency (%)", fontdict={"size": 22})
13195
+ plt.show()
13196
+
13197
+ freq_dict = {freq: length for length, freq in enumerate(freq_percent, start=1)}
13198
+ self.network_analysis.format_for_upperright_table(freq_dict, metric='Frequency (%)',
13199
+ value='Shortest Path Length',
13200
+ title="Distribution of shortest path length in G")
13201
+ except Exception as e:
13202
+ print(f"Error generating shortest path histogram: {e}")
13203
+
13204
+ def degree_centrality_histogram(self):
13205
+ try:
13206
+ degree_centrality = nx.centrality.degree_centrality(self.G)
13207
+ plt.figure(figsize=(15, 8))
13208
+ plt.hist(degree_centrality.values(), bins=25)
13209
+ plt.xticks(ticks=[0, 0.025, 0.05, 0.1, 0.15, 0.2])
13210
+ plt.title("Degree Centrality Histogram ", fontdict={"size": 35}, loc="center")
13211
+ plt.xlabel("Degree Centrality", fontdict={"size": 20})
13212
+ plt.ylabel("Counts", fontdict={"size": 20})
13213
+ plt.show()
13214
+ self.network_analysis.format_for_upperright_table(degree_centrality, metric='Node',
13215
+ value='Degree Centrality',
13216
+ title="Degree Centrality Table")
13217
+ except Exception as e:
13218
+ print(f"Error generating degree centrality histogram: {e}")
13219
+
13220
+ def betweenness_centrality_histogram(self):
13221
+ try:
13222
+ betweenness_centrality = nx.centrality.betweenness_centrality(self.G)
13223
+ plt.figure(figsize=(15, 8))
13224
+ plt.hist(betweenness_centrality.values(), bins=100)
13225
+ plt.xticks(ticks=[0, 0.02, 0.1, 0.2, 0.3, 0.4, 0.5])
13226
+ plt.title("Betweenness Centrality Histogram ", fontdict={"size": 35}, loc="center")
13227
+ plt.xlabel("Betweenness Centrality", fontdict={"size": 20})
13228
+ plt.ylabel("Counts", fontdict={"size": 20})
13229
+ plt.show()
13230
+ self.network_analysis.format_for_upperright_table(betweenness_centrality, metric='Node',
13231
+ value='Betweenness Centrality',
13232
+ title="Betweenness Centrality Table")
13233
+ except Exception as e:
13234
+ print(f"Error generating betweenness centrality histogram: {e}")
13235
+
13236
+ def closeness_centrality_histogram(self):
13237
+ try:
13238
+ closeness_centrality = nx.centrality.closeness_centrality(self.G)
13239
+ plt.figure(figsize=(15, 8))
13240
+ plt.hist(closeness_centrality.values(), bins=60)
13241
+ plt.title("Closeness Centrality Histogram ", fontdict={"size": 35}, loc="center")
13242
+ plt.xlabel("Closeness Centrality", fontdict={"size": 20})
13243
+ plt.ylabel("Counts", fontdict={"size": 20})
13244
+ plt.show()
13245
+ self.network_analysis.format_for_upperright_table(closeness_centrality, metric='Node',
13246
+ value='Closeness Centrality',
13247
+ title="Closeness Centrality Table")
13248
+ except Exception as e:
13249
+ print(f"Error generating closeness centrality histogram: {e}")
13250
+
13251
+ def eigenvector_centrality_histogram(self):
13252
+ try:
13253
+ eigenvector_centrality = nx.centrality.eigenvector_centrality(self.G)
13254
+ plt.figure(figsize=(15, 8))
13255
+ plt.hist(eigenvector_centrality.values(), bins=60)
13256
+ plt.xticks(ticks=[0, 0.01, 0.02, 0.04, 0.06, 0.08])
13257
+ plt.title("Eigenvector Centrality Histogram ", fontdict={"size": 35}, loc="center")
13258
+ plt.xlabel("Eigenvector Centrality", fontdict={"size": 20})
13259
+ plt.ylabel("Counts", fontdict={"size": 20})
13260
+ plt.show()
13261
+ self.network_analysis.format_for_upperright_table(eigenvector_centrality, metric='Node',
13262
+ value='Eigenvector Centrality',
13263
+ title="Eigenvector Centrality Table")
13264
+ except Exception as e:
13265
+ print(f"Error generating eigenvector centrality histogram: {e}")
13266
+
13267
+ def clustering_coefficient_histogram(self):
13268
+ try:
13269
+ clusters = nx.clustering(self.G)
13270
+ plt.figure(figsize=(15, 8))
13271
+ plt.hist(clusters.values(), bins=50)
13272
+ plt.title("Clustering Coefficient Histogram ", fontdict={"size": 35}, loc="center")
13273
+ plt.xlabel("Clustering Coefficient", fontdict={"size": 20})
13274
+ plt.ylabel("Counts", fontdict={"size": 20})
13275
+ plt.show()
13276
+ self.network_analysis.format_for_upperright_table(clusters, metric='Node',
13277
+ value='Clustering Coefficient',
13278
+ title="Clustering Coefficient Table")
13279
+ except Exception as e:
13280
+ print(f"Error generating clustering coefficient histogram: {e}")
13281
+
13282
+ def bridges_analysis(self):
13283
+ try:
13284
+ bridges = list(nx.bridges(self.G))
13285
+ self.network_analysis.format_for_upperright_table(bridges, metric='Node Pair',
13286
+ title="Bridges")
13287
+ except Exception as e:
13288
+ print(f"Error generating bridges analysis: {e}")
13289
+
13290
+ def degree_distribution_histogram(self):
13291
+ """Raw degree distribution - very useful for understanding network topology"""
13292
+ try:
13293
+ degrees = [self.G.degree(n) for n in self.G.nodes()]
13294
+ plt.figure(figsize=(15, 8))
13295
+ plt.hist(degrees, bins=max(30, int(np.sqrt(len(degrees)))), alpha=0.7)
13296
+ plt.title("Degree Distribution", fontdict={"size": 35}, loc="center")
13297
+ plt.xlabel("Degree", fontdict={"size": 20})
13298
+ plt.ylabel("Frequency", fontdict={"size": 20})
13299
+ plt.yscale('log') # Often useful for degree distributions
13300
+ plt.show()
13301
+
13302
+ degree_dict = {node: deg for node, deg in self.G.degree()}
13303
+ self.network_analysis.format_for_upperright_table(degree_dict, metric='Node',
13304
+ value='Degree', title="Degree Distribution Table")
13305
+ except Exception as e:
13306
+ print(f"Error generating degree distribution histogram: {e}")
13307
+
13308
+
13309
+ def node_connectivity_histogram(self):
13310
+ """Local node connectivity - minimum number of nodes that must be removed to disconnect neighbors"""
13311
+ try:
13312
+ if self.G.number_of_nodes() > 500: # Skip for large networks (computationally expensive)
13313
+ print("Note this analysis may be slow for large network (>500 nodes)")
13314
+ #return
13315
+
13316
+ connectivity = {}
13317
+ for node in self.G.nodes():
13318
+ neighbors = list(self.G.neighbors(node))
13319
+ if len(neighbors) > 1:
13320
+ connectivity[node] = nx.node_connectivity(self.G, neighbors[0], neighbors[1])
13321
+ else:
13322
+ connectivity[node] = 0
13323
+
13324
+ plt.figure(figsize=(15, 8))
13325
+ plt.hist(connectivity.values(), bins=20, alpha=0.7)
13326
+ plt.title("Node Connectivity Distribution", fontdict={"size": 35}, loc="center")
13327
+ plt.xlabel("Node Connectivity", fontdict={"size": 20})
13328
+ plt.ylabel("Frequency", fontdict={"size": 20})
13329
+ plt.show()
13330
+ self.network_analysis.format_for_upperright_table(connectivity, metric='Node',
13331
+ value='Connectivity', title="Node Connectivity Table")
13332
+ except Exception as e:
13333
+ print(f"Error generating node connectivity histogram: {e}")
13334
+
13335
+ def eccentricity_histogram(self):
13336
+ """Eccentricity - maximum distance from a node to any other node"""
13337
+ try:
13338
+ if not nx.is_connected(self.G):
13339
+ print("Graph is not connected. Using largest connected component.")
13340
+ largest_cc = max(nx.connected_components(self.G), key=len)
13341
+ G_cc = self.G.subgraph(largest_cc)
13342
+ eccentricity = nx.eccentricity(G_cc)
13343
+ else:
13344
+ eccentricity = nx.eccentricity(self.G)
13345
+
13346
+ plt.figure(figsize=(15, 8))
13347
+ plt.hist(eccentricity.values(), bins=20, alpha=0.7)
13348
+ plt.title("Eccentricity Distribution", fontdict={"size": 35}, loc="center")
13349
+ plt.xlabel("Eccentricity", fontdict={"size": 20})
13350
+ plt.ylabel("Frequency", fontdict={"size": 20})
13351
+ plt.show()
13352
+ self.network_analysis.format_for_upperright_table(eccentricity, metric='Node',
13353
+ value='Eccentricity', title="Eccentricity Table")
13354
+ except Exception as e:
13355
+ print(f"Error generating eccentricity histogram: {e}")
13356
+
13357
+ def kcore_histogram(self):
13358
+ """K-core decomposition - identifies cohesive subgroups"""
13359
+ try:
13360
+ kcore = nx.core_number(self.G)
13361
+ plt.figure(figsize=(15, 8))
13362
+ plt.hist(kcore.values(), bins=max(5, max(kcore.values())), alpha=0.7)
13363
+ plt.title("K-Core Distribution", fontdict={"size": 35}, loc="center")
13364
+ plt.xlabel("K-Core Number", fontdict={"size": 20})
13365
+ plt.ylabel("Frequency", fontdict={"size": 20})
13366
+ plt.show()
13367
+ self.network_analysis.format_for_upperright_table(kcore, metric='Node',
13368
+ value='K-Core', title="K-Core Table")
13369
+ except Exception as e:
13370
+ print(f"Error generating k-core histogram: {e}")
13371
+
13372
+ def triangle_count_histogram(self):
13373
+ """Number of triangles each node participates in"""
13374
+ try:
13375
+ triangles = nx.triangles(self.G)
13376
+ plt.figure(figsize=(15, 8))
13377
+ plt.hist(triangles.values(), bins=30, alpha=0.7)
13378
+ plt.title("Triangle Count Distribution", fontdict={"size": 35}, loc="center")
13379
+ plt.xlabel("Number of Triangles", fontdict={"size": 20})
13380
+ plt.ylabel("Frequency", fontdict={"size": 20})
13381
+ plt.show()
13382
+ self.network_analysis.format_for_upperright_table(triangles, metric='Node',
13383
+ value='Triangle Count', title="Triangle Count Table")
13384
+ except Exception as e:
13385
+ print(f"Error generating triangle count histogram: {e}")
13386
+
13387
+ def load_centrality_histogram(self):
13388
+ """Load centrality - fraction of shortest paths passing through each node"""
13389
+ try:
13390
+ if self.G.number_of_nodes() > 1000: # Skip for very large networks
13391
+ print("Note this analysis may be slow for large network (>1000 nodes)")
13392
+ #return
13393
+
13394
+ load_centrality = nx.load_centrality(self.G)
13395
+ plt.figure(figsize=(15, 8))
13396
+ plt.hist(load_centrality.values(), bins=50, alpha=0.7)
13397
+ plt.title("Load Centrality Distribution", fontdict={"size": 35}, loc="center")
13398
+ plt.xlabel("Load Centrality", fontdict={"size": 20})
13399
+ plt.ylabel("Frequency", fontdict={"size": 20})
13400
+ plt.show()
13401
+ self.network_analysis.format_for_upperright_table(load_centrality, metric='Node',
13402
+ value='Load Centrality', title="Load Centrality Table")
13403
+ except Exception as e:
13404
+ print(f"Error generating load centrality histogram: {e}")
13405
+
13406
+ def communicability_centrality_histogram(self):
13407
+ """Communicability centrality - based on communicability between nodes"""
13408
+ try:
13409
+ if self.G.number_of_nodes() > 500: # Skip for large networks (memory intensive)
13410
+ print("Note this analysis may be slow for large network (>500 nodes)")
13411
+ #return
13412
+
13413
+ # Use the correct function name - it's in the communicability module
13414
+ comm_centrality = nx.communicability_betweenness_centrality(self.G)
13415
+ plt.figure(figsize=(15, 8))
13416
+ plt.hist(comm_centrality.values(), bins=50, alpha=0.7)
13417
+ plt.title("Communicability Betweenness Centrality Distribution", fontdict={"size": 35}, loc="center")
13418
+ plt.xlabel("Communicability Betweenness Centrality", fontdict={"size": 20})
13419
+ plt.ylabel("Frequency", fontdict={"size": 20})
13420
+ plt.show()
13421
+ self.network_analysis.format_for_upperright_table(comm_centrality, metric='Node',
13422
+ value='Communicability Betweenness Centrality',
13423
+ title="Communicability Betweenness Centrality Table")
13424
+ except Exception as e:
13425
+ print(f"Error generating communicability betweenness centrality histogram: {e}")
13426
+
13427
+ def harmonic_centrality_histogram(self):
13428
+ """Harmonic centrality - better than closeness for disconnected networks"""
13429
+ try:
13430
+ harmonic_centrality = nx.harmonic_centrality(self.G)
13431
+ plt.figure(figsize=(15, 8))
13432
+ plt.hist(harmonic_centrality.values(), bins=50, alpha=0.7)
13433
+ plt.title("Harmonic Centrality Distribution", fontdict={"size": 35}, loc="center")
13434
+ plt.xlabel("Harmonic Centrality", fontdict={"size": 20})
13435
+ plt.ylabel("Frequency", fontdict={"size": 20})
13436
+ plt.show()
13437
+ self.network_analysis.format_for_upperright_table(harmonic_centrality, metric='Node',
13438
+ value='Harmonic Centrality',
13439
+ title="Harmonic Centrality Table")
13440
+ except Exception as e:
13441
+ print(f"Error generating harmonic centrality histogram: {e}")
13442
+
13443
+ def current_flow_betweenness_histogram(self):
13444
+ """Current flow betweenness - models network as electrical circuit"""
13445
+ try:
13446
+ if self.G.number_of_nodes() > 500: # Skip for large networks (computationally expensive)
13447
+ print("Note this analysis may be slow for large network (>500 nodes)")
13448
+ #return
13449
+
13450
+ current_flow = nx.current_flow_betweenness_centrality(self.G)
13451
+ plt.figure(figsize=(15, 8))
13452
+ plt.hist(current_flow.values(), bins=50, alpha=0.7)
13453
+ plt.title("Current Flow Betweenness Centrality Distribution", fontdict={"size": 35}, loc="center")
13454
+ plt.xlabel("Current Flow Betweenness Centrality", fontdict={"size": 20})
13455
+ plt.ylabel("Frequency", fontdict={"size": 20})
13456
+ plt.show()
13457
+ self.network_analysis.format_for_upperright_table(current_flow, metric='Node',
13458
+ value='Current Flow Betweenness',
13459
+ title="Current Flow Betweenness Table")
13460
+ except Exception as e:
13461
+ print(f"Error generating current flow betweenness histogram: {e}")
13462
+
13463
+ def dispersion_histogram(self):
13464
+ """Dispersion - measures how scattered a node's neighbors are"""
13465
+ try:
13466
+ if self.G.number_of_nodes() > 300: # Skip for large networks (very computationally expensive)
13467
+ print("Note this analysis may be slow for large network (>300 nodes)")
13468
+ #return
13469
+
13470
+ # Calculate average dispersion for each node
13471
+ dispersion_values = {}
13472
+ nodes = list(self.G.nodes())
13473
+
13474
+ for u in nodes:
13475
+ if self.G.degree(u) < 2: # Need at least 2 neighbors for dispersion
13476
+ dispersion_values[u] = 0
13477
+ continue
13478
+
13479
+ # Calculate dispersion for node u with all its neighbors
13480
+ neighbors = list(self.G.neighbors(u))
13481
+ if len(neighbors) < 2:
13482
+ dispersion_values[u] = 0
13483
+ continue
13484
+
13485
+ # Get dispersion scores for this node with all neighbors
13486
+ disp_scores = []
13487
+ for v in neighbors:
13488
+ try:
13489
+ disp_score = nx.dispersion(self.G, u, v)
13490
+ disp_scores.append(disp_score)
13491
+ except:
13492
+ continue
13493
+
13494
+ # Average dispersion for this node
13495
+ dispersion_values[u] = sum(disp_scores) / len(disp_scores) if disp_scores else 0
13496
+
13497
+ plt.figure(figsize=(15, 8))
13498
+ plt.hist(dispersion_values.values(), bins=30, alpha=0.7)
13499
+ plt.title("Average Dispersion Distribution", fontdict={"size": 35}, loc="center")
13500
+ plt.xlabel("Average Dispersion", fontdict={"size": 20})
13501
+ plt.ylabel("Frequency", fontdict={"size": 20})
13502
+ plt.show()
13503
+ self.network_analysis.format_for_upperright_table(dispersion_values, metric='Node',
13504
+ value='Average Dispersion',
13505
+ title="Average Dispersion Table")
13506
+ except Exception as e:
13507
+ print(f"Error generating dispersion histogram: {e}")
13508
+
13509
+
13131
13510
 
13132
13511
  # Initiating this program from the script line:
13133
13512