nettracer3d 1.0.7__py3-none-any.whl → 1.0.9__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.

@@ -36,6 +36,7 @@ from threading import Lock
36
36
  from scipy import ndimage
37
37
  import os
38
38
  from . import painting
39
+ from . import stats as net_stats
39
40
 
40
41
 
41
42
 
@@ -425,8 +426,6 @@ class ImageViewerWindow(QMainWindow):
425
426
  self.canvas.mpl_connect('button_release_event', self.on_mouse_release)
426
427
  self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move)
427
428
 
428
- #self.canvas.mpl_connect('button_press_event', self.on_mouse_click)
429
-
430
429
  # Initialize measurement tracking
431
430
  self.measurement_points = [] # List to store point pairs
432
431
  self.angle_measurements = [] # NEW: List to store angle trios
@@ -462,8 +461,6 @@ class ImageViewerWindow(QMainWindow):
462
461
  self.last_paint_pos = None
463
462
 
464
463
  self.resume = False
465
-
466
- self.hold_update = False
467
464
  self._first_pan_done = False
468
465
 
469
466
 
@@ -879,6 +876,21 @@ class ImageViewerWindow(QMainWindow):
879
876
  elif self.scroll_direction > 0 and new_value <= self.slice_slider.maximum():
880
877
  self.slice_slider.setValue(new_value)
881
878
 
