nettracer3d 0.8.8__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.

Potentially problematic release.


This version of nettracer3d might be problematic. Click here for more details.

@@ -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
@@ -3161,6 +3161,18 @@ class ImageViewerWindow(QMainWindow):
3161
3161
  # End this stroke but keep session active for continuous painting
3162
3162
  self.painting = False
3163
3163
 
3164
+ if self.erase:
3165
+ if (hasattr(self, 'virtual_draw_operations') and self.virtual_draw_operations) or \
3166
+ (hasattr(self, 'virtual_erase_operations') and self.virtual_erase_operations) or \
3167
+ (hasattr(self, 'current_operation') and self.current_operation):
3168
+ # Finish current operation first
3169
+ if hasattr(self, 'current_operation') and self.current_operation:
3170
+ self.pm.finish_current_virtual_operation()
3171
+ # Now convert to real data
3172
+ self.pm.convert_virtual_strokes_to_data()
3173
+ current_xlim = self.ax.get_xlim()
3174
+ current_ylim = self.ax.get_ylim()
3175
+ self.update_display(preserve_zoom=(current_xlim, current_ylim))
3164
3176
 
3165
3177
  if self.resume:
3166
3178
  self.machine_window.segmentation_worker.resume()
@@ -3459,12 +3471,10 @@ class ImageViewerWindow(QMainWindow):
3459
3471
  stats_menu = analysis_menu.addMenu("Stats")
3460
3472
  allstats_action = stats_menu.addAction("Calculate Generic Network Stats")
3461
3473
  allstats_action.triggered.connect(self.stats)
3462
- histos_action = stats_menu.addAction("Calculate Generic Network Histograms")
3474
+ histos_action = stats_menu.addAction("Network Statistic Histograms")
3463
3475
  histos_action.triggered.connect(self.histos)
3464
3476
  radial_action = stats_menu.addAction("Radial Distribution Analysis")
3465
3477
  radial_action.triggered.connect(self.show_radial_dialog)
3466
- degree_dist_action = stats_menu.addAction("Degree Distribution Analysis")
3467
- degree_dist_action.triggered.connect(self.show_degree_dist_dialog)
3468
3478
  neighbor_id_action = stats_menu.addAction("Identity Distribution of Neighbors")
3469
3479
  neighbor_id_action.triggered.connect(self.show_neighbor_id_dialog)
3470
3480
  ripley_action = stats_menu.addAction("Ripley Clustering Analysis")
@@ -3503,7 +3513,7 @@ class ImageViewerWindow(QMainWindow):
3503
3513
 
3504
3514
  # Process menu
3505
3515
  process_menu = menubar.addMenu("Process")
3506
- calculate_menu = process_menu.addMenu("Calculate")
3516
+ calculate_menu = process_menu.addMenu("Calculate Network")
3507
3517
  calc_all_action = calculate_menu.addAction("Calculate Connectivity Network (Find Node-Edge-Node Network)")
3508
3518
  calc_all_action.triggered.connect(self.show_calc_all_dialog)
3509
3519
  calc_prox_action = calculate_menu.addAction("Calculate Proximity Network (connect nodes by distance)")
@@ -3603,46 +3613,75 @@ class ImageViewerWindow(QMainWindow):
3603
3613
  menubar.setCornerWidget(cam_button, Qt.Corner.TopRightCorner)
3604
3614
 
3605
3615
  def snap(self):
3606
-
3607
3616
  try:
3608
-
3617
+ # Check if we have any data to save
3618
+ data = False
3609
3619
  for thing in self.channel_data:
3610
3620
  if thing is not None:
3611
3621
  data = True
3622
+ break
3612
3623
  if not data:
3613
3624
  return
3614
-
3615
- snap = self.create_composite_for_pan()
3616
-
3625
+
3626
+ # Get filename from user
3617
3627
  filename, _ = QFileDialog.getSaveFileName(
3618
3628
  self,
3619
3629
  f"Save Image As",
3620
- "", # Default directory
3621
- "TIFF Files (*.tif *.tiff);;All Files (*)" # File type filter
3630
+ "",
3631
+ "PNG Files (*.png);;TIFF Files (*.tif *.tiff);;JPEG Files (*.jpg *.jpeg);;All Files (*)"
3622
3632
  )
3623
3633
 
3624
- if filename: # Only proceed if user didn't cancel
3625
- # If user didn't type an extension, add .tif
3626
- if not filename.endswith(('.tif', '.tiff')):
3627
- filename += '.tif'
3628
-
3629
- import tifffile
3630
- tifffile.imwrite(filename, snap)
3631
-
3632
- except:
3633
- 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}")
3634
3660
 
3635
3661
 
3636
3662
  def open_cellpose(self):