879
+
880
+ def confirm_mini_thresh(self):
881
+
882
+ if self.shape[0] * self.shape[1] * self.shape[2] > self.mini_thresh:
883
+ self.mini_overlay = True
884
+ return True
885
+ else:
886
+ return False
887
+
888
+ def evaluate_mini(self, mode = 'nodes'):
889
+ if self.confirm_mini_thresh():
890
+ self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
891
+ else:
892
+ self.create_highlight_overlay(node_indices=self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
893
+
882
894
  def create_highlight_overlay(self, node_indices=None, edge_indices=None, overlay1_indices = None, overlay2_indices = None, bounds = False):
883
895
  """
884
896
  Create a binary overlay highlighting specific nodes and/or edges using parallel processing.
@@ -1004,13 +1016,13 @@ class ImageViewerWindow(QMainWindow):
1004
1016
 
1005
1017
  # Combine results
1006
1018
  if node_overlay is not None:
1007
- self.highlight_overlay = np.maximum(self.highlight_overlay, node_overlay)
1019
+ self.highlight_overlay = np.maximum(self.highlight_overlay, node_overlay).astype(np.uint8)
1008
1020
  if edge_overlay is not None:
1009
- self.highlight_overlay = np.maximum(self.highlight_overlay, edge_overlay)
1021
+ self.highlight_overlay = np.maximum(self.highlight_overlay, edge_overlay).astype(np.uint8)
1010
1022
  if overlay1_overlay is not None:
1011
- self.highlight_overlay = np.maximum(self.highlight_overlay, overlay1_overlay)
1023
+ self.highlight_overlay = np.maximum(self.highlight_overlay, overlay1_overlay).astype(np.uint8)
1012
1024
  if overlay2_overlay is not None:
1013
- self.highlight_overlay = np.maximum(self.highlight_overlay, overlay2_overlay)
1025
+ self.highlight_overlay = np.maximum(self.highlight_overlay, overlay2_overlay).astype(np.uint8)
1014
1026
 
1015
1027
  # Update display
1016
1028
  self.update_display(preserve_zoom=(current_xlim, current_ylim))
@@ -1019,6 +1031,9 @@ class ImageViewerWindow(QMainWindow):
1019
1031
 
1020
1032
  """Highlight overlay generation method specific for the segmenter interactive mode"""
1021
1033
 
1034
+ self.mini_overlay_data = None
1035
+ self.highlight_overlay = None
1036
+
1022
1037
  def process_chunk_bounds(chunk_data, indices_to_check):
1023
1038
  """Process a single chunk of the array to create highlight mask"""
1024
1039
  mask = (chunk_data >= indices_to_check[0]) & (chunk_data <= indices_to_check[1])
@@ -1291,6 +1306,9 @@ class ImageViewerWindow(QMainWindow):
1291
1306
  select_nodes = select_all_menu.addAction("Nodes")
1292
1307
  select_both = select_all_menu.addAction("Nodes + Edges")
1293
1308
  select_edges = select_all_menu.addAction("Edges")
1309
+ select_net_nodes = select_all_menu.addAction("Nodes in Network")
1310
+ select_net_both = select_all_menu.addAction("Nodes + Edges in Network")
1311
+ select_net_edges = select_all_menu.addAction("Edges in Network")
1294
1312
  context_menu.addMenu(select_all_menu)
1295
1313
 
1296
1314
  if len(self.clicked_values['nodes']) > 0 or len(self.clicked_values['edges']) > 0:
@@ -1365,6 +1383,9 @@ class ImageViewerWindow(QMainWindow):
1365
1383
  select_nodes.triggered.connect(lambda: self.handle_select_all(edges = False, nodes = True))
1366
1384
  select_both.triggered.connect(lambda: self.handle_select_all(edges = True))
1367
1385
  select_edges.triggered.connect(lambda: self.handle_select_all(edges = True, nodes = False))
1386
+ select_net_nodes.triggered.connect(lambda: self.handle_select_all(edges = False, nodes = True, network = True))
1387
+ select_net_both.triggered.connect(lambda: self.handle_select_all(edges = True, network = True))
1388
+ select_net_edges.triggered.connect(lambda: self.handle_select_all(edges = True, nodes = False, network = True))
1368
1389
  if self.highlight_overlay is not None or self.mini_overlay_data is not None:
1369
1390
  highlight_select = context_menu.addAction("Add highlight in network selection")
1370
1391
  highlight_select.triggered.connect(self.handle_highlight_select)
@@ -1705,28 +1726,12 @@ class ImageViewerWindow(QMainWindow):
1705
1726
  if edges:
1706
1727
  edge_indices = filtered_df.iloc[:, 2].unique().tolist()
1707
1728
  self.clicked_values['edges'] = edge_indices
1708
-
1709
- if self.channel_data[1].shape[0] * self.channel_data[1].shape[1] * self.channel_data[1].shape[2] > self.mini_thresh:
1710
- self.mini_overlay = True
1711
- self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
1712
- else:
1713
- self.create_highlight_overlay(
1714
- node_indices=self.clicked_values['nodes'],
1715
- edge_indices=self.clicked_values['edges']
1716
- )
1729
+ self.evaluate_mini(mode = 'edges')
1717
1730
  else:
1718
- if self.channel_data[0].shape[0] * self.channel_data[0].shape[1] * self.channel_data[0].shape[2] > self.mini_thresh:
1719
- self.mini_overlay = True
1720
- self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
1721
- else:
1722
- self.create_highlight_overlay(
1723
- node_indices=self.clicked_values['nodes'],
1724
- edge_indices = self.clicked_values['edges']
1725
- )
1726
-
1731
+ self.evaluate_mini()
1727
1732
 
1728
1733
  except Exception as e:
1729
- print(f"Error processing neighbors: {e}")
1734
+ print(f"Error showing neighbors: {e}")
1730
1735
 
1731
1736
 
1732
1737
  def handle_show_component(self, edges = False, nodes = True):
@@ -1797,23 +1802,10 @@ class ImageViewerWindow(QMainWindow):
1797
1802
  if edges:
1798
1803
  edge_indices = filtered_df.iloc[:, 2].unique().tolist()
1799
1804
  self.clicked_values['edges'] = edge_indices
1800
- if self.channel_data[1].shape[0] * self.channel_data[1].shape[1] * self.channel_data[1].shape[2] > self.mini_thresh:
1801
- self.mini_overlay = True
1802
- self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
1803
- else:
1804
- self.create_highlight_overlay(
1805
- node_indices=self.clicked_values['nodes'],
1806
- edge_indices=edge_indices
1807
- )
1805
+ self.evaluate_mini(mode = 'edges')
1808
1806
  else:
1809
- if self.channel_data[0].shape[0] * self.channel_data[0].shape[1] * self.channel_data[0].shape[2] > self.mini_thresh:
1810
- self.mini_overlay = True
1811
- self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
1812
- else:
1813
- self.create_highlight_overlay(
1814
- node_indices = self.clicked_values['nodes'],
1815
- edge_indices = self.clicked_values['edges']
1816
- )
1807
+ self.evaluate_mini()
1808
+
1817
1809
 
1818
1810
  except Exception as e:
1819
1811
 
@@ -2078,12 +2070,15 @@ class ImageViewerWindow(QMainWindow):
2078
2070
 
2079
2071
 
2080
2072
 
2081
- def handle_select_all(self, nodes = True, edges = False):
2073
+ def handle_select_all(self, nodes = True, edges = False, network = False):
2082
2074
 
2083
2075
  try:
2084
2076
 
2085
2077
  if nodes:
2086
- nodes = list(np.unique(my_network.nodes))
2078
+ if not network:
2079
+ nodes = list(np.unique(my_network.nodes))
2080
+ else:
2081
+ nodes = list(set(my_network.network_lists[0] + my_network.network_lists[1]))
2087
2082
  if nodes[0] == 0:
2088
2083
  del nodes[0]
2089
2084
  num = (self.channel_data[0].shape[0] * self.channel_data[0].shape[1] * self.channel_data[0].shape[2])
@@ -2091,7 +2086,10 @@ class ImageViewerWindow(QMainWindow):
2091
2086
  else:
2092
2087
  nodes = []
2093
2088
  if edges:
2094
- edges = list(np.unique(my_network.edges))
2089
+ if not network:
2090
+ edges = list(np.unique(my_network.edges))
2091
+ else:
2092
+ edges = my_network.network_lists[2]
2095
2093
  num = (self.channel_data[1].shape[0] * self.channel_data[1].shape[1] * self.channel_data[1].shape[2])
2096
2094
  if edges[0] == 0:
2097
2095
  del edges[0]
@@ -2214,7 +2212,7 @@ class ImageViewerWindow(QMainWindow):
2214
2212
  except:
2215
2213
  pass
2216
2214
 
2217
- self.format_for_upperright_table(info_dict, title = f'Info on Object')
2215
+ self.format_for_upperright_table(info_dict, title = f'Info on Object', sort = False)
2218
2216
 
2219
2217
  except:
2220
2218
  pass
@@ -2640,9 +2638,9 @@ class ImageViewerWindow(QMainWindow):
2640
2638
  self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
2641
2639
  self.needs_mini = False
2642
2640
  else:
2643
- self.create_highlight_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
2641
+ self.evaluate_mini()
2644
2642
  else:
2645
- self.create_highlight_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
2643
+ self.evaluate_mini()
2646
2644
 
2647
2645
 
2648
2646
  self.update_display(preserve_zoom=(current_xlim, current_ylim))
@@ -3707,17 +3705,23 @@ class ImageViewerWindow(QMainWindow):
3707
3705
 
3708
3706
  # Add highlight overlays if they exist (with downsampling)
3709
3707
  if self.mini_overlay and self.highlight and self.machine_window is None:
3710
- display_overlay = crop_and_downsample_image(self.mini_overlay_data, y_min_padded, y_max_padded, x_min_padded, x_max_padded, downsample_factor)
3711
- highlight_rgba = self.create_highlight_rgba(display_overlay, yellow=True)
3712
- composite = self.blend_layers(composite, highlight_rgba)
3708
+ try:
3709
+ display_overlay = crop_and_downsample_image(self.mini_overlay_data, y_min_padded, y_max_padded, x_min_padded, x_max_padded, downsample_factor)
3710
+ highlight_rgba = self.create_highlight_rgba(display_overlay, yellow=True)
3711
+ composite = self.blend_layers(composite, highlight_rgba)
3712
+ except:
3713
+ pass
3713
3714
  elif self.highlight_overlay is not None and self.highlight:
3714
- highlight_slice = self.highlight_overlay[self.current_slice]
3715
- display_highlight = crop_and_downsample_image(highlight_slice, y_min_padded, y_max_padded, x_min_padded, x_max_padded, downsample_factor)
3716
- if self.machine_window is None:
3717
- highlight_rgba = self.create_highlight_rgba(display_highlight, yellow=True)
3718
- else:
3719
- highlight_rgba = self.create_highlight_rgba(display_highlight, yellow=False)
3720
- composite = self.blend_layers(composite, highlight_rgba)
3715
+ try:
3716
+ highlight_slice = self.highlight_overlay[self.current_slice]
3717
+ display_highlight = crop_and_downsample_image(highlight_slice, y_min_padded, y_max_padded, x_min_padded, x_max_padded, downsample_factor)
3718
+ if self.machine_window is None:
3719
+ highlight_rgba = self.create_highlight_rgba(display_highlight, yellow=True)
3720
+ else:
3721
+ highlight_rgba = self.create_highlight_rgba(display_highlight, yellow=False)
3722
+ composite = self.blend_layers(composite, highlight_rgba)
3723
+ except:
3724
+ pass
3721
3725
 
3722
3726
  # Convert to 0-255 range for display
3723
3727
  return (composite * 255).astype(np.uint8)
@@ -4529,6 +4533,8 @@ class ImageViewerWindow(QMainWindow):
4529
4533
  for i in range(4):
4530
4534
  load_action = load_menu.addAction(f"Load {self.channel_names[i]}")
4531
4535
  load_action.triggered.connect(lambda checked, ch=i: self.load_channel(ch))
4536
+ load_action = load_menu.addAction("Load Full-Sized Highlight Overlay")
4537
+ load_action.triggered.connect(lambda: self.load_channel(channel_index = 4, load_highlight = True))
4532
4538
  load_action = load_menu.addAction("Load Network")
4533
4539
  load_action.triggered.connect(self.load_network)
4534
4540
  load_action = load_menu.addAction("Load From Excel Helper")
@@ -4569,6 +4575,8 @@ class ImageViewerWindow(QMainWindow):
4569
4575
  allstats_action.triggered.connect(self.stats)
4570
4576
  histos_action = stats_menu.addAction("Network Statistic Histograms")
4571
4577
  histos_action.triggered.connect(self.histos)
4578
+ sig_action = stats_menu.addAction("Significance Testing")
4579
+ sig_action.triggered.connect(self.sig_test)
4572
4580
  radial_action = stats_menu.addAction("Radial Distribution Analysis")
4573
4581
  radial_action.triggered.connect(self.show_radial_dialog)
4574
4582
  neighbor_id_action = stats_menu.addAction("Identity Distribution of Neighbors")
@@ -4780,7 +4788,10 @@ class ImageViewerWindow(QMainWindow):
4780
4788
  # Invalid input - reset to default
4781
4789
  self.downsample_factor = 1
4782
4790
 
4783
- self.throttle = self.shape[1] * self.shape[2] > 3000 * 3000 * self.downsample_factor
4791
+ try:
4792
+ self.throttle = self.shape[1] * self.shape[2] > 3000 * 3000 * self.downsample_factor
4793
+ except:
4794
+ self.throttle = False
4784
4795
 
4785
4796
  # Optional: Trigger display update if you want immediate effect
4786
4797
  if update:
@@ -4913,6 +4924,16 @@ class ImageViewerWindow(QMainWindow):
4913
4924
  except Exception as e:
4914
4925
  print(f"Error creating histogram selector: {e}")
4915
4926
 
4927
+ def sig_test(self):
4928
+ # Get the existing QApplication instance
4929
+ app = QApplication.instance()
4930
+
4931
+ # Create the statistical GUI window without starting a new event loop
4932
+ stats_window = net_stats.main(app)
4933
+
4934
+ # Keep a reference so it doesn't get garbage collected
4935
+ self.stats_window = stats_window
4936
+
4916
4937
  def volumes(self):
4917
4938
 
4918
4939
 
@@ -4938,7 +4959,7 @@ class ImageViewerWindow(QMainWindow):
4938
4959
 
4939
4960
 
4940
4961
 
4941
- def format_for_upperright_table(self, data, metric='Metric', value='Value', title=None):
4962
+ def format_for_upperright_table(self, data, metric='Metric', value='Value', title=None, sort = True):
4942
4963
  """
4943
4964
  Format dictionary or list data for display in upper right table.
4944
4965
 
@@ -5024,11 +5045,12 @@ class ImageViewerWindow(QMainWindow):
5024
5045
  table = CustomTableView(self)
5025
5046
  table.setModel(PandasModel(df))
5026
5047
 
5027
- try:
5028
- first_column_name = table.model()._data.columns[0]
5029
- table.sort_table(first_column_name, ascending=True)
5030
- except:
5031
- pass
5048
+ if sort:
5049
+ try:
5050
+ first_column_name = table.model()._data.columns[0]
5051
+ table.sort_table(first_column_name, ascending=True)
5052
+ except:
5053
+ pass
5032
5054
 
5033
5055
  # Add to tabbed widget
5034
5056
  if title is None:
@@ -5620,6 +5642,7 @@ class ImageViewerWindow(QMainWindow):
5620
5642
  self.last_saved = os.path.dirname(directory)
5621
5643
  self.last_save_name = directory
5622
5644
 
5645
+ self.channel_data = [None] * 5
5623
5646
  if directory != "":
5624
5647
 
5625
5648
  self.reset(network = True, xy_scale = 1, z_scale = 1, nodes = True, edges = True, network_overlay = True, id_overlay = True, update = False)
@@ -5700,6 +5723,8 @@ class ImageViewerWindow(QMainWindow):
5700
5723
  f"Failed to load Network 3D Object: {str(e)}"
5701
5724
  )