3637
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
+
3638
3675
  try:
3639
3676
 
3640
3677
  from . import cellpose_manager
3641
3678
  self.cellpose_launcher = cellpose_manager.CellposeGUILauncher(parent_widget=self)
3642
3679
 
3643
- self.cellpose_launcher.launch_cellpose_gui()
3680
+ self.cellpose_launcher.launch_cellpose_gui(use_3d = use_3d)
3644
3681
 
3645
3682
  except:
3683
+ import traceback
3684
+ print(traceback.format_exc())
3646
3685
  pass
3647
3686
 
3648
3687
 
@@ -3668,106 +3707,26 @@ class ImageViewerWindow(QMainWindow):
3668
3707
  print(f"Error finding stats: {e}")
3669
3708
 
3670
3709
  def histos(self):
3671
-
3672
- """from networkx documentation"""
3673
-
3710
+ """
3711
+ Show a PyQt6 window with buttons to select which histogram to generate.
3712
+ Only calculates the histogram that the user selects.
3713
+ """
3674
3714
  try:
3675
-
3676
- G = my_network.network
3677
-
3678
- shortest_path_lengths = dict(nx.all_pairs_shortest_path_length(G))
3679
- diameter = max(nx.eccentricity(G, sp=shortest_path_lengths).values())
3680
- # We know the maximum shortest path length (the diameter), so create an array
3681
- # to store values from 0 up to (and including) diameter
3682
- path_lengths = np.zeros(diameter + 1, dtype=int)
3683
-
3684
-
3685
-
3686
- # Extract the frequency of shortest path lengths between two nodes
3687
- for pls in shortest_path_lengths.values():
3688
- pl, cnts = np.unique(list(pls.values()), return_counts=True)
3689
- path_lengths[pl] += cnts
3690
-
3691
- # Express frequency distribution as a percentage (ignoring path lengths of 0)
3692
- freq_percent = 100 * path_lengths[1:] / path_lengths[1:].sum()
3693
-
3694
- # Plot the frequency distribution (ignoring path lengths of 0) as a percentage
3695
- fig, ax = plt.subplots(figsize=(15, 8))
3696
- ax.bar(np.arange(1, diameter + 1), height=freq_percent)
3697
- ax.set_title(
3698
- "Distribution of shortest path length in G", fontdict={"size": 35}, loc="center"
3699
- )
3700
- ax.set_xlabel("Shortest Path Length", fontdict={"size": 22})
3701
- ax.set_ylabel("Frequency (%)", fontdict={"size": 22})
3702
-
3703
- plt.show()
3704
- freq_dict = {freq: length for length, freq in enumerate(freq_percent, start=1)}
3705
- self.format_for_upperright_table(freq_dict, metric='Frequency (%)', value='Shortest Path Length', title="Distribution of shortest path length in G")
3706
-
3707
- degree_centrality = nx.centrality.degree_centrality(G)
3708
- plt.figure(figsize=(15, 8))
3709
- plt.hist(degree_centrality.values(), bins=25)
3710
- plt.xticks(ticks=[0, 0.025, 0.05, 0.1, 0.15, 0.2]) # set the x axis ticks
3711
- plt.title("Degree Centrality Histogram ", fontdict={"size": 35}, loc="center")
3712
- plt.xlabel("Degree Centrality", fontdict={"size": 20})
3713
- plt.ylabel("Counts", fontdict={"size": 20})
3714
- plt.show()
3715
- self.format_for_upperright_table(degree_centrality, metric='Node', value='Degree Centrality', title="Degree Centrality Table")
3716
-
3717
-
3718
- betweenness_centrality = nx.centrality.betweenness_centrality(
3719
- G
3720
- )
3721
- plt.figure(figsize=(15, 8))
3722
- plt.hist(betweenness_centrality.values(), bins=100)
3723
- plt.xticks(ticks=[0, 0.02, 0.1, 0.2, 0.3, 0.4, 0.5]) # set the x axis ticks
3724
- plt.title("Betweenness Centrality Histogram ", fontdict={"size": 35}, loc="center")
3725
- plt.xlabel("Betweenness Centrality", fontdict={"size": 20})
3726
- plt.ylabel("Counts", fontdict={"size": 20})
3727
- plt.show()
3728
- self.format_for_upperright_table(betweenness_centrality, metric='Node', value='Betweenness Centrality', title="Betweenness Centrality Table")
3729
-
3730
-
3731
- closeness_centrality = nx.centrality.closeness_centrality(
3732
- G
3733
- )
3734
- plt.figure(figsize=(15, 8))
3735
- plt.hist(closeness_centrality.values(), bins=60)
3736
- plt.title("Closeness Centrality Histogram ", fontdict={"size": 35}, loc="center")
3737
- plt.xlabel("Closeness Centrality", fontdict={"size": 20})
3738
- plt.ylabel("Counts", fontdict={"size": 20})
3739
- plt.show()
3740
- self.format_for_upperright_table(closeness_centrality, metric='Node', value='Closeness Centrality', title="Closeness Centrality Table")
3741
-
3742
-
3743
- eigenvector_centrality = nx.centrality.eigenvector_centrality(
3744
- G
3745
- )
3746
- plt.figure(figsize=(15, 8))
3747
- plt.hist(eigenvector_centrality.values(), bins=60)
3748
- plt.xticks(ticks=[0, 0.01, 0.02, 0.04, 0.06, 0.08]) # set the x axis ticks
3749
- plt.title("Eigenvector Centrality Histogram ", fontdict={"size": 35}, loc="center")
3750
- plt.xlabel("Eigenvector Centrality", fontdict={"size": 20})
3751
- plt.ylabel("Counts", fontdict={"size": 20})
3752
- plt.show()
3753
- self.format_for_upperright_table(eigenvector_centrality, metric='Node', value='Eigenvector Centrality', title="Eigenvector Centrality Table")
3754
-
3755
-
3756
-
3757
- clusters = nx.clustering(G)
3758
- plt.figure(figsize=(15, 8))
3759
- plt.hist(clusters.values(), bins=50)
3760
- plt.title("Clustering Coefficient Histogram ", fontdict={"size": 35}, loc="center")
3761
- plt.xlabel("Clustering Coefficient", fontdict={"size": 20})
3762
- plt.ylabel("Counts", fontdict={"size": 20})
3763
- plt.show()
3764
- self.format_for_upperright_table(clusters, metric='Node', value='Clustering Coefficient', title="Clustering Coefficient Table")
3765
-
3766
- bridges = list(nx.bridges(G))
3767
- self.format_for_upperright_table(bridges, metric = 'Node Pair', title="Bridges")
3768
-
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
+
3769
3728
  except Exception as e:
3770
- print(f"Error generating histograms: {e}")
3729
+ print(f"Error creating histogram selector: {e}")
3771
3730
 
3772
3731
  def volumes(self):
3773
3732
 
@@ -4779,7 +4738,7 @@ class ImageViewerWindow(QMainWindow):
4779
4738
  try:
4780
4739
  #if len(self.channel_data[channel_index].shape) == 4:
4781
4740
  if 1 in self.channel_data[channel_index].shape:
4782
- print("Removing singleton dimension (I am assuming this is a channel dimension?)")
4741
+ #print("Removing singleton dimension (I am assuming this is a channel dimension?)")
4783
4742
  self.channel_data[channel_index] = np.squeeze(self.channel_data[channel_index])
4784
4743
  except:
4785
4744
  pass
@@ -4907,6 +4866,14 @@ class ImageViewerWindow(QMainWindow):
4907
4866
  self.original_ylim, self.original_xlim = (self.shape[1] + 0.5, -0.5), (-0.5, self.shape[2] - 0.5)
4908
4867
  #print(self.original_xlim)
4909
4868
 
4869
+ self.completed_paint_strokes = [] #Reset pending paint operations
4870
+ self.current_stroke_points = []
4871
+ self.current_stroke_type = None
4872
+ self.virtual_draw_operations = []
4873
+ self.virtual_erase_operations = []
4874
+ self.current_operation = []
4875
+ self.current_operation_type = None
4876
+
4910
4877
  if not end_paint:
4911
4878
 
4912
4879
  self.update_display(reset_resize = reset_resize, preserve_zoom = preserve_zoom)
@@ -5143,8 +5110,8 @@ class ImageViewerWindow(QMainWindow):
5143
5110
  if self.mini_overlay == True: #If we are rendering the highlight overlay for selected values one at a time.
5144
5111
  self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
5145
5112
  self.update_display(preserve_zoom=view_settings)
5146
- if self.machine_window is not None:
5147
- self.machine_window.poke_segmenter()
5113
+ #if self.machine_window is not None:
5114
+ #self.machine_window.poke_segmenter()
5148
5115
  self.pending_slice = None
5149
5116
 
5150
5117
  def update_brightness(self, channel_index, values):
@@ -5251,21 +5218,38 @@ class ImageViewerWindow(QMainWindow):
5251
5218
  else:
5252
5219
  current_image = self.channel_data[channel]
5253
5220
 
5254
- if is_rgb and self.channel_data[channel].shape[-1] == 3:
5255
- # For RGB images, just display directly without colormap
5256
- self.ax.imshow(current_image,
5257
- alpha=0.7)
5258
- elif is_rgb and self.channel_data[channel].shape[-1] == 4:
5259
- self.ax.imshow(current_image) #For images that already have an alpha value and RGB, don't update alpha
5260
-
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
+
5261
5246
  else:
5262
- # Regular channel processing with colormap
5247
+ # Regular channel processing with colormap (your existing code)
5263
5248
  # Calculate brightness/contrast limits from entire volume
5264
- if self.min_max[channel][0] == None:
5265
- self.min_max[channel][0] = np.min(channel)
5266
- if self.min_max[channel][1] == None:
5267
- self.min_max[channel][1] = np.max(channel)
5268
-
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])
5269
5253
  img_min = self.min_max[channel][0]
5270
5254
  img_max = self.min_max[channel][1]
5271
5255
 
@@ -5324,7 +5308,7 @@ class ImageViewerWindow(QMainWindow):
5324
5308
  )
5325
5309
  self.ax.imshow(self.mini_overlay_data,
5326
5310
  cmap=highlight_cmap,
5327
- alpha=0.5)
5311
+ alpha=0.8)
5328
5312
  elif self.highlight_overlay is not None and self.highlight and self.machine_window is None:
5329
5313
  highlight_slice = self.highlight_overlay[self.current_slice]
5330
5314
  highlight_cmap = LinearSegmentedColormap.from_list(
@@ -5333,7 +5317,7 @@ class ImageViewerWindow(QMainWindow):
5333
5317
  )
5334
5318
  self.ax.imshow(highlight_slice,
5335
5319
  cmap=highlight_cmap,
5336
- alpha=0.5)
5320
+ alpha=0.8)
5337
5321
  elif self.highlight_overlay is not None and self.highlight:
5338
5322
  highlight_slice = self.highlight_overlay[self.current_slice]
5339
5323
  highlight_cmap = LinearSegmentedColormap.from_list(
@@ -5346,7 +5330,7 @@ class ImageViewerWindow(QMainWindow):
5346
5330
  cmap=highlight_cmap,
5347
5331
  vmin=0,
5348
5332
  vmax=2, # Important: set vmax to 2 to accommodate both values
5349
- alpha=0.5)
5333
+ alpha=0.3)
5350
5334
 
5351
5335
  if self.channel_data[4] is not None:
5352
5336
 
@@ -5514,10 +5498,6 @@ class ImageViewerWindow(QMainWindow):
5514
5498
  dialog = RadialDialog(self)
5515
5499
  dialog.exec()
5516
5500
 
5517
- def show_degree_dist_dialog(self):
5518
- dialog = DegreeDistDialog(self)
5519
- dialog.exec()
5520
-
5521
5501
  def show_neighbor_id_dialog(self):
5522
5502
  dialog = NeighborIdentityDialog(self)
5523
5503
  dialog.exec()
@@ -6478,6 +6458,13 @@ class BrightnessContrastDialog(QDialog):
6478
6458
  channel_layout.addWidget(slider_container)
6479
6459
  layout.addWidget(channel_widget)
6480
6460
 
6461
+ #debouncing
6462
+ self.debounce_timer = QTimer()
6463
+ self.debounce_timer.setSingleShot(True)
6464
+ self.debounce_timer.timeout.connect(self._apply_pending_updates)
6465
+ self.pending_updates = {}
6466
+ self.debounce_delay = 300 # 300ms delay
6467
+
6481
6468
  # Connect signals
6482
6469
  slider.valueChanged.connect(lambda values, ch=i: self.on_slider_change(ch, values))
6483
6470
  min_input.editingFinished.connect(lambda ch=i: self.on_min_input_change(ch))
@@ -6488,7 +6475,18 @@ class BrightnessContrastDialog(QDialog):
6488
6475
  min_val, max_val = values
6489
6476
  self.min_inputs[channel].setText(str(min_val))
6490
6477
  self.max_inputs[channel].setText(str(max_val))
6491
- self.parent().update_brightness(channel, values)
6478
+
6479
+ # Store the pending update
6480
+ self.pending_updates[channel] = values
6481
+
6482
+ # Restart the debounce timer
6483
+ self.debounce_timer.start(self.debounce_delay)
6484
+
6485
+ def _apply_pending_updates(self):
6486
+ """Apply all pending brightness updates"""
6487
+ for channel, values in self.pending_updates.items():
6488
+ self.parent().update_brightness(channel, values)
6489
+ self.pending_updates.clear()
6492
6490
 
6493
6491
  def on_min_input_change(self, channel):
6494
6492
  """Handle changes to minimum value input"""
@@ -7084,6 +7082,15 @@ class ColorOverlayDialog(QDialog):
7084
7082
 
7085
7083
  layout = QFormLayout(self)