5702
5725
 
5726
+
5727
+
5703
5728
  def load_network(self):
5704
5729
  """Load in the network from a .xlsx (need to add .csv support)"""
5705
5730
 
@@ -5940,12 +5965,41 @@ class ImageViewerWindow(QMainWindow):
5940
5965
  msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
5941
5966
  return msg.exec() == QMessageBox.StandardButton.Yes
5942
5967
 
5943
- def load_channel(self, channel_index, channel_data=None, data=False, assign_shape = True, preserve_zoom = None, end_paint = False, begin_paint = False, color = False):
5968
+ def get_scaling_metadata_only(self, filename):
5969
+ # This only reads headers/metadata, not image data
5970
+ with tifffile.TiffFile(filename) as tif:
5971
+ x_scale = y_scale = z_scale = unit = None
5972
+
5973
+ # ImageJ metadata (very lightweight)
5974
+ if hasattr(tif, 'imagej_metadata') and tif.imagej_metadata:
5975
+ metadata = tif.imagej_metadata
5976
+ z_scale = metadata.get('spacing')
5977
+ unit = metadata.get('unit')
5978
+
5979
+ # TIFF tags (also lightweight - just header info)
5980
+ page = tif.pages[0] # This doesn't load image data
5981
+ tags = page.tags
5982
+
5983
+ if 'XResolution' in tags:
5984
+ x_res = tags['XResolution'].value
5985
+ x_scale = x_res[1] / x_res[0] if isinstance(x_res, tuple) else 1.0 / x_res
5986
+
5987
+ if 'YResolution' in tags:
5988
+ y_res = tags['YResolution'].value
5989
+ y_scale = y_res[1] / y_res[0] if isinstance(y_res, tuple) else 1.0 / y_res
5990
+
5991
+ if x_scale is None:
5992
+ x_scale = 1
5993
+ if z_scale is None:
5994
+ z_scale = 1
5995
+
5996
+ return x_scale, z_scale
5997
+
5998
+ def load_channel(self, channel_index, channel_data=None, data=False, assign_shape = True, preserve_zoom = None, end_paint = False, begin_paint = False, color = False, load_highlight = False):
5944
5999
  """Load a channel and enable active channel selection if needed."""
5945
6000
 
5946
6001
  try:
5947
6002
 
5948
- self.hold_update = True
5949
6003
  if not data: # For solo loading