7086
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
+
7087
7094
  self.down_factor = QLineEdit("")
7088
7095
  layout.addRow("down_factor (for speeding up overlay generation - optional):", self.down_factor)
7089
7096
 
@@ -7098,11 +7105,10 @@ class ColorOverlayDialog(QDialog):
7098
7105
 
7099
7106
  down_factor = float(self.down_factor.text()) if self.down_factor.text().strip() else None
7100
7107
 
7101
- if self.parent().active_channel == 0:
7102
- mode = 0
7108
+ mode = self.mode_selector.currentIndex()
7109
+ if mode == 0:
7103
7110
  self.sort = 'Node'
7104
7111
  else:
7105
- mode = 1
7106
7112
  self.sort = 'Edge'
7107
7113
 
7108
7114
 
@@ -7620,39 +7626,6 @@ class RadialDialog(QDialog):
7620
7626
  except Exception as e:
7621
7627
  print(f"An error occurred: {e}")
7622
7628
 
7623
- class DegreeDistDialog(QDialog):
7624
-
7625
- def __init__(self, parent=None):
7626
-
7627
- super().__init__(parent)
7628
- self.setWindowTitle("Degree Distribution Parameters")
7629
- self.setModal(True)
7630
-
7631
- layout = QFormLayout(self)
7632
-
7633
- self.directory = QLineEdit("")
7634
- layout.addRow("Output Directory:", self.directory)
7635
-
7636
- # Add Run button
7637
- run_button = QPushButton("Get Degree Distribution")
7638
- run_button.clicked.connect(self.degreedist)
7639
- layout.addWidget(run_button)
7640
-
7641
- def degreedist(self):
7642
-
7643
- try:
7644
-
7645
- directory = str(self.distance.text()) if self.directory.text().strip() else None
7646
-
7647
- degrees = my_network.degree_distribution(directory = directory)
7648
-
7649
-
7650
- self.parent().format_for_upperright_table(degrees, 'Degree (k)', 'Proportion of nodes with degree (p(k))', title = 'Degree Distribution Analysis')
7651
-
7652
- self.accept()
7653
-
7654
- except Exception as e:
7655
- print(f"An error occurred: {e}")
7656
7629
 
7657
7630
  class NearNeighDialog(QDialog):
7658
7631
  def __init__(self, parent=None):
@@ -7687,9 +7660,12 @@ class NearNeighDialog(QDialog):
7687
7660
  self.num = QLineEdit("1")
7688
7661
  identities_layout.addRow("Number of Nearest Neighbors to Evaluate Per Node?:", self.num)
7689
7662
 
7690
-
7691
- 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)
7692
7667
 
7668
+ main_layout.addWidget(identities_group)
7693
7669
 
7694
7670
  # Optional Heatmap group box
7695
7671
  heatmap_group = QGroupBox("Optional Heatmap")
@@ -7723,39 +7699,71 @@ class NearNeighDialog(QDialog):
7723
7699
 
7724
7700
  main_layout.addWidget(quant_group)
7725
7701
 
7726
- # Get Distribution group box
7702
+ # Get Distribution group box - ENHANCED STYLING
7727
7703
  distribution_group = QGroupBox("Get Distribution")
7728
7704
  distribution_layout = QVBoxLayout(distribution_group)
7729
7705
 
7730
- 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
+ """)
7731
7725
  run_button.clicked.connect(self.run)
7732
7726
  distribution_layout.addWidget(run_button)
7733
7727
 
7734
7728
  main_layout.addWidget(distribution_group)
7735
7729
 
7736
- # Get All Averages group box (only if node_identities exists)
7730
+ # Get All Averages group box - ENHANCED STYLING (only if node_identities exists)
7737
7731
  if my_network.node_identities is not None:
7738
7732
  averages_group = QGroupBox("Get All Averages")
7739
7733
  averages_layout = QVBoxLayout(averages_group)
7740
7734
 
7741
- 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
+ """)
7742
7755
  run_button2.clicked.connect(self.run2)
7743
7756
  averages_layout.addWidget(run_button2)
7744
7757
 
7745
7758
  main_layout.addWidget(averages_group)
7746
7759
 
7747
7760
  def toggle_map(self):
7748
-
7749
7761
  if self.numpy.isChecked():
7750
-
7751
7762
  if not self.map.isChecked():
7752
-
7753
7763
  self.map.click()
7754
7764
 
7755
7765
  def run(self):
7756
-
7757
7766
  try:
7758
-
7759
7767
  try:
7760
7768
  root = self.root.currentText()
7761
7769
  except:
@@ -7770,6 +7778,9 @@ class NearNeighDialog(QDialog):
7770
7778
  numpy = self.numpy.isChecked()
7771
7779
  num = int(self.num.text()) if self.num.text().strip() else 1
7772
7780
  quant = self.quant.isChecked()
7781
+ centroids = self.centroids.isChecked()
7782
+ if not centroids:
7783
+ num = 1
7773
7784
 
7774
7785
  if root is not None and targ is not None:
7775
7786
  title = f"Nearest {num} Neighbor(s) Distance of {targ} from {root}"
@@ -7780,15 +7791,15 @@ class NearNeighDialog(QDialog):
7780
7791
  header = f"Shortest Distance to Closest {num} Nodes"
7781
7792
  header2 = "Root Node ID"
7782
7793
 
7783
- if my_network.node_centroids is None:
7794
+ if centroids and my_network.node_centroids is None:
7784
7795
  self.parent().show_centroid_dialog()
7785
7796
  if my_network.node_centroids is None:
7786
7797
  return
7787
7798
 
7788
7799
  if not numpy:
7789
- 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)
7790
7801
  else:
7791
- 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)
7792
7803
  self.parent().load_channel(3, overlay, data = True)
7793
7804
 
7794
7805
  if quant_overlay is not None:
@@ -7802,27 +7813,24 @@ class NearNeighDialog(QDialog):
7802
7813
  except Exception as e:
7803
7814
  import traceback
7804
7815
  print(traceback.format_exc())
7805
-
7806
7816
  print(f"Error: {e}")
7807
7817
 
7808
7818
  def run2(self):
7809
-
7810
7819
  try:
7811
-
7812
7820
  available = list(set(my_network.node_identities.values()))
7813
-
7814
7821
  num = int(self.num.text()) if self.num.text().strip() else 1
7815
7822
 
7823
+ centroids = self.centroids.isChecked()
7824
+ if not centroids:
7825
+ num = 1
7826
+
7816
7827
  output_dict = {}
7817
7828
 
7818
7829
  while len(available) > 1:
7819
-
7820
7830
  root = available[0]
7821
7831
 
7822
7832
  for targ in available:
7823
-
7824
- avg, _, _ = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num)
7825
-
7833
+ avg, _, _ = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num, centroids = centroids)
7826
7834
  output_dict[f"{root} vs {targ}"] = avg
7827
7835
 
7828
7836
  del available[0]
@@ -7832,7 +7840,6 @@ class NearNeighDialog(QDialog):
7832
7840
  self.accept()
7833
7841
 
7834
7842
  except Exception as e:
7835
-
7836
7843
  print(f"Error: {e}")
7837
7844
 
7838
7845
 
@@ -9338,11 +9345,13 @@ class ThresholdDialog(QDialog):
9338
9345
 
9339
9346
  def start_ml(self, GPU = False):
9340
9347
 
9341
- try:
9342
- print("Please select image to load into nodes channel for segmentation or press X if you already have the one you want. Note that this load may permit a color image in the nodes channel for segmentation purposes only, which is otherwise not allowed.")
9343
- self.parent().load_channel(0, color = True)
9344
- except:
9345
- pass
9348
+ if self.parent().channel_data[0] is None:
9349
+
9350
+ try:
9351
+ print("Please select image to load into nodes channel for segmentation or press X if you already have the one you want. Note that this load may permit a color image in the nodes channel for segmentation purposes only, which is otherwise not allowed.")
9352
+ self.parent().load_channel(0, color = True)
9353
+ except:
9354
+ pass
9346
9355
 
9347
9356
 
9348
9357
  if self.parent().channel_data[2] is not None or self.parent().channel_data[3] is not None or self.parent().highlight_overlay is not None:
@@ -9589,14 +9598,8 @@ class MachineWindow(QMainWindow):
9589
9598
  train_quick.clicked.connect(lambda: self.train_model(speed=True))
9590
9599
  train_detailed = QPushButton("Train Detailed Model (For Morphology)")
9591
9600
  train_detailed.clicked.connect(lambda: self.train_model(speed=False))
9592
- save = QPushButton("Save Model")
9593
- save.clicked.connect(self.save_model)
9594
- load = QPushButton("Load Model")
9595
- load.clicked.connect(self.load_model)
9596
9601
  training_layout.addWidget(train_quick)
9597
9602
  training_layout.addWidget(train_detailed)
9598
- training_layout.addWidget(save)
9599
- training_layout.addWidget(load)
9600
9603
  training_group.setLayout(training_layout)
9601
9604
 
9602
9605
  # Group 4: Segmentation Options
@@ -9621,12 +9624,27 @@ class MachineWindow(QMainWindow):
9621
9624
  segmentation_layout.addWidget(full_button)
9622
9625
  segmentation_group.setLayout(segmentation_layout)
9623
9626
 