5950
6004
  filename, _ = QFileDialog.getOpenFileName(
5951
6005
  self,
@@ -5965,6 +6019,13 @@ class ImageViewerWindow(QMainWindow):
5965
6019
  try:
5966
6020
  if file_extension in ['tif', 'tiff']:
5967
6021
  import tifffile
6022
+ self.channel_data[channel_index] = None
6023
+ if (self.channel_data[0] is None and self.channel_data[1] is None) and (channel_index == 0 or channel_index == 1):
6024
+ try:
6025
+ my_network.xy_scale, my_network.z_scale = self.get_scaling_metadata_only(filename)
6026
+ print(f"xy_scale property set to {my_network.xy_scale}; z_scale property set to {my_network.z_scale}")
6027
+ except:
6028
+ pass
5968
6029
  self.channel_data[channel_index] = tifffile.imread(filename)
5969
6030
 
5970
6031
  elif file_extension == 'nii':
@@ -6070,51 +6131,52 @@ class ImageViewerWindow(QMainWindow):
6070
6131
  my_network.id_overlay = self.channel_data[channel_index]
6071
6132
 
6072
6133
  # Enable the channel button
6073
- self.channel_buttons[channel_index].setEnabled(True)
6074
- self.delete_buttons[channel_index].setEnabled(True)
6134
+ if channel_index != 4:
6135
+ self.channel_buttons[channel_index].setEnabled(True)
6136
+ self.delete_buttons[channel_index].setEnabled(True)
6075
6137
 
6076
6138
 
6077
- # Enable active channel selector if this is the first channel loaded
6078
- if not self.active_channel_combo.isEnabled():
6079
- self.active_channel_combo.setEnabled(True)
6139
+ # Enable active channel selector if this is the first channel loaded
6140
+ if not self.active_channel_combo.isEnabled():
6141
+ self.active_channel_combo.setEnabled(True)
6080
6142
 
6081
- # Update slider range if this is the first channel loaded
6082
- try:
6083
- if len(self.channel_data[channel_index].shape) == 3 or len(self.channel_data[channel_index].shape) == 4:
6084
- if not self.slice_slider.isEnabled():
6085
- self.slice_slider.setEnabled(True)
6086
- self.slice_slider.setMinimum(0)
6087
- self.slice_slider.setMaximum(self.channel_data[channel_index].shape[0] - 1)
6088
- if self.slice_slider.value() < self.channel_data[channel_index].shape[0] - 1:
6089
- self.current_slice = self.slice_slider.value()
6143
+ # Update slider range if this is the first channel loaded
6144
+ try:
6145
+ if len(self.channel_data[channel_index].shape) == 3 or len(self.channel_data[channel_index].shape) == 4:
6146
+ if not self.slice_slider.isEnabled():
6147
+ self.slice_slider.setEnabled(True)
6148
+ self.slice_slider.setMinimum(0)
6149
+ self.slice_slider.setMaximum(self.channel_data[channel_index].shape[0] - 1)
6150
+ if self.slice_slider.value() < self.channel_data[channel_index].shape[0] - 1:
6151
+ self.current_slice = self.slice_slider.value()
6152
+ else:
6153
+ self.slice_slider.setValue(0)
6154
+ self.current_slice = 0
6090
6155
  else:
6091
- self.slice_slider.setValue(0)
6092
- self.current_slice = 0
6156
+ self.slice_slider.setEnabled(True)
6157
+ self.slice_slider.setMinimum(0)
6158
+ self.slice_slider.setMaximum(self.channel_data[channel_index].shape[0] - 1)
6159
+ if self.slice_slider.value() < self.channel_data[channel_index].shape[0] - 1:
6160
+ self.current_slice = self.slice_slider.value()
6161
+ else:
6162
+ self.current_slice = 0
6163
+ self.slice_slider.setValue(0)
6093
6164
  else:
6094
- self.slice_slider.setEnabled(True)
6095
- self.slice_slider.setMinimum(0)
6096
- self.slice_slider.setMaximum(self.channel_data[channel_index].shape[0] - 1)
6097
- if self.slice_slider.value() < self.channel_data[channel_index].shape[0] - 1:
6098
- self.current_slice = self.slice_slider.value()
6099
- else:
6100
- self.current_slice = 0
6101
- self.slice_slider.setValue(0)
6102
- else:
6103
- self.slice_slider.setEnabled(False)
6104
- except:
6105
- pass
6165
+ self.slice_slider.setEnabled(False)
6166
+ except:
6167
+ pass
6106
6168
 
6107
-
6108
- # If this is the first channel loaded, make it active
6109
- if all(not btn.isEnabled() for btn in self.channel_buttons[:channel_index]):
6110
- self.set_active_channel(channel_index)
6169
+
6170
+ # If this is the first channel loaded, make it active
6171
+ if all(not btn.isEnabled() for btn in self.channel_buttons[:channel_index]):
6172
+ self.set_active_channel(channel_index)
6111
6173
 
6112
- if not self.channel_buttons[channel_index].isChecked():
6113
- self.channel_buttons[channel_index].click()
6174
+ if not self.channel_buttons[channel_index].isChecked():
6175
+ self.channel_buttons[channel_index].click()
6114
6176
 
6115
- self.min_max[channel_index][0] = np.min(self.channel_data[channel_index])
6116
- self.min_max[channel_index][1] = np.max(self.channel_data[channel_index])
6117
- self.volume_dict[channel_index] = None #reset volumes
6177
+ self.min_max[channel_index][0] = np.min(self.channel_data[channel_index])
6178
+ self.min_max[channel_index][1] = np.max(self.channel_data[channel_index])
6179
+ self.volume_dict[channel_index] = None #reset volumes
6118
6180
 
6119
6181
  try:
6120
6182
  if assign_shape: #keep original shape tracked to undo resampling.
@@ -6138,7 +6200,6 @@ class ImageViewerWindow(QMainWindow):
6138
6200
 
6139
6201
  self.img_height, self.img_width = self.shape[1], self.shape[2]
6140
6202
  self.original_ylim, self.original_xlim = (self.shape[1] + 0.5, -0.5), (-0.5, self.shape[2] - 0.5)
6141
- #print(self.original_xlim)
6142
6203
 
6143
6204
  self.completed_paint_strokes = [] #Reset pending paint operations
6144
6205
  self.current_stroke_points = []
@@ -6148,6 +6209,12 @@ class ImageViewerWindow(QMainWindow):
6148
6209
  self.current_operation = []
6149
6210
  self.current_operation_type = None
6150
6211
 
6212
+ if load_highlight:
6213
+ self.highlight_overlay = n3d.binarize(self.channel_data[4].astype(np.uint8))
6214
+ self.mini_overlay_data = None
6215
+ self.mini_overlay = False
6216
+ self.channel_data[4] = None
6217
+
6151
6218
  if self.pan_mode:
6152
6219
  self.pan_button.click()
6153
6220
  if self.show_channels:
@@ -6156,7 +6223,6 @@ class ImageViewerWindow(QMainWindow):
6156
6223
  elif not end_paint:
6157
6224
 
6158
6225
  self.update_display(reset_resize = reset_resize, preserve_zoom = preserve_zoom)
6159
-
6160
6226
 
6161
6227
  except Exception as e:
6162
6228
 
@@ -6165,7 +6231,7 @@ class ImageViewerWindow(QMainWindow):
6165
6231
  QMessageBox.critical(
6166
6232
  self,
6167
6233
  "Error Loading File",
6168
- f"Failed to load tiff file: {str(e)}"
6234
+ f"Failed to load file: {str(e)}"
6169
6235
  )
6170
6236
 
6171
6237
  def delete_channel(self, channel_index, called = True, update = True):
@@ -6394,15 +6460,13 @@ class ImageViewerWindow(QMainWindow):
6394
6460
  self.pm.convert_virtual_strokes_to_data()
6395
6461
  self.current_slice = slice_value
6396
6462
  if self.preview:
6463
+ self.highlight_overlay = None
6464
+ self.mini_overlay_data = None
6465
+ self.mini_overlay = False
6397
6466
  self.create_highlight_overlay_slice(self.targs, bounds=self.bounds)
6398
6467
  elif self.mini_overlay == True: #If we are rendering the highlight overlay for selected values one at a time.
6399
6468
  self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
6400
- if not self.hold_update:
6401
- self.update_display(preserve_zoom=view_settings)
6402
- else:
6403
- self.hold_update = False
6404
- #if self.machine_window is not None:
6405
- #self.machine_window.poke_segmenter()
6469
+ self.update_display(preserve_zoom=view_settings)
6406
6470
  if self.pan_mode:
6407
6471
  self.pan_button.click()
6408
6472
  self.pending_slice = None
@@ -6419,7 +6483,6 @@ class ImageViewerWindow(QMainWindow):
6419
6483
  self.channel_brightness[channel_index]['max'] = max_val / 65535
6420
6484
  self.update_display(preserve_zoom = (current_xlim, current_ylim))
6421
6485
 
6422
-
6423
6486
  def update_display(self, preserve_zoom=None, dims=None, called=False, reset_resize=False, skip=False):
6424
6487
  """Optimized display update with view-based cropping for performance."""
6425
6488
  try:
@@ -6645,7 +6708,7 @@ class ImageViewerWindow(QMainWindow):
6645
6708
  # Handle preview, overlays, and measurements (apply cropping here too)
6646
6709
 
6647
6710
  # Overlay handling (optimized with cropping and downsampling)
6648
- if self.mini_overlay and self.highlight and self.machine_window is None:
6711
+ if self.mini_overlay and self.highlight and self.machine_window is None and not self.preview:
6649
6712
  highlight_cmap = LinearSegmentedColormap.from_list('highlight', [(0, 0, 0, 0), (1, 1, 0, 1)])
6650
6713
  display_overlay = crop_and_downsample_image(
6651
6714
  self.mini_overlay_data, y_min_padded, y_max_padded,
@@ -6774,8 +6837,6 @@ class ImageViewerWindow(QMainWindow):
6774
6837
  #print(traceback.format_exc())
6775
6838
 
6776
6839
 
6777
-
6778
-
6779
6840
  def get_channel_image(self, channel):
6780
6841
  """Find the matplotlib image object for a specific channel."""
6781
6842
  if not hasattr(self.ax, 'images'):
@@ -10028,6 +10089,22 @@ class InteractionDialog(QDialog):
10028
10089
  self.mode_selector.setCurrentIndex(0) # Default to Mode 1
10029
10090
  layout.addRow("Execution Mode:", self.mode_selector)
10030
10091
 
10092
+ self.length = QPushButton("Return Lengths")
10093
+ self.length.setCheckable(True)
10094
+ self.length.setChecked(False)
10095
+ layout.addRow("(Will Skeletonize the Edge Mirror and use that to calculate adjacent length of edges, as opposed to default volumes):", self.length)
10096
+
10097
+ self.auto = QPushButton("Auto")
10098
+ self.auto.setCheckable(True)
10099
+ try:
10100
+ if self.parent().shape[0] == 1:
10101
+ self.auto.setChecked(False)
10102
+ else:
10103
+ self.auto.setChecked(True)
10104
+ except:
10105
+ self.auto.setChecked(False)
10106
+ layout.addRow("(If Above): Attempt to Auto Correct Skeleton Looping:", self.auto)
10107
+
10031
10108
  self.fastdil = QPushButton("Fast Dilate")
10032
10109
  self.fastdil.setCheckable(True)
10033
10110
  self.fastdil.setChecked(False)
@@ -10051,10 +10128,16 @@ class InteractionDialog(QDialog):
10051
10128
 
10052
10129
 
10053
10130
  fastdil = self.fastdil.isChecked()
10131
+ length = self.length.isChecked()
10132
+ auto = self.auto.isChecked()
10054
10133
 
10055
- result = my_network.interactions(search = node_search, cores = accepted_mode, fastdil = fastdil)
10134
+ result = my_network.interactions(search = node_search, cores = accepted_mode, skele = length, length = length, auto = auto, fastdil = fastdil)
10135
+
10136
+ if not length:
10137
+ self.parent().format_for_upperright_table(result, 'Node ID', ['Volume of Nearby Edge (Scaled)', 'Volume of Search Region (Scaled)'], title = 'Node/Edge Interactions')
10138
+ else:
10139
+ self.parent().format_for_upperright_table(result, 'Node ID', ['~Length of Nearby Edge (Scaled)', 'Volume of Search Region (Scaled)'], title = 'Node/Edge Interactions')
10056
10140
 
10057
- self.parent().format_for_upperright_table(result, 'Node ID', ['Volume of Nearby Edge (Scaled)', 'Volume of Search Region'], title = 'Node/Edge Interactions')
10058
10141
 
10059
10142
  self.accept()
10060
10143
 
@@ -10616,6 +10699,9 @@ class MotherDialog(QDialog):
10616
10699
 
10617
10700
  except Exception as e:
10618
10701
 
10702
+ import traceback
10703
+ print(traceback.format_exc())
10704
+
10619
10705
  print(f"Error finding mothers: {e}")
10620
10706
 
10621
10707
 
@@ -11303,12 +11389,11 @@ class ThresholdDialog(QDialog):
11303
11389
  print("Error - please calculate network first")
11304
11390
  return
11305
11391
 
11306
- if self.parent().mini_overlay_data is not None:
11307
- self.parent().mini_overlay_data = None
11308
-
11309
11392
  thresh_window = ThresholdWindow(self.parent(), accepted_mode)
11310
11393
  thresh_window.show() # Non-modal window
11311
11394
  self.highlight_overlay = None
11395
+ #self.mini_overlay = False
11396
+ self.mini_overlay_data = None
11312
11397
  self.accept()
11313
11398
  except:
11314
11399
  import traceback
@@ -12282,16 +12367,23 @@ class ThresholdWindow(QMainWindow):
12282
12367
  self.parent().bounds = False
12283
12368
 
12284
12369
  elif accepted_mode == 0:
12285
- targ_shape = self.parent().channel_data[self.parent().active_channel].shape
12286
- if (targ_shape[0] + targ_shape[1] + targ_shape[2]) > 2500: #Take a simpler histogram on big arrays
12287
- temp_max = np.max(self.parent().channel_data[self.parent().active_channel])
12288
- temp_min = np.min(self.parent().channel_data[self.parent().active_channel])
12289
- temp_array = n3d.downsample(self.parent().channel_data[self.parent().active_channel], 5)
12290
- self.histo_list = temp_array.flatten().tolist()
12291
- self.histo_list.append(temp_min)
12292
- self.histo_list.append(temp_max)
12293
- else: #Otherwise just use full array data
12294
- self.histo_list = self.parent().channel_data[self.parent().active_channel].flatten().tolist()
12370
+ data = self.parent().channel_data[self.parent().active_channel]
12371
+ nonzero_data = data[data != 0]
12372
+
12373
+ if nonzero_data.size > 578009537:
12374
+ # For large arrays, use numpy histogram directly
12375
+ counts, bin_edges = np.histogram(nonzero_data, bins= min(int(np.sqrt(nonzero_data.size)), 500), density=False)
12376
+ # Store min/max separately if needed elsewhere
12377
+ self.data_min = np.min(nonzero_data)
12378
+ self.data_max = np.max(nonzero_data)
12379
+ self.histo_list = [self.data_min, self.data_max]
12380
+ else:
12381
+ # For smaller arrays, can still use histogram method for consistency
12382
+ counts, bin_edges = np.histogram(nonzero_data, bins='auto', density=False)
12383
+ self.data_min = np.min(nonzero_data)
12384
+ self.data_max = np.max(nonzero_data)
12385
+ self.histo_list = [self.data_min, self.data_max]
12386
+
12295
12387
  self.bounds = True
12296
12388
  self.parent().bounds = True
12297
12389
 
@@ -12304,16 +12396,26 @@ class ThresholdWindow(QMainWindow):
12304
12396
  layout.addWidget(self.canvas)
12305
12397
 
12306
12398
  # Pre-compute histogram with numpy
12307
- counts, bin_edges = np.histogram(self.histo_list, bins=50)
12308
- bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
12399
+ if accepted_mode != 0:
12400
+ counts, bin_edges = np.histogram(self.histo_list, bins=50)
12401
+ bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
12402
+ # Store histogram bounds
12403
+ if self.bounds:
12404
+ self.data_min = 0
12405
+ else:
12406
+ self.data_min = min(self.histo_list)
12407
+ self.data_max = max(self.histo_list)
12408
+ else:
12409
+ bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
12410
+ bin_width = bin_edges[1] - bin_edges[0]
12309
12411
 
12310
12412
  # Plot pre-computed histogram
12311
12413
  self.ax = fig.add_subplot(111)
12312
12414
  self.ax.bar(bin_centers, counts, width=bin_edges[1] - bin_edges[0], alpha=0.5)
12313
12415
 
12314
12416
  # Add vertical lines for thresholds
12315
- self.min_line = self.ax.axvline(min(self.histo_list), color='r')
12316
- self.max_line = self.ax.axvline(max(self.histo_list), color='b')
12417
+ self.min_line = self.ax.axvline(self.data_min, color='r')
12418
+ self.max_line = self.ax.axvline(self.data_max, color='b')
12317
12419
 
12318
12420
  # Connect events for dragging
12319
12421
  self.canvas.mpl_connect('button_press_event', self.on_press)
@@ -12321,13 +12423,6 @@ class ThresholdWindow(QMainWindow):
12321
12423
  self.canvas.mpl_connect('button_release_event', self.on_release)
12322
12424
 
12323
12425
  self.dragging = None
12324
-
12325
- # Store histogram bounds
12326
- if self.bounds:
12327
- self.data_min = 0
12328
- else:
12329
- self.data_min = min(self.histo_list)
12330
- self.data_max = max(self.histo_list)
12331
12426
 
12332
12427
  # Create form layout for inputs
12333
12428
  form_layout = QFormLayout()
@@ -12360,7 +12455,7 @@ class ThresholdWindow(QMainWindow):
12360
12455
  button_layout.addWidget(run_button)
12361
12456
 
12362
12457
  # Add Cancel button for external dialog use
12363
- cancel_button = QPushButton("Cancel/Skip")
12458
+ cancel_button = QPushButton("Cancel/Skip (Retains Selection)")
12364
12459
  cancel_button.clicked.connect(self.cancel_processing)
12365
12460
  button_layout.addWidget(cancel_button)
12366
12461
 
@@ -15282,31 +15377,81 @@ class HistogramSelector(QWidget):
15282
15377
  """)
15283
15378
  layout.addWidget(button)
15284
15379
 
15380
+
15285
15381
  def shortest_path_histogram(self):
15286
15382
  try:
15287
- shortest_path_lengths = dict(nx.all_pairs_shortest_path_length(self.G))
15288
- diameter = max(nx.eccentricity(self.G, sp=shortest_path_lengths).values())
15289
- path_lengths = np.zeros(diameter + 1, dtype=int)
15290
-
15291
- for pls in shortest_path_lengths.values():
15292
- pl, cnts = np.unique(list(pls.values()), return_counts=True)
15383
+ # Check if graph has multiple disconnected components
15384
+ components = list(nx.connected_components(self.G))
15385
+
15386
+ if len(components) > 1:
15387
+ print(f"Warning: Graph has {len(components)} disconnected components. Computing shortest paths within each component separately.")
15388
+
15389
+ # Initialize variables to collect data from all components
15390
+ all_path_lengths = []
15391
+ max_diameter = 0
15392
+
15393
+ # Process each component separately
15394
+ for i, component in enumerate(components):
15395
+ subgraph = self.G.subgraph(component)
15396
+
15397
+ if len(component) < 2:
15398
+ # Skip single-node components (no paths to compute)
15399
+ continue
15400
+
15401
+ # Compute shortest paths for this component
15402
+ shortest_path_lengths = dict(nx.all_pairs_shortest_path_length(subgraph))
15403
+ component_diameter = max(nx.eccentricity(subgraph, sp=shortest_path_lengths).values())
15404
+ max_diameter = max(max_diameter, component_diameter)
15405
+
15406
+ # Collect path lengths from this component
15407
+ for pls in shortest_path_lengths.values():
15408
+ all_path_lengths.extend(list(pls.values()))
15409
+
15410
+ # Remove self-paths (length 0) and create histogram
15411
+ all_path_lengths = [pl for pl in all_path_lengths if pl > 0]
15412
+
15413
+ if not all_path_lengths:
15414
+ print("No paths found across components (only single-node components)")
15415
+ return
15416
+
15417
+ # Create combined histogram
15418
+ path_lengths = np.zeros(max_diameter + 1, dtype=int)
15419
+ pl, cnts = np.unique(all_path_lengths, return_counts=True)
15293
15420
  path_lengths[pl] += cnts
15294
-
15421
+
15422
+ title_suffix = f" (across {len(components)} components)"
15423
+
15424
+ else:
15425
+ # Single component
15426
+ shortest_path_lengths = dict(nx.all_pairs_shortest_path_length(self.G))
15427
+ diameter = max(nx.eccentricity(self.G, sp=shortest_path_lengths).values())
15428
+ path_lengths = np.zeros(diameter + 1, dtype=int)
15429
+ for pls in shortest_path_lengths.values():
15430
+ pl, cnts = np.unique(list(pls.values()), return_counts=True)
15431
+ path_lengths[pl] += cnts
15432
+ max_diameter = diameter
15433
+ title_suffix = ""
15434
+
15435
+ # Generate visualization and results (same for both cases)
15295
15436
  freq_percent = 100 * path_lengths[1:] / path_lengths[1:].sum()
15296
-
15297
15437
  fig, ax = plt.subplots(figsize=(15, 8))
15298
- ax.bar(np.arange(1, diameter + 1), height=freq_percent)
15438
+ ax.bar(np.arange(1, max_diameter + 1), height=freq_percent)
15299
15439
  ax.set_title(
15300
- "Distribution of shortest path length in G", fontdict={"size": 35}, loc="center"
15440
+ f"Distribution of shortest path length in G{title_suffix}",
15441
+ fontdict={"size": 35}, loc="center"
15301
15442
  )
15302
15443
  ax.set_xlabel("Shortest Path Length", fontdict={"size": 22})
15303
15444
  ax.set_ylabel("Frequency (%)", fontdict={"size": 22})
15304
15445
  plt.show()
15305
15446
 
15306
15447
  freq_dict = {freq: length for length, freq in enumerate(freq_percent, start=1)}
15307
- self.network_analysis.format_for_upperright_table(freq_dict, metric='Frequency (%)',
15308
- value='Shortest Path Length',
15309
- title="Distribution of shortest path length in G")
15448
+ self.network_analysis.format_for_upperright_table(
15449
+ freq_dict,
15450
+ metric='Frequency (%)',
15451
+ value='Shortest Path Length',
15452
+ title=f"Distribution of shortest path length in G{title_suffix}"
15453
+ )
15454
+
15310
15455
  except Exception as e:
15311
15456
  print(f"Error generating shortest path histogram: {e}")
15312
15457
 
@@ -15325,20 +15470,62 @@ class HistogramSelector(QWidget):
15325
15470
  title="Degree Centrality Table")
15326
15471
  except Exception as e:
15327
15472
  print(f"Error generating degree centrality histogram: {e}")
15328
-
15473
+
15329
15474
  def betweenness_centrality_histogram(self):
15330
15475
  try:
15331
- betweenness_centrality = nx.centrality.betweenness_centrality(self.G)
15476
+ # Check if graph has multiple disconnected components
15477
+ components = list(nx.connected_components(self.G))
15478
+
15479
+ if len(components) > 1:
15480
+ print(f"Warning: Graph has {len(components)} disconnected components. Computing betweenness centrality within each component separately.")
15481
+
15482
+ # Initialize dictionary to collect betweenness centrality from all components
15483
+ combined_betweenness_centrality = {}
15484
+
15485
+ # Process each component separately
15486
+ for i, component in enumerate(components):
15487
+ if len(component) < 2:
15488
+ # For single-node components, betweenness centrality is 0
15489
+ for node in component:
15490
+ combined_betweenness_centrality[node] = 0.0
15491
+ continue
15492
+
15493
+ # Create subgraph for this component
15494
+ subgraph = self.G.subgraph(component)
15495
+
15496
+ # Compute betweenness centrality for this component
15497
+ component_betweenness = nx.centrality.betweenness_centrality(subgraph)
15498
+
15499
+ # Add to combined results
15500
+ combined_betweenness_centrality.update(component_betweenness)
15501
+
15502
+ betweenness_centrality = combined_betweenness_centrality
15503
+ title_suffix = f" (across {len(components)} components)"
15504
+
15505
+ else:
15506
+ # Single component
15507
+ betweenness_centrality = nx.centrality.betweenness_centrality(self.G)
15508
+ title_suffix = ""
15509
+
15510
+ # Generate visualization and results (same for both cases)
15332
15511
  plt.figure(figsize=(15, 8))
15333
15512
  plt.hist(betweenness_centrality.values(), bins=100)
15334
15513
  plt.xticks(ticks=[0, 0.02, 0.1, 0.2, 0.3, 0.4, 0.5])
15335
- plt.title("Betweenness Centrality Histogram ", fontdict={"size": 35}, loc="center")
15514
+ plt.title(
15515
+ f"Betweenness Centrality Histogram{title_suffix}",
15516
+ fontdict={"size": 35}, loc="center"
15517
+ )
15336
15518
  plt.xlabel("Betweenness Centrality", fontdict={"size": 20})
15337
15519
  plt.ylabel("Counts", fontdict={"size": 20})
15338
15520
  plt.show()
15339
- self.network_analysis.format_for_upperright_table(betweenness_centrality, metric='Node',
15340
- value='Betweenness Centrality',
15341
- title="Betweenness Centrality Table")
15521
+
15522
+ self.network_analysis.format_for_upperright_table(
15523
+ betweenness_centrality,
15524
+ metric='Node',
15525
+ value='Betweenness Centrality',
15526
+ title=f"Betweenness Centrality Table{title_suffix}"
15527
+ )
15528
+
15342
15529
  except Exception as e:
15343
15530
  print(f"Error generating betweenness centrality histogram: {e}")
15344
15531
 
@@ -15391,7 +15578,27 @@ class HistogramSelector(QWidget):
15391
15578
  def bridges_analysis(self):
15392
15579
  try:
15393
15580
  bridges = list(nx.bridges(self.G))
15394
- self.network_analysis.format_for_upperright_table(bridges, metric='Node Pair',
15581
+ try:
15582
+ # Get the existing DataFrame from the model
15583
+ original_df = self.network_analysis.network_table.model()._data
15584
+
15585
+ # Create boolean mask
15586
+ mask = pd.Series([False] * len(original_df))
15587
+
15588
+ for u, v in bridges:
15589
+ # Check for both (u,v) and (v,u) orientations
15590
+ bridge_mask = (
15591
+ ((original_df.iloc[:, 0] == u) & (original_df.iloc[:, 1] == v)) |
15592
+ ((original_df.iloc[:, 0] == v) & (original_df.iloc[:, 1] == u))
15593
+ )
15594
+ mask |= bridge_mask
15595
+ # Filter the DataFrame to only include bridge connections
15596
+ filtered_df = original_df[mask].copy()
15597
+ df_dict = {i: row.tolist() for i, row in enumerate(filtered_df.values)}
15598
+ self.network_analysis.format_for_upperright_table(df_dict, metric='Bridge ID', value = ['NodeA', 'NodeB', 'EdgeC'],
15599
+ title="Bridges")
15600
+ except:
15601
+ self.network_analysis.format_for_upperright_table(bridges, metric='Node Pair',
15395
15602
  title="Bridges")
15396
15603
  except Exception as e:
15397
15604
  print(f"Error generating bridges analysis: {e}")
@@ -15418,7 +15625,7 @@ class HistogramSelector(QWidget):
15418
15625
  def node_connectivity_histogram(self):
15419
15626
  """Local node connectivity - minimum number of nodes that must be removed to disconnect neighbors"""
15420
15627
  try:
15421
- if self.G.number_of_nodes() > 500: # Skip for large networks (computationally expensive)
15628
+ if self.G.number_of_nodes() > 500:
15422
15629
  print("Note this analysis may be slow for large network (>500 nodes)")
15423
15630
  #return
15424
15631
 
@@ -15496,7 +15703,7 @@ class HistogramSelector(QWidget):
15496
15703
  def load_centrality_histogram(self):
15497
15704
  """Load centrality - fraction of shortest paths passing through each node"""
15498
15705
  try:
15499
- if self.G.number_of_nodes() > 1000: # Skip for very large networks
15706
+ if self.G.number_of_nodes() > 1000:
15500
15707
  print("Note this analysis may be slow for large network (>1000 nodes)")
15501
15708
  #return
15502
15709
 
@@ -15515,21 +15722,67 @@ class HistogramSelector(QWidget):
15515
15722
  def communicability_centrality_histogram(self):
15516
15723
  """Communicability centrality - based on communicability between nodes"""
15517
15724
  try:
15518
- if self.G.number_of_nodes() > 500: # Skip for large networks (memory intensive)
15725
+ if self.G.number_of_nodes() > 500:
15519
15726
  print("Note this analysis may be slow for large network (>500 nodes)")
15520
15727
  #return
15728
+
15729
+ # Check if graph has multiple disconnected components
15730
+ components = list(nx.connected_components(self.G))
15731
+
15732
+ if len(components) > 1:
15733
+ print(f"Warning: Graph has {len(components)} disconnected components. Computing communicability centrality within each component separately.")
15734
+
15735
+ # Initialize dictionary to collect communicability centrality from all components
15736
+ combined_comm_centrality = {}
15737
+
15738
+ # Process each component separately
15739
+ for i, component in enumerate(components):
15740
+ if len(component) < 2:
15741
+ # For single-node components, communicability betweenness centrality is 0
15742
+ for node in component:
15743
+ combined_comm_centrality[node] = 0.0
15744
+ continue
15745
+
15746
+ # Create subgraph for this component
15747
+ subgraph = self.G.subgraph(component)
15748
+
15749
+ # Compute communicability betweenness centrality for this component
15750
+ try:
15751
+ component_comm_centrality = nx.communicability_betweenness_centrality(subgraph)
15752
+ # Add to combined results
15753
+ combined_comm_centrality.update(component_comm_centrality)
15754
+ except Exception as comp_e:
15755
+ print(f"Error computing communicability centrality for component {i+1}: {comp_e}")
15756
+ # Set centrality to 0 for nodes in this component if computation fails
15757
+ for node in component:
15758
+ combined_comm_centrality[node] = 0.0
15759
+
15760
+ comm_centrality = combined_comm_centrality
15761
+ title_suffix = f" (across {len(components)} components)"
15521
15762
 
15522
- # Use the correct function name - it's in the communicability module
15523
- comm_centrality = nx.communicability_betweenness_centrality(self.G)
15763
+ else:
15764
+ # Single component
15765
+ comm_centrality = nx.communicability_betweenness_centrality(self.G)
15766
+ title_suffix = ""
15767
+
15768
+ # Generate visualization and results (same for both cases)
15524
15769
  plt.figure(figsize=(15, 8))
15525
15770
  plt.hist(comm_centrality.values(), bins=50, alpha=0.7)
15526
- plt.title("Communicability Betweenness Centrality Distribution", fontdict={"size": 35}, loc="center")
15771
+ plt.title(
15772
+ f"Communicability Betweenness Centrality Distribution{title_suffix}",
15773
+ fontdict={"size": 35}, loc="center"
15774
+ )
15527
15775
  plt.xlabel("Communicability Betweenness Centrality", fontdict={"size": 20})
15528
15776
  plt.ylabel("Frequency", fontdict={"size": 20})
15529
15777
  plt.show()
15530
- self.network_analysis.format_for_upperright_table(comm_centrality, metric='Node',
15531
- value='Communicability Betweenness Centrality',
15532
- title="Communicability Betweenness Centrality Table")
15778
+
15779
+ self.network_analysis.format_for_upperright_table(
15780
+ comm_centrality,
15781
+ metric='Node',
15782
+ value='Communicability Betweenness Centrality',
15783
+ title=f"Communicability Betweenness Centrality Table{title_suffix}"
15784
+ )
15785
+
15533
15786
  except Exception as e:
15534
15787
  print(f"Error generating communicability betweenness centrality histogram: {e}")
15535
15788
 
@@ -15552,20 +15805,67 @@ class HistogramSelector(QWidget):
15552
15805
  def current_flow_betweenness_histogram(self):
15553
15806
  """Current flow betweenness - models network as electrical circuit"""
15554
15807
  try:
15555
- if self.G.number_of_nodes() > 500: # Skip for large networks (computationally expensive)
15808
+ if self.G.number_of_nodes() > 500:
15556
15809
  print("Note this analysis may be slow for large network (>500 nodes)")
15557
15810
  #return
15811
+
15812
+ # Check if graph has multiple disconnected components
15813
+ components = list(nx.connected_components(self.G))
15814
+
15815
+ if len(components) > 1:
15816
+ print(f"Warning: Graph has {len(components)} disconnected components. Computing current flow betweenness centrality within each component separately.")
15817
+
15818
+ # Initialize dictionary to collect current flow betweenness from all components
15819
+ combined_current_flow = {}
15558
15820
 
15559
- current_flow = nx.current_flow_betweenness_centrality(self.G)
15821
+ # Process each component separately
15822
+ for i, component in enumerate(components):
15823
+ if len(component) < 2:
15824
+ # For single-node components, current flow betweenness centrality is 0
15825
+ for node in component:
15826
+ combined_current_flow[node] = 0.0
15827
+ continue
15828
+
15829
+ # Create subgraph for this component
15830
+ subgraph = self.G.subgraph(component)
15831
+
15832
+ # Compute current flow betweenness centrality for this component
15833
+ try:
15834
+ component_current_flow = nx.current_flow_betweenness_centrality(subgraph)
15835
+ # Add to combined results
15836
+ combined_current_flow.update(component_current_flow)
15837
+ except Exception as comp_e:
15838
+ print(f"Error computing current flow betweenness for component {i+1}: {comp_e}")
15839
+ # Set centrality to 0 for nodes in this component if computation fails
15840
+ for node in component:
15841
+ combined_current_flow[node] = 0.0
15842
+
15843
+ current_flow = combined_current_flow
15844
+ title_suffix = f" (across {len(components)} components)"
15845
+
15846
+ else:
15847
+ # Single component
15848
+ current_flow = nx.current_flow_betweenness_centrality(self.G)
15849
+ title_suffix = ""
15850
+
15851
+ # Generate visualization and results (same for both cases)
15560
15852
  plt.figure(figsize=(15, 8))
15561
15853
  plt.hist(current_flow.values(), bins=50, alpha=0.7)
15562
- plt.title("Current Flow Betweenness Centrality Distribution", fontdict={"size": 35}, loc="center")
15854
+ plt.title(
15855
+ f"Current Flow Betweenness Centrality Distribution{title_suffix}",
15856
+ fontdict={"size": 35}, loc="center"
15857
+ )
15563
15858
  plt.xlabel("Current Flow Betweenness Centrality", fontdict={"size": 20})
15564
15859
  plt.ylabel("Frequency", fontdict={"size": 20})
15565
15860
  plt.show()
15566
- self.network_analysis.format_for_upperright_table(current_flow, metric='Node',
15567
- value='Current Flow Betweenness',
15568
- title="Current Flow Betweenness Table")
15861
+
15862
+ self.network_analysis.format_for_upperright_table(
15863
+ current_flow,
15864
+ metric='Node',
15865
+ value='Current Flow Betweenness',
15866
+ title=f"Current Flow Betweenness Table{title_suffix}"
15867
+ )
15868
+
15569
15869
  except Exception as e:
15570
15870
  print(f"Error generating current flow betweenness histogram: {e}")
15571
15871