9627
+ # Group 5: Loading Options
9628
+ loading_group = QGroupBox("Saving/Loading")
9629
+ loading_layout = QHBoxLayout()
9630
+ self.save = QPushButton("Save Model")
9631
+ self.save.clicked.connect(self.save_model)
9632
+ self.load = QPushButton("Load Model")
9633
+ self.load.clicked.connect(self.load_model)
9634
+ load_nodes = QPushButton("Load Image (For Seg - Supports Color Images)")
9635
+ load_nodes.clicked.connect(self.load_nodes)
9636
+ loading_layout.addWidget(self.save)
9637
+ loading_layout.addWidget(self.load)
9638
+ loading_layout.addWidget(load_nodes)
9639
+ loading_group.setLayout(loading_layout)
9640
+
9624
9641
  # Add all groups to main layout
9625
9642
  main_layout.addWidget(drawing_group)
9626
9643
  if not GPU:
9627
9644
  main_layout.addWidget(processing_group)
9628
9645
  main_layout.addWidget(training_group)
9629
9646
  main_layout.addWidget(segmentation_group)
9647
+ main_layout.addWidget(loading_group)
9630
9648
 
9631
9649
  # Set the main widget as the central widget
9632
9650
  self.setCentralWidget(main_widget)
@@ -9649,6 +9667,79 @@ class MachineWindow(QMainWindow):
9649
9667
  except:
9650
9668
  return
9651
9669
 
9670
+ def load_nodes(self):
9671
+
9672
+ def confirm_machine_dialog():
9673
+ """Shows a dialog asking user to confirm if they want to start the segmenter"""
9674
+ msg = QMessageBox()
9675
+ msg.setIcon(QMessageBox.Icon.Question)
9676
+ msg.setText("Alert")
9677
+ msg.setInformativeText("Use of this feature will require use of both overlay channels and the highlight overlay. Please save any data and return, or proceed if you do not need those overlays")
9678
+ msg.setWindowTitle("Proceed?")
9679
+ msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
9680
+ return msg.exec() == QMessageBox.StandardButton.Yes
9681
+
9682
+ if self.parent().channel_data[2] is not None or self.parent().channel_data[3] is not None or self.parent().highlight_overlay is not None:
9683
+ if confirm_machine_dialog():
9684
+ pass
9685
+ else:
9686
+ return
9687
+
9688
+ try:
9689
+
9690
+ try:
9691
+ print("Please select image to load into nodes channel for segmentation or press X if you already have the one you want. Note that this load may permit a color image in the nodes channel for segmentation purposes only, which is otherwise not allowed.")
9692
+ self.parent().reset(nodes = True, edges = True, search_region = True, network_overlay = True, id_overlay = True)
9693
+ self.parent().highlight_overlay = None
9694
+ self.parent().load_channel(0, color = True)
9695
+ if self.parent().active_channel == 0:
9696
+ if self.parent().channel_data[0] is not None:
9697
+ try:
9698
+ active_data = self.parent().channel_data[0]
9699
+ act_channel = 0
9700
+ except:
9701
+ active_data = self.parent().channel_data[1]
9702
+ act_channel = 1
9703
+ import traceback
9704
+ traceback.print_exc()
9705
+ else:
9706
+ active_data = self.parent().channel_data[1]
9707
+ act_channel = 1
9708
+
9709
+ try:
9710
+ if len(active_data.shape) == 3:
9711
+ array1 = np.zeros_like(active_data).astype(np.uint8)
9712
+ elif len(active_data.shape) == 4:
9713
+ array1 = np.zeros_like(active_data)[:,:,:,0].astype(np.uint8)
9714
+ except:
9715
+ print("No data in nodes channel")
9716
+ import traceback
9717
+ traceback.print_exc()
9718
+ return
9719
+ array3 = np.zeros_like(array1).astype(np.uint8)
9720
+ self.parent().highlight_overlay = array3 #Clear this out for the segmenter to use
9721
+
9722
+ self.parent().load_channel(2, array1, True)
9723
+ self.trained = False
9724
+ self.previewing = False
9725
+
9726
+ self.segmenter = segmenter.InteractiveSegmenter(active_data, use_gpu=False)
9727
+
9728
+ self.segmentation_worker = None
9729
+
9730
+ self.fore_button.click()
9731
+ self.fore_button.click()
9732
+
9733
+ self.num_chunks = 0
9734
+ self.parent().update_display()
9735
+ except:
9736
+ import traceback
9737
+ traceback.print_exc()
9738
+ pass
9739
+
9740
+ except:
9741
+ pass
9742
+
9652
9743
  def toggle_segment(self):
9653
9744
 
9654
9745
  if self.segmentation_worker is not None:
@@ -9850,10 +9941,8 @@ class MachineWindow(QMainWindow):
9850
9941
  self.kill_segmentation()
9851
9942
  time.sleep(0.1)
9852
9943
 
9853
- if self.use_two:
9854
- self.previewing = True
9855
- else:
9856
- print("Beginning new segmentation...")
9944
+ if not self.trained:
9945
+ return
9857
9946
 
9858
9947
  if self.parent().channel_data[2] is not None:
9859
9948
  active_data = self.parent().channel_data[2]
@@ -9863,19 +9952,16 @@ class MachineWindow(QMainWindow):
9863
9952
  array3 = np.zeros_like(active_data).astype(np.uint8)
9864
9953
  self.parent().highlight_overlay = array3 #Clear this out for the segmenter to use
9865
9954
 
9866
- if not self.trained:
9867
- return
9868
- else:
9869
- self.segmentation_worker = SegmentationWorker(self.parent().highlight_overlay, self.segmenter, self.use_gpu, self.use_two, self.previewing, self, self.mem_lock)
9870
- self.segmentation_worker.chunk_processed.connect(self.update_display) # Just update display
9871
- current_xlim = self.parent().ax.get_xlim()
9872
- current_ylim = self.parent().ax.get_ylim()
9873
- try:
9874
- x, y = self.parent().get_current_mouse_position()
9875
- except:
9876
- x, y = 0, 0
9877
- self.segmenter.update_position(self.parent().current_slice, x, y)
9878
- self.segmentation_worker.start()
9955
+ self.segmentation_worker = SegmentationWorker(self.parent().highlight_overlay, self.segmenter, self.use_gpu, self.use_two, self.previewing, self, self.mem_lock)
9956
+ self.segmentation_worker.chunk_processed.connect(self.update_display) # Just update display
9957
+ current_xlim = self.parent().ax.get_xlim()
9958
+ current_ylim = self.parent().ax.get_ylim()
9959
+ try:
9960
+ x, y = self.parent().get_current_mouse_position()
9961
+ except:
9962
+ x, y = 0, 0
9963
+ self.segmenter.update_position(self.parent().current_slice, x, y)
9964
+ self.segmentation_worker.start()
9879
9965
 
9880
9966
  def confirm_seg_dialog(self):
9881
9967
  """Shows a dialog asking user to confirm segment all"""
@@ -10086,7 +10172,10 @@ class SegmentationWorker(QThread):
10086
10172
  self.mem_lock = mem_lock
10087
10173
  self._stop = False
10088
10174
  self._paused = False # Add pause flag
10089
- self.update_interval = 1 # Increased to 500ms
10175
+ if self.machine_window.parent().shape[1] * self.machine_window.parent().shape[2] > 3000 * 3000: #arbitrary throttle for large arrays.
10176
+ self.update_interval = 10
10177
+ else:
10178
+ self.update_interval = 1 # Increased to 1s
10090
10179
  self.chunks_since_update = 0
10091
10180
  self.chunks_per_update = 5 # Only update every 5 chunks
10092
10181
  self.poked = False # If it should wake up or not
@@ -10138,13 +10227,20 @@ class SegmentationWorker(QThread):
10138
10227
  current_time = time.time()
10139
10228
  if (self.chunks_since_update >= self.chunks_per_update and
10140
10229
  current_time - self.last_update >= self.update_interval):
10141
- if self.machine_window.parent().shape[1] * self.machine_window.parent().shape[2] > 3000 * 3000: #arbitrary throttle for large arrays.
10142
- self.msleep(3000)
10230
+ #if self.machine_window.parent().shape[1] * self.machine_window.parent().shape[2] > 3000 * 3000: #arbitrary throttle for large arrays.
10231
+ #self.msleep(3000)
10143
10232
  self.chunk_processed.emit()
10144
10233
  self.chunks_since_update = 0
10145
10234
  self.last_update = current_time
10146
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
+
10147
10242
  self.finished.emit()
10243
+
10148
10244
 
10149
10245
  except Exception as e:
10150
10246
  print(f"Error in segmentation: {e}")
@@ -11211,7 +11307,7 @@ class SkeletonizeDialog(QDialog):
11211
11307
  )
11212
11308
 
11213
11309
  if remove > 0:
11214
- result = n3d.remove_branches(result, remove)
11310
+ result = n3d.remove_branches_new(result, remove)
11215
11311
 
11216
11312
 
11217
11313
  # Update both the display data and the network object
@@ -13008,6 +13104,409 @@ class ProxDialog(QDialog):
13008
13104
  print(traceback.format_exc())
13009
13105
 
13010
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
+
13011
13510
 
13012
13511
  # Initiating this program from the script line:
13013
13512