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

@@ -239,14 +239,14 @@ class ImageViewerWindow(QMainWindow):
239
239
 
240
240
 
241
241
  self.toggle_scale = QPushButton("📏")
242
- self.toggle_scale.setFixedSize(20, 20)
242
+ self.toggle_scale.setFixedSize(32, 32)
243
243
  self.toggle_scale.clicked.connect(self.toggle_scalebar)
244
244
  self.toggle_scale.setCheckable(True)
245
245
  self.toggle_scale.setChecked(False)
246
246
  control_layout.addWidget(self.toggle_scale)
247
247
 
248
248
  self.reset_view = QPushButton("🏠")
249
- self.reset_view.setFixedSize(20, 20)
249
+ self.reset_view.setFixedSize(32, 32)
250
250
  self.reset_view.clicked.connect(self.home)
251
251
  control_layout.addWidget(self.reset_view)
252
252
 
@@ -559,24 +559,26 @@ class ImageViewerWindow(QMainWindow):
559
559
  # Create graph widgets
560
560
  self.network_graph_widget = ngw.NetworkGraphWidget(
561
561
  parent=self,
562
- weight=False,
562
+ weight=True,
563
563
  geometric=False,
564
564
  centroids=None,
565
- communities=True,
565
+ communities=False,
566
566
  community_dict=None,
567
567
  labels=True,
568
- z_size = True
568
+ z_size = True,
569
+ popout = True
569
570
  )
570
571
 
571
572
  self.selection_graph_widget = ngw.NetworkGraphWidget(
572
573
  parent=self,
573
- weight=False,
574
+ weight=True,
574
575
  geometric=False,
575
576
  centroids=None,
576
577
  communities=False,
577
578
  community_dict=None,
578
579
  labels=True,
579
- z_size = True
580
+ z_size = True,
581
+ popout = True
580
582
  )
581
583
 
582
584
  # Create both table views
@@ -656,7 +658,11 @@ class ImageViewerWindow(QMainWindow):
656
658
  self.disable_pan = False
657
659
  self.grid_ready = False
658
660
  self.remove_scale = False
661
+ self.remove_grid = False
659
662
  self.original_dims = None
663
+ self.thresh_min = None
664
+ self.thresh_max = None
665
+ self.temp_graph_widgets = []
660
666
 
661
667
  #Deprecated:
662
668
  self.figure = Figure(figsize=(8, 8))
@@ -1283,11 +1289,13 @@ class ImageViewerWindow(QMainWindow):
1283
1289
  self.selection_graph_widget.select_nodes(node_indices)
1284
1290
  except:
1285
1291
  pass
1286
- try:
1287
- if self.temp_graph_widget.rendered:
1288
- self.temp_graph_widget.select_nodes(node_indices)
1289
- except:
1290
- pass
1292
+
1293
+ for graph in self.temp_graph_widgets:
1294
+ try:
1295
+ if graph.rendered:
1296
+ graph.select_nodes(node_indices)
1297
+ except:
1298
+ pass
1291
1299
 
1292
1300
  def table_subgraph(self, table_widget, table):
1293
1301
 
@@ -3127,7 +3135,7 @@ class ImageViewerWindow(QMainWindow):
3127
3135
 
3128
3136
  if my_network.network_lists is not None:
3129
3137
  for i in range(len(my_network.network_lists[0]) - 1, -1, -1):
3130
- if my_network.network_lists[0][i] in self.clicked_values['nodes'] or my_network.network_lists[0][i] in self.clicked_values['nodes']:
3138
+ if my_network.network_lists[0][i] in self.clicked_values['nodes'] or my_network.network_lists[1][i] in self.clicked_values['nodes']:
3131
3139
  del my_network.network_lists[0][i]
3132
3140
  del my_network.network_lists[1][i]
3133
3141
  del my_network.network_lists[2][i]
@@ -3319,6 +3327,7 @@ class ImageViewerWindow(QMainWindow):
3319
3327
  self.update_display(preserve_zoom=(self.ax.get_xlim(), self.ax.get_ylim()))
3320
3328
  self.toggle_scale.setChecked(True)
3321
3329
  self.remove_scale = False
3330
+ self.remove_grid = True
3322
3331
  return
3323
3332
 
3324
3333
  if self.scale_bar:
@@ -3326,7 +3335,9 @@ class ImageViewerWindow(QMainWindow):
3326
3335
  self._draw_scalebar()
3327
3336
  self.update_display(preserve_zoom=(self.ax.get_xlim(), self.ax.get_ylim()))
3328
3337
  else:
3338
+ self.coord_label.setText(" ")
3329
3339
  self.grid_ready = False
3340
+ self.remove_grid = False
3330
3341
  self.toggle_grid()
3331
3342
  self._remove_scalebar()
3332
3343
  self.update_display(preserve_zoom=(self.ax.get_xlim(), self.ax.get_ylim()))
@@ -3820,7 +3831,13 @@ class ImageViewerWindow(QMainWindow):
3820
3831
 
3821
3832
  mouse_point = self.view.mapSceneToView(pos)
3822
3833
  x, y = mouse_point.x(), mouse_point.y()
3823
-
3834
+
3835
+ try:
3836
+ if self.remove_grid or self.remove_scale:
3837
+ self.coord_label.setText(f"Z: {self.current_slice}, Y: {int(y)}, X: {int(x)}")
3838
+ except:
3839
+ pass
3840
+
3824
3841
  try:
3825
3842
  # Check if within image bounds
3826
3843
  if self.original_dims is None:
@@ -4439,7 +4456,7 @@ class ImageViewerWindow(QMainWindow):
4439
4456
  allstats_action = stats_net_menu.addAction("Calculate Generic Network Stats")
4440
4457
  allstats_action.triggered.connect(self.stats)
4441
4458
  histos_action = stats_net_menu.addAction("Network Statistic Histograms")
4442
- histos_action.triggered.connect(self.histos)
4459
+ histos_action.triggered.connect(self.histograms)
4443
4460
  radial_action = stats_net_menu.addAction("Radial Distribution Analysis")
4444
4461
  radial_action.triggered.connect(self.show_radial_dialog)
4445
4462
  heatmap_action = stats_net_menu.addAction("Community Cluster Heatmap")
@@ -4606,7 +4623,19 @@ class ImageViewerWindow(QMainWindow):
4606
4623
  corner_widget = QWidget()
4607
4624
  corner_layout = QHBoxLayout(corner_widget)
4608
4625
  corner_layout.setContentsMargins(5, 0, 5, 0)
4609
-
4626
+
4627
+ self.coord_label = QLabel(f" ")
4628
+ self.coord_label.setText(f" ")
4629
+ corner_layout.addWidget(self.coord_label)
4630
+
4631
+ self.xy_scale_label = QLabel(f"xy_scale: {my_network.xy_scale} ")
4632
+ self.xy_scale_label.setText(f"xy_scale: {my_network.xy_scale} ")
4633
+ corner_layout.addWidget(self.xy_scale_label)
4634
+
4635
+ self.z_scale_label = QLabel(f"z_scale: {my_network.z_scale} ")
4636
+ self.z_scale_label.setText(f"z_scale: {my_network.z_scale} ")
4637
+ corner_layout.addWidget(self.z_scale_label)
4638
+
4610
4639
  self.z_label = QLabel(f"Slice {self.current_slice}")
4611
4640
  self.z_label.setText(f"Slice {self.current_slice}")
4612
4641
  corner_layout.addWidget(self.z_label)
@@ -4948,9 +4977,11 @@ class ImageViewerWindow(QMainWindow):
4948
4977
 
4949
4978
  self.format_for_upperright_table(stats, title = 'Network Stats')
4950
4979
  except Exception as e:
4980
+ import traceback
4981
+ traceback.print_exc()
4951
4982
  print(f"Error finding stats: {e}")
4952
4983
 
4953
- def histos(self):
4984
+ def histograms(self):
4954
4985
  """
4955
4986
  Show a PyQt6 window with buttons to select which histogram to generate.
4956
4987
  Only calculates the histogram that the user selects.
@@ -4962,7 +4993,8 @@ class ImageViewerWindow(QMainWindow):
4962
4993
  app = QApplication(sys.argv)
4963
4994
 
4964
4995
  # Create and show the histogram selector window
4965
- self.histogram_selector = HistogramSelector(self, self.stats_dict)
4996
+ from . import histos
4997
+ self.histogram_selector = histos.HistogramSelector(self, self.stats_dict, my_network.network)
4966
4998
  self.histogram_selector.show()
4967
4999
 
4968
5000
  # Keep the window open (you might want to handle this differently based on your application structure)
@@ -5572,7 +5604,8 @@ class ImageViewerWindow(QMainWindow):
5572
5604
  try:
5573
5605
  if sort == 'Node Identities':
5574
5606
  my_network.load_node_identities(file_path = filename)
5575
-
5607
+ self.network_graph_widget.identity_dict = my_network.node_identities
5608
+ self.selection_graph_widget.identity_dict = my_network.node_identities
5576
5609
 
5577
5610
  if hasattr(my_network, 'node_identities') and my_network.node_identities is not None:
5578
5611
  try:
@@ -5583,6 +5616,7 @@ class ImageViewerWindow(QMainWindow):
5583
5616
  elif sort == 'Node Centroids':
5584
5617
  my_network.load_node_centroids(file_path = filename)
5585
5618
  self.network_graph_widget.centroids = my_network.node_centroids
5619
+ self.selection_graph_widget.centroids = my_network.node_centroids
5586
5620
 
5587
5621
  if hasattr(my_network, 'node_centroids') and my_network.node_centroids is not None:
5588
5622
  try:
@@ -5601,6 +5635,9 @@ class ImageViewerWindow(QMainWindow):
5601
5635
  elif sort == 'Communities':
5602
5636
  my_network.load_communities(file_path = filename)
5603
5637
 
5638
+ self.network_graph_widget.community_dict = my_network.communities
5639
+ self.selection_graph_widget.community_dict = my_network.communities
5640
+
5604
5641
  if hasattr(my_network, 'communities') and my_network.communities is not None:
5605
5642
  try:
5606
5643
  self.format_for_upperright_table(my_network.communities, 'NodeID', 'Identity', 'Node Communities')
@@ -5755,11 +5792,21 @@ class ImageViewerWindow(QMainWindow):
5755
5792
  self.network_graph_widget.set_graph(None)
5756
5793
  self.network_graph_widget.community_dict = None
5757
5794
  self.network_graph_widget.identity_dict = None
5758
- self.network_graph_widget.node_centroids = None
5795
+ self.network_graph_widget.centroids = None
5759
5796
  self.selection_graph_widget.set_graph(None)
5760
5797
  self.selection_graph_widget.community_dict = None
5761
5798
  self.selection_graph_widget.identity_dict = None
5762
- self.selection_graph_widget.node_centroids = None
5799
+ self.selection_graph_widget.centroids = None
5800
+
5801
+ def update_graph_fields(self):
5802
+
5803
+ #self.network_graph_widget.set_graph(my_network.network)
5804
+ self.network_graph_widget.community_dict = my_network.communities
5805
+ self.network_graph_widget.identity_dict = my_network.node_identities
5806
+ self.network_graph_widget.centroids = my_network.node_centroids
5807
+ self.selection_graph_widget.community_dict = my_network.communities
5808
+ self.selection_graph_widget.identity_dict = my_network.node_identities
5809
+ self.selection_graph_widget.centroids = my_network.node_centroids
5763
5810
 
5764
5811
  # Modify load_from_network_obj method
5765
5812
  def load_from_network_obj(self, directory = None):
@@ -5776,7 +5823,8 @@ class ImageViewerWindow(QMainWindow):
5776
5823
 
5777
5824
  self.last_load = directory
5778
5825
  self.last_saved = os.path.dirname(directory)
5779
- self.last_save_name = directory
5826
+ self.last_save_name = directory
5827
+ self.setWindowTitle(f"NetTracer3D - Session: {self.last_save_name}")
5780
5828
 
5781
5829
  self.channel_data = [None] * 5
5782
5830
  if directory != "":
@@ -5785,8 +5833,11 @@ class ImageViewerWindow(QMainWindow):
5785
5833
 
5786
5834
  self.clear_subgraphs()
5787
5835
  my_network.assemble(directory)
5836
+ self.xy_scale_label.setText(f"xy_scale: {my_network.xy_scale:.2e} ")
5837
+ self.z_scale_label.setText(f"z_scale: {my_network.z_scale:.2e} ")
5788
5838
  self.network_graph_widget.set_graph(my_network.network)
5789
5839
  self.network_graph_widget.centroids = my_network.node_centroids
5840
+ self.selection_graph_widget.centroids = my_network.node_centroids
5790
5841
  #self.network_graph_widget.load_graph()
5791
5842
 
5792
5843
  # Load image channelsTrue
@@ -6100,12 +6151,14 @@ class ImageViewerWindow(QMainWindow):
6100
6151
  msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
6101
6152
  return msg.exec() == QMessageBox.StandardButton.Yes
6102
6153
 
6103
- def confirm_resize_dialog(self):
6154
+ def confirm_resize_dialog(self, shapes):
6104
6155
  """Shows a dialog asking user to resize image"""
6156
+ old_shape = shapes[0]
6157
+ new_shape = shapes[1]
6105
6158
  msg = QMessageBox()
6106
6159
  msg.setIcon(QMessageBox.Icon.Question)
6107
6160
  msg.setText("Image Format Alert")
6108
- msg.setInformativeText(f"This image is a different shape than the ones loaded into the viewer window. This program is not designed to accomodate loading of differently sized images.\nPress yes to resize the new image to the other images. Press no to go back.")
6161
+ msg.setInformativeText(f"This image is a different shape (New shape: {new_shape}) than the ones loaded into the viewer window (Current shape: {old_shape}). This program is not designed to accomodate loading of differently sized images.\nPress yes to resize the new image to the other images. Press no to go back.")
6109
6162
  msg.setWindowTitle("Resize")
6110
6163
  msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
6111
6164
  return msg.exec() == QMessageBox.StandardButton.Yes
@@ -6174,6 +6227,8 @@ class ImageViewerWindow(QMainWindow):
6174
6227
  try:
6175
6228
  my_network.xy_scale, my_network.z_scale = self.get_scaling_metadata_only(filename)
6176
6229
  print(f"xy_scale property set to {my_network.xy_scale}; z_scale property set to {my_network.z_scale}")
6230
+ self.xy_scale_label.setText(f"xy_scale: {my_network.xy_scale:.2e} ")
6231
+ self.z_scale_label.setText(f"z_scale: {my_network.z_scale:.2e} ")
6177
6232
  except:
6178
6233
  pass
6179
6234
  test_channel_data = tifffile.imread(filename)
@@ -6211,8 +6266,8 @@ class ImageViewerWindow(QMainWindow):
6211
6266
  else:
6212
6267
  self.channel_data[channel_index] = channel_data
6213
6268
  if channel_data is None:
6214
- self.channel_buttons[channel_index].setEnabled(False)
6215
- self.delete_buttons[channel_index].setEnabled(False)
6269
+ self.delete_channel(channel_index, called = False, update = True)
6270
+ return
6216
6271
 
6217
6272
  try:
6218
6273
  #if len(self.channel_data[channel_index].shape) == 4:
@@ -6268,7 +6323,7 @@ class ImageViewerWindow(QMainWindow):
6268
6323
  else:
6269
6324
  old_shape = self.channel_data[i].shape[:3] #Ask user to resize images that are shaped differently
6270
6325
  if old_shape != self.channel_data[channel_index].shape[:3]:
6271
- if self.confirm_resize_dialog():
6326
+ if self.confirm_resize_dialog([old_shape, self.channel_data[channel_index].shape[:3]]):
6272
6327
  self.channel_data[channel_index] = n3d.upsample_with_padding(self.channel_data[channel_index], original_shape = old_shape)
6273
6328
  break
6274
6329
  else:
@@ -6454,6 +6509,8 @@ class ImageViewerWindow(QMainWindow):
6454
6509
  # Set scales first before any clearing operations
6455
6510
  my_network.xy_scale = xy_scale
6456
6511
  my_network.z_scale = z_scale
6512
+ self.xy_scale_label.setText(f"xy_scale: {my_network.xy_scale:.2e} ")
6513
+ self.z_scale_label.setText(f"z_scale: {my_network.z_scale:.2e} ")
6457
6514
 
6458
6515
  if network:
6459
6516
  my_network.network = None
@@ -6495,51 +6552,67 @@ class ImageViewerWindow(QMainWindow):
6495
6552
  def save_network_3d(self, asbool=True):
6496
6553
  try:
6497
6554
  if asbool: # Save As
6498
- # First let user select parent directory
6499
- parent_dir = QFileDialog.getExistingDirectory(
6555
+ # Use getSaveFileName which allows non-existent paths
6556
+ # This lets users navigate to parent AND type the child folder name in one step
6557
+ full_path, _ = QFileDialog.getSaveFileName(
6500
6558
  self,
6501
- "Select Location for Network3D Object Outputs",
6559
+ "Select Location and Name for Network3D Output Folder",
6502
6560
  "",
6503
- QFileDialog.Option.ShowDirsOnly
6504
- )
6505
- if not parent_dir: # If user canceled the directory selection
6506
- return # Exit the method early
6507
-
6508
- # Prompt user for new folder name
6509
- new_folder_name, ok = QInputDialog.getText(
6510
- self,
6511
- "New Folder",
6512
- "Enter name for new output folder:"
6561
+ "Folder (*.folder)", # Dummy filter, we'll ignore the extension
6562
+ options=QFileDialog.Option.DontConfirmOverwrite # Don't warn about overwriting
6513
6563
  )
6514
6564
 
6515
- # Check if user canceled the folder name dialog
6516
- if not ok or not new_folder_name:
6517
- return # Exit the method early
6518
-
6519
- else: # Save
6565
+ if not full_path: # User canceled
6566
+ return
6567
+
6568
+ # Parse the result: extract parent directory and folder name
6569
+ import os
6570
+ parent_dir = os.path.dirname(full_path)
6571
+ new_folder_name = os.path.basename(full_path)
6572
+
6573
+ # Remove any extension the user might have typed (from dummy filter)
6574
+ if new_folder_name.endswith('.folder'):
6575
+ new_folder_name = new_folder_name[:-7]
6576
+
6577
+ # Validate parent directory exists
6578
+ if not os.path.isdir(parent_dir):
6579
+ QMessageBox.critical(
6580
+ self,
6581
+ "Invalid Location",
6582
+ f"Parent directory does not exist: {parent_dir}"
6583
+ )
6584
+ return
6585
+
6586
+ # Validate folder name is not empty
6587
+ if not new_folder_name:
6588
+ QMessageBox.critical(
6589
+ self,
6590
+ "Invalid Name",
6591
+ "Please enter a name for the output folder."
6592
+ )
6593
+ return
6520
6594
 
6595
+ else: # Save
6521
6596
  if self.last_saved is None:
6522
6597
  self.save_network_3d()
6598
+ return
6523
6599
  else:
6524
- if len(self.channel_data[0].shape) == 4:
6525
- try:
6526
- self.load_channel(0, self.reduce_rgb_dimension(self.channel_data[0], 'weight'), True)
6527
- except:
6528
- pass
6529
- my_network.dump(parent_dir=self.last_saved, name=self.last_save_name)
6530
-
6600
+ parent_dir = self.last_saved
6601
+ new_folder_name = self.last_save_name
6602
+
6603
+ # Handle RGB dimension reduction before saving
6531
6604
  if len(self.channel_data[0].shape) == 4:
6532
6605
  try:
6533
6606
  self.load_channel(0, self.reduce_rgb_dimension(self.channel_data[0], 'weight'), True)
6534
6607
  except:
6535
6608
  pass
6536
-
6609
+
6537
6610
  # Call appropriate save method
6538
- if asbool:
6539
- my_network.dump(parent_dir=parent_dir, name=new_folder_name)
6540
- self.last_saved = parent_dir
6541
- self.last_save_name = new_folder_name
6542
-
6611
+ my_network.dump(parent_dir=parent_dir, name=new_folder_name)
6612
+ self.last_saved = parent_dir
6613
+ self.last_save_name = new_folder_name
6614
+ self.setWindowTitle(f"NetTracer3D - Session: {self.last_save_name}")
6615
+
6543
6616
  except Exception as e:
6544
6617
  QMessageBox.critical(
6545
6618
  self,
@@ -6565,10 +6638,24 @@ class ImageViewerWindow(QMainWindow):
6565
6638
  if not filename.endswith(('.tif', '.tiff')):
6566
6639
  filename += '.tif'
6567
6640
  else: # Save
6568
- filename = None # Let the backend handle default save location
6641
+ if self.last_saved is None:
6642
+ self.save(ch_index)
6643
+ return
6644
+ else:
6645
+ if ch_index == 0:
6646
+ filename = self.last_save_name + "/labelled_nodes.tif"
6647
+ print(filename)
6648
+ elif ch_index == 1:
6649
+ filename = self.last_save_name + "/labelled_edges.tif"
6650
+ elif ch_index == 2:
6651
+ filename = self.last_save_name + "/overlay_1.tif"
6652
+ elif ch_index == 3:
6653
+ filename = self.last_save_name + "/overlay_2.tif"
6654
+ elif ch_index == 4:
6655
+ filename = self.last_save_name + "/Highlighted_Element.tif"
6569
6656
 
6570
6657
  # Call appropriate save method
6571
- if filename is not None or not asbool: # Proceed if we have a filename OR if it's a regular save
6658
+ if (filename is not None and filename != "") or not asbool: # Proceed if we have a filename OR if it's a regular save
6572
6659
  if ch_index == 0:
6573
6660
  my_network.save_nodes(filename=filename)
6574
6661
  elif ch_index == 1:
@@ -6618,7 +6705,7 @@ class ImageViewerWindow(QMainWindow):
6618
6705
  self.pending_slice = (self.slice_slider.value(), (current_xlim, current_ylim))
6619
6706
 
6620
6707
  # Reset and restart timer
6621
- self._slice_update_timer.start(20) # 20ms delay
6708
+ self._slice_update_timer.start(1) # 20ms delay
6622
6709
 
6623
6710
  def _do_slice_update(self):
6624
6711
  """Actually perform the slice update after debounce delay."""
@@ -7089,7 +7176,7 @@ class ImageViewerWindow(QMainWindow):
7089
7176
  try:
7090
7177
  # Basic graph properties
7091
7178
  stats['num_nodes'] = my_network.network.number_of_nodes()
7092
- stats['num_edges'] = my_network.network.number_of_edges()
7179
+ stats['num_edges'] = len(my_network.network_lists[0])
7093
7180
  except:
7094
7181
  try:
7095
7182
  stats['num_nodes'] = len(np.unique(my_network.nodes)) - 1
@@ -8217,7 +8304,7 @@ class BrightnessContrastDialog(QDialog):
8217
8304
  self.debounce_timer.setSingleShot(True)
8218
8305
  self.debounce_timer.timeout.connect(self._apply_pending_updates)
8219
8306
  self.pending_updates = {}
8220
- self.debounce_delay = 20 # 300ms delay
8307
+ self.debounce_delay = 1 # 300ms delay
8221
8308
 
8222
8309
  # Connect signals
8223
8310
  slider.valueChanged.connect(lambda values, ch=i: self.on_slider_change(ch, values))
@@ -8688,6 +8775,7 @@ class MergeNodeIdDialog(QDialog):
8688
8775
 
8689
8776
  # For loop example - get threshold for multiple images/data
8690
8777
  results = []
8778
+ thresh_dict = {}
8691
8779
 
8692
8780
  img_list = n3d.directory_info(selected_path)
8693
8781
  data_backup = copy.deepcopy(data)
@@ -8702,7 +8790,7 @@ class MergeNodeIdDialog(QDialog):
8702
8790
  if img.endswith('.tiff') or img.endswith('.tif'):
8703
8791
 
8704
8792
  print(f"Please threshold {img}")
8705
-
8793
+ self.parent().setWindowTitle(f"NetTracer3D: Please threshold {img}")
8706
8794
 
8707
8795
  mask = tifffile.imread(f'{selected_path}/{img}')
8708
8796
  self.parent().load_channel(2, mask, data = True)
@@ -8712,6 +8800,8 @@ class MergeNodeIdDialog(QDialog):
8712
8800
  processing_completed = self.wait_for_threshold_processing()
8713
8801
 
8714
8802
  if not processing_completed:
8803
+ self.parent().thresh_min = None
8804
+ self.parent().thresh_max = None
8715
8805
  # User cancelled, ask if they want to continue
8716
8806
  reply = QMessageBox.question(self, 'Continue?',
8717
8807
  f'Threshold cancelled for item {i+1}. Continue with remaining items?',
@@ -8720,6 +8810,7 @@ class MergeNodeIdDialog(QDialog):
8720
8810
  break
8721
8811
  continue
8722
8812
 
8813
+ thresh_dict[img] = [self.parent().thresh_min, self.parent().thresh_max]
8723
8814
  # At this point, the thresholded image is in the main window's memory
8724
8815
  # Get the processed/thresholded data from wherever ThresholdWindow stored it
8725
8816
  thresholded_vals = list(np.unique(self.parent().channel_data[0]))
@@ -8773,8 +8864,10 @@ class MergeNodeIdDialog(QDialog):
8773
8864
  pass
8774
8865
 
8775
8866
  my_network.node_identities = modify_dict
8776
-
8867
+ self.parent().setWindowTitle(f"NetTracer3D")
8868
+
8777
8869
  self.parent().format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity')
8870
+ self.parent().format_for_upperright_table(thresh_dict, 'Identity', ['Min Value', 'Max Value'], 'Threshold Information')
8778
8871
 
8779
8872
  all_keys = id_dicts[0].keys()
8780
8873
  result = {key: np.array([d[key] for d in id_dicts]) for key in all_keys}
@@ -9223,7 +9316,7 @@ class ShuffleDialog(QDialog):
9223
9316
  except:
9224
9317
  self.parent().highlight_overay = None
9225
9318
  else:
9226
- self.parent().load_channel(accepted_mode, channel_data = target_data, data = True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
9319
+ self.parent().load_channel(accepted_mode, channel_data = target_data, data = True)
9227
9320
  except:
9228
9321
  pass
9229
9322
 
@@ -9235,14 +9328,14 @@ class ShuffleDialog(QDialog):
9235
9328
  except:
9236
9329
  self.parent().highlight_overlay = None
9237
9330
  else:
9238
- self.parent().load_channel(accepted_target, channel_data = active_data, data = True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
9331
+ self.parent().load_channel(accepted_target, channel_data = active_data, data = True)
9239
9332
  except:
9240
9333
  pass
9241
9334
 
9242
9335
 
9243
9336
 
9244
9337
 
9245
- self.parent().update_display(preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
9338
+ self.parent().update_display()
9246
9339
 
9247
9340
  self.accept()
9248
9341
 
@@ -9265,40 +9358,68 @@ class NetShowDialog(QDialog):
9265
9358
  self.setWindowTitle("Display Parameters")
9266
9359
  self.setModal(True)
9267
9360
 
9268
- layout = QFormLayout(self)
9361
+ main_layout = QVBoxLayout(self)
9269
9362
 
9270
9363
  self.called = called
9271
9364
 
9365
+ layout_group = QGroupBox("Layout")
9366
+ layout_layout = QFormLayout(layout_group)
9367
+
9272
9368
  # Add mode selection dropdown
9273
9369
  self.render_mode = QComboBox()
9274
- self.render_mode.addItems(["Spring Layout (Try to Logically Group Nodes)", "Centroid Layout (Place Nodes to Match Image)", "Component Layout (Separate All Nontouching Components)"])
9370
+ self.render_mode.addItems(["Spring Layout (Try to Logically Group Nodes)", "Centroid Layout (Place Nodes to Match Image)", "Component Layout Spring (Separate All Nontouching Components)", "Component Layout Shell (Centrally Places Important Nodes)"])
9275
9371
  self.render_mode.setCurrentIndex(0) # Default to Mode 1
9276
- layout.addRow("Render Mode:", self.render_mode)
9372
+ layout_layout.addRow("Render Mode:", self.render_mode)
9277
9373
 
9278
9374
  # Add mode selection dropdown
9279
9375
  self.mode_selector = QComboBox()
9280
9376
  self.mode_selector.addItems(["Default", "Community Coded", "Node ID Coded"])
9281
9377
  self.mode_selector.setCurrentIndex(0) # Default to Mode 1
9282
- layout.addRow("Execution Mode:", self.mode_selector)
9378
+ layout_layout.addRow("Execution Mode:", self.mode_selector)
9379
+
9380
+ main_layout.addWidget(layout_group)
9381
+
9382
+ render_group = QGroupBox("Node/Edge Rendering")
9383
+ render_layout = QFormLayout(render_group)
9384
+
9385
+ # Add mode selection dropdown
9386
+ self.edge_color = QComboBox()
9387
+ self.edge_color.addItems(["Translucent Gray", "Solid Black"])
9388
+ self.edge_color.setCurrentIndex(0) # Default to Mode 1
9389
+ render_layout.addRow("Edge Color:", self.edge_color)
9390
+
9391
+ self.node_size = QLineEdit("10")
9392
+ render_layout.addRow("Node Sizes", self.node_size)
9393
+
9394
+ self.edge_size = QLineEdit("1")
9395
+ render_layout.addRow("Edge Sizes", self.edge_size)
9396
+
9397
+ main_layout.addWidget(render_group)
9398
+
9399
+ misc_group = QGroupBox("Misc")
9400
+ misc_layout = QFormLayout(misc_group)
9283
9401
 
9284
9402
  self.show_labels = QCheckBox("Show Node Numerical IDs?")
9285
9403
  self.show_labels.setChecked(True)
9286
- layout.addRow(self.show_labels)
9404
+ misc_layout.addRow(self.show_labels)
9287
9405
 
9288
9406
  # weighted checkbox
9289
9407
  self.weighted = QCheckBox("Draw weighted edges (if applicable)")
9290
- self.weighted.setChecked(False)
9291
- layout.addRow(self.weighted)
9408
+ self.weighted.setChecked(True)
9409
+ misc_layout.addRow(self.weighted)
9292
9410
 
9293
9411
  # weighted checkbox (default True)
9294
9412
  self.z_size = QCheckBox("For Centroid Layout: Scale Node Sizes by Z?")
9295
9413
  self.z_size.setChecked(True)
9296
- layout.addRow(self.z_size)
9414
+ misc_layout.addRow(self.z_size)
9415
+
9416
+ main_layout.addWidget(misc_group)
9417
+
9297
9418
 
9298
9419
  # Add Run button
9299
9420
  run_button = QPushButton("Show Network")
9300
9421
  run_button.clicked.connect(self.show_network)
9301
- layout.addWidget(run_button)
9422
+ main_layout.addWidget(run_button)
9302
9423
 
9303
9424
  def show_network(self):
9304
9425
 
@@ -9309,12 +9430,18 @@ class NetShowDialog(QDialog):
9309
9430
 
9310
9431
  geo = False
9311
9432
  component = False
9433
+ shell = False
9312
9434
 
9313
9435
  render = self.render_mode.currentIndex()
9436
+
9437
+ edge_color = self.edge_color.currentIndex()
9438
+
9314
9439
  if render == 1:
9315
9440
  geo = True
9316
9441
  elif render == 2:
9317
9442
  component = True
9443
+ elif render == 3:
9444
+ shell = True
9318
9445
  if geo:
9319
9446
  if my_network.node_centroids is None:
9320
9447
  self.parent().show_centroid_dialog()
@@ -9322,6 +9449,17 @@ class NetShowDialog(QDialog):
9322
9449
  # Get directory (None if empty)
9323
9450
  directory = None
9324
9451
 
9452
+ try:
9453
+ node_size = min(abs(int(self.node_size.text())), 100)
9454
+ except:
9455
+ node_size = 10
9456
+
9457
+ try:
9458
+ edge_size = min(abs(int(self.edge_size.text())), 100)
9459
+ except:
9460
+ edge_size = 1
9461
+
9462
+
9325
9463
  weighted = self.weighted.isChecked()
9326
9464
  z_size = self.z_size.isChecked()
9327
9465
 
@@ -9333,9 +9471,20 @@ class NetShowDialog(QDialog):
9333
9471
  elif accepted_mode == 2:
9334
9472
  identities = True
9335
9473
 
9474
+ if accepted_mode == 1:
9475
+
9476
+ if my_network.communities is None:
9477
+ self.parent().show_partition_dialog()
9478
+ if my_network.communities is None:
9479
+ return
9480
+ if edge_color == 0:
9481
+ black_edges = False
9482
+ else:
9483
+ black_edges = True
9484
+
9336
9485
  if not self.called:
9337
9486
  # Create graph widgets
9338
- self.parent().temp_graph_widget = ngw.NetworkGraphWidget(
9487
+ temp_graph_widget = ngw.NetworkGraphWidget(
9339
9488
  parent=self.parent(),
9340
9489
  weight=weighted,
9341
9490
  geometric=geo,
@@ -9346,15 +9495,20 @@ class NetShowDialog(QDialog):
9346
9495
  labels=show_labels,
9347
9496
  identities = identities,
9348
9497
  identity_dict = my_network.node_identities,
9349
- z_size = z_size
9498
+ z_size = z_size,
9499
+ shell = shell,
9500
+ node_size = node_size,
9501
+ black_edges = black_edges,
9502
+ edge_size = edge_size
9350
9503
  )
9351
9504
 
9352
- self.parent().temp_graph_widget.set_graph(my_network.network)
9353
- self.parent().temp_graph_widget.show_in_window(title="Network Graph", width=1000, height=800)
9354
- self.parent().temp_graph_widget.load_graph()
9505
+ temp_graph_widget.set_graph(my_network.network)
9506
+ temp_graph_widget.show_in_window(title="Network Graph", width=1000, height=800)
9507
+ temp_graph_widget.load_graph()
9508
+ self.parent().temp_graph_widgets.append(temp_graph_widget)
9355
9509
  self.accept()
9356
9510
  else:
9357
- self.called.weight, self.called.geometric, self.called.component, self.called.centroids, self.called.communities, self.called.community_dict, self.called.labels, self.called.identities, self.called.identity_dict, self.called.z_size = weighted, geo, component, my_network.node_centroids, communities, my_network.communities, show_labels, identities, my_network.node_identities, z_size
9511
+ self.called.weight, self.called.geometric, self.called.component, self.called.centroids, self.called.communities, self.called.community_dict, self.called.labels, self.called.identities, self.called.identity_dict, self.called.z_size, self.called.shell, self.called.node_size, self.called.black_edges, self.called.edge_size = edge_size = weighted, geo, component, my_network.node_centroids, communities, my_network.communities, show_labels, identities, my_network.node_identities, z_size, shell, node_size, black_edges, edge_size
9358
9512
  self.called._clear_graph()
9359
9513
  self.called.load_graph()
9360
9514
  self.accept()
@@ -9362,30 +9516,6 @@ class NetShowDialog(QDialog):
9362
9516
  except Exception as e:
9363
9517
  print(f"Error: {e}")
9364
9518
 
9365
- """
9366
-
9367
- if accepted_mode == 1:
9368
-
9369
- if my_network.communities is None:
9370
- self.parent().show_partition_dialog()
9371
- if my_network.communities is None:
9372
- return
9373
-
9374
- try:
9375
- if accepted_mode == 0:
9376
- my_network.show_network(geometric=geo, show_labels = show_labels)
9377
- elif accepted_mode == 1:
9378
- my_network.show_communities_flex(geometric=geo, weighted = weighted, partition = my_network.communities, show_labels = show_labels)
9379
- self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'CommunityID')
9380
- elif accepted_mode == 2:
9381
- my_network.show_identity_network(geometric=geo, show_labels = show_labels)
9382
-
9383
- self.accept()
9384
- except Exception as e:
9385
- print(f"Error showing network: {e}")
9386
- import traceback
9387
- print(traceback.format_exc())
9388
- """
9389
9519
 
9390
9520
  class PartitionDialog(QDialog):
9391
9521
  def __init__(self, parent=None):
@@ -9397,7 +9527,7 @@ class PartitionDialog(QDialog):
9397
9527
  layout = QFormLayout(self)
9398
9528
 
9399
9529
  # weighted checkbox (default True)
9400
- self.weighted = QPushButton("weighted")
9530
+ self.weighted = QPushButton("weighted (Considers Duplicate Connections)")
9401
9531
  self.weighted.setCheckable(True)
9402
9532
  self.weighted.setChecked(True)
9403
9533
  layout.addRow("Use Weighted Network:", self.weighted)
@@ -11588,6 +11718,8 @@ class ResizeDialog(QDialog):
11588
11718
  else:
11589
11719
  my_network.xy_scale = cardinal
11590
11720
  my_network.z_scale = cardinal
11721
+ self.parent().xy_scale_label.setText(f"xy_scale: {my_network.xy_scale:.2e} ")
11722
+ self.parent().z_scale_label.setText(f"z_scale: {my_network.z_scale:.2e} ")
11591
11723
 
11592
11724
  try:
11593
11725
  if my_network.node_centroids is not None:
@@ -11675,16 +11807,62 @@ class CleanDialog(QDialog):
11675
11807
  run_button.clicked.connect(self.holes)
11676
11808
  layout.addRow("Call the fill holes function:", run_button)
11677
11809
 
11810
+ # Add Run button
11811
+ run_button = QPushButton("Connect Endpoints")
11812
+ run_button.clicked.connect(self.endpoints)
11813
+ layout.addRow("Connect Endpoints? (Unsupervised - Weak to noise):", run_button)
11814
+
11678
11815
  # Add Run button
11679
11816
  run_button = QPushButton("Trace Filaments")
11680
11817
  run_button.clicked.connect(self.fils)
11681
- layout.addRow("For Segmentations of Blood Vessels/Nerves: ", run_button)
11818
+ layout.addRow("For Segmentations of Blood Vessels/Nerves:", run_button)
11682
11819
 
11683
11820
  # Add Run button
11684
11821
  run_button = QPushButton("Threshold Noise")
11685
11822
  run_button.clicked.connect(self.thresh)
11686
11823
  layout.addRow("Threshold Noise By Volume:", run_button)
11687
11824
 
11825
+ def endpoints(self):
11826
+
11827
+ class CleanDialog(QDialog):
11828
+ def __init__(self, parent=None):
11829
+ super().__init__(parent)
11830
+ self.setWindowTitle("Rote Endpoint Joining - Connects ALL Detectable Endpoints Within Distance")
11831
+ self.setModal(True)
11832
+
11833
+ layout = QFormLayout(self)
11834
+
11835
+ self.amount = QLineEdit("10")
11836
+ layout.addRow("Voxel Distance to Connect Endpoints (Will be slow if large):", self.amount)
11837
+
11838
+ self.spine = QLineEdit("0")
11839
+ layout.addRow("Skeleton Spine Removal Distance:", self.spine)
11840
+
11841
+ run_button = QPushButton("Run")
11842
+ run_button.clicked.connect(self.run)
11843
+ layout.addRow(run_button)
11844
+
11845
+ def run(self):
11846
+ try:
11847
+ amount = float(self.amount.text())
11848
+ except:
11849
+ return
11850
+ try:
11851
+ spine = int(self.spine.text())
11852
+ except:
11853
+ spine = 0
11854
+
11855
+ try:
11856
+ from . import endpoint_joiner
11857
+ joined = endpoint_joiner.connect_endpoints(self.parent().channel_data[self.parent().active_channel], amount, spine)
11858
+ self.parent().load_channel(3, joined, data = True)
11859
+ self.accept()
11860
+ except Exception as e:
11861
+ print(f"Error: {e}")
11862
+
11863
+ dialog = CleanDialog(self.parent())
11864
+ dialog.exec()
11865
+
11688
11866
  def close(self):
11689
11867
 
11690
11868
  try:
@@ -12439,8 +12617,7 @@ class MachineWindow(QMainWindow):
12439
12617
  full_button = QPushButton("Segment All")
12440
12618
  full_button.clicked.connect(self.segment)
12441
12619
  segmentation_layout.addWidget(seg_button)
12442
- segmentation_layout.addWidget(self.pause_button) # <--- for some reason the segmenter preview is still running even when killed, may be regenerating itself somewhere. May or may not actually try to resolve this because this feature isnt that necessary.
12443
- #segmentation_layout.addWidget(self.lock_button) # Also turned this off
12620
+ segmentation_layout.addWidget(self.pause_button)
12444
12621
  segmentation_layout.addWidget(full_button)
12445
12622
  segmentation_group.setLayout(segmentation_layout)
12446
12623
 
@@ -12852,9 +13029,6 @@ class MachineWindow(QMainWindow):
12852
13029
  current_time = time.time()
12853
13030
  if current_time - self._last_update >= 1: # Match worker's interval
12854
13031
  try:
12855
- # Store current view state
12856
- current_xlim = self.parent().ax.get_xlim()
12857
- current_ylim = self.parent().ax.get_ylim()
12858
13032
 
12859
13033
  try:
12860
13034
  x, y = self.parent().get_current_mouse_position()
@@ -12864,7 +13038,7 @@ class MachineWindow(QMainWindow):
12864
13038
 
12865
13039
  if not self.parent().painting:
12866
13040
  # Only update if view limits are valid
12867
- self.parent().update_display(preserve_zoom=(current_xlim, current_ylim))
13041
+ self.parent().update_display()
12868
13042
 
12869
13043
  self._last_update = current_time
12870
13044
  except Exception as e:
@@ -12978,7 +13152,8 @@ class MachineWindow(QMainWindow):
12978
13152
  pass
12979
13153
 
12980
13154
  self.parent().machine_window = None
12981
- event.accept() # IMPORTANT: Accept the close event
13155
+ self.parent().highlight_overlay = None
13156
+ event.accept()
12982
13157
  else:
12983
13158
  event.ignore() # User cancelled, ignore the close
12984
13159
  else:
@@ -13074,17 +13249,11 @@ class SegmentationWorker(QThread):
13074
13249
  current_time = time.time()
13075
13250
  if (self.chunks_since_update >= self.chunks_per_update and
13076
13251
  current_time - self.last_update >= self.update_interval):
13077
- #if self.machine_window.parent().shape[1] * self.machine_window.parent().shape[2] > 3000 * 3000: #arbitrary throttle for large arrays.
13078
- #self.msleep(3000)
13079
13252
  self.chunk_processed.emit()
13080
13253
  self.chunks_since_update = 0
13081
13254
  self.last_update = current_time
13082
-
13083
- current_xlim = self.machine_window.parent().ax.get_xlim()
13084
-
13085
- current_ylim = self.machine_window.parent().ax.get_ylim()
13086
13255
 
13087
- self.machine_window.parent().update_display(preserve_zoom=(current_xlim, current_ylim))
13256
+ self.machine_window.parent().update_display()
13088
13257
 
13089
13258
  self.finished.emit()
13090
13259
 
@@ -13509,6 +13678,8 @@ class ThresholdWindow(QMainWindow):
13509
13678
  channel_data = self.parent().channel_data[self.parent().active_channel]
13510
13679
  mask = self.parent().highlight_overlay > 0
13511
13680
  channel_data = channel_data * mask
13681
+ self.parent().thresh_min = self.prev_min
13682
+ self.parent().thresh_max = self.prev_max
13512
13683
  self.parent().load_channel(self.parent().active_channel, channel_data, True)
13513
13684
  self.parent().update_display()
13514
13685
  self.close()
@@ -13863,7 +14034,7 @@ class FilamentDialog(QDialog):
13863
14034
  speedup_group = QGroupBox("Speedup")
13864
14035
  speedup_layout = QFormLayout()
13865
14036
  self.kernel_spacing = QLineEdit("3")
13866
- speedup_layout.addRow("Kernel Spacing (1 is most accurate, can increase to speed up):", self.kernel_spacing)
14037
+ speedup_layout.addRow("Kernel Spacing (lower is more sensitive to gaps, can increase to speed up or if too many gaps filled):", self.kernel_spacing)
13867
14038
  self.downsample_factor = QLineEdit("1")
13868
14039
  speedup_layout.addRow("Temporary Downsample Factor (Note that the below distances are not adjusted for this):", self.downsample_factor)
13869
14040
  speedup_group.setLayout(speedup_layout)
@@ -13874,7 +14045,7 @@ class FilamentDialog(QDialog):
13874
14045
  reconnection_layout = QFormLayout()
13875
14046
  self.max_distance = QLineEdit("20")
13876
14047
  reconnection_layout.addRow("Max Distance to Consider Connecting Filaments (Will Slow Down a lot if Large):", self.max_distance)
13877
- self.gap_tolerance = QLineEdit("5")
14048
+ self.gap_tolerance = QLineEdit("6")
13878
14049
  reconnection_layout.addRow("Gap Tolerance. Higher Values Increase Likelihood of Connecting over Larger Gaps:", self.gap_tolerance)
13879
14050
  self.score_threshold = QLineEdit("2")
13880
14051
  reconnection_layout.addRow("Connection Quality Threshold. Lower Values Increase Likelihood of Connecting In General, can be Negative:", self.score_threshold)
@@ -13894,6 +14065,8 @@ class FilamentDialog(QDialog):
13894
14065
  artifact_layout.addRow("Remove Branch Spines Below this Length?", self.spine_removal)
13895
14066
  artifact_group.setLayout(artifact_layout)
13896
14067
  main_layout.addWidget(artifact_group)
14068
+ self.state = None
14069
+ self.first = True
13897
14070
 
13898
14071
 
13899
14072
  # Run Button
@@ -13922,8 +14095,9 @@ class FilamentDialog(QDialog):
13922
14095
 
13923
14096
  if downsample_factor and downsample_factor > 1:
13924
14097
  data = n3d.downsample(data, downsample_factor)
14098
+ self.state = None
13925
14099
 
13926
- result = filaments.trace(data, kernel_spacing, max_distance, min_component, gap_tolerance, blob_sphericity, blob_volume, spine_removal, score_threshold, my_network.xy_scale, my_network.z_scale)
14100
+ result, self.state = filaments.trace(data, kernel_spacing, max_distance, min_component, gap_tolerance, blob_sphericity, blob_volume, spine_removal, score_threshold, my_network.xy_scale, my_network.z_scale, cached_state = self.state)
13927
14101
 
13928
14102
  if downsample_factor and downsample_factor > 1:
13929
14103
 
@@ -13932,51 +14106,22 @@ class FilamentDialog(QDialog):
13932
14106
 
13933
14107
  self.parent().load_channel(3, result, True)
13934
14108
 
13935
- self.accept()
14109
+ if self.first:
14110
+ QMessageBox.information(
14111
+ self,
14112
+ "Success",
14113
+ f"Filaments traced succesfully. Heavy computations are cached while this menu is open and so re-computing with different params will be fast. Feel free to try out other params. Altering kernel spacing, downsampling, or spine removal will reset this cache, however."
14114
+ )
14115
+ self.first = False
13936
14116
 
13937
14117
  except Exception as e:
13938
14118
  import traceback
13939
14119
  print(traceback.format_exc())
13940
14120
  print(f"Error: {e}")
13941
14121
 
13942
- def wait_for_threshold_processing(self):
13943
- """
13944
- Opens ThresholdWindow and waits for user to process the image.
13945
- Returns True if completed, False if cancelled.
13946
- The thresholded image will be available in the main window after completion.
13947
- """
13948
- # Create event loop to wait for user
13949
- loop = QEventLoop()
13950
- result = {'completed': False}
13951
-
13952
- # Create the threshold window
13953
- thresh_window = ThresholdWindow(self.parent(), 0)
13954
-
13955
-
13956
- # Connect signals
13957
- def on_processing_complete():
13958
- result['completed'] = True
13959
- loop.quit()
13960
-
13961
- def on_processing_cancelled():
13962
- result['completed'] = False
13963
- loop.quit()
13964
-
13965
- thresh_window.processing_complete.connect(on_processing_complete)
13966
- thresh_window.processing_cancelled.connect(on_processing_cancelled)
13967
-
13968
- # Show window and wait
13969
- thresh_window.show()
13970
- thresh_window.raise_()
13971
- thresh_window.activateWindow()
13972
-
13973
- # Block until user clicks "Apply Threshold & Continue" or "Cancel"
13974
- loop.exec()
13975
-
13976
- # Clean up
13977
- thresh_window.deleteLater()
13978
-
13979
- return result['completed']
14122
+ def closeEvent(self, event):
14123
+ self.state = None
14124
+ event.accept()
13980
14125
 
13981
14126
 
13982
14127
 
@@ -15064,6 +15209,8 @@ class GenNodesDialog(QDialog):
15064
15209
  my_network.xy_scale = my_network.xy_scale * down_factor
15065
15210
  my_network.z_scale = my_network.z_scale * down_factor
15066
15211
  print("xy_scales and z_scales have been adjusted per downsample. Check image -> properties to manually reset them to 1 if desired.")
15212
+ self.parent().xy_scale_label.setText(f"xy_scale: {my_network.xy_scale:.2e} ")
15213
+ self.parent().z_scale_label.setText(f"z_scale: {my_network.z_scale:.2e} ")
15067
15214
 
15068
15215
  try: #Resets centroid fields
15069
15216
  if my_network.node_centroids is not None:
@@ -15390,7 +15537,7 @@ class AlterDialog(QDialog):
15390
15537
  def __init__(self, parent=None):
15391
15538
  super().__init__(parent)
15392
15539
  self.setWindowTitle("Enter Node/Edge groups to add/remove")
15393
- self.setModal(True)
15540
+ self.setModal(False)
15394
15541
  layout = QFormLayout(self)
15395
15542
 
15396
15543
  # Node 1
@@ -15432,10 +15579,7 @@ class AlterDialog(QDialog):
15432
15579
  # Add edge value (0 if none provided)
15433
15580
  my_network.network_lists[2].append(edge if edge is not None else 0)
15434
15581
 
15435
- # Add reverse pair with same edge value
15436
- my_network.network_lists[0].append(node2)
15437
- my_network.network_lists[1].append(node1)
15438
- my_network.network_lists[2].append(edge if edge is not None else 0)
15582
+ my_network.network_lists = my_network.network_lists
15439
15583
  try:
15440
15584
  if hasattr(my_network, 'network_lists'):
15441
15585
  model = PandasModel(my_network.network_lists)
@@ -15443,6 +15587,8 @@ class AlterDialog(QDialog):
15443
15587
  # Adjust column widths to content
15444
15588
  for column in range(model.columnCount(None)):
15445
15589
  self.parent().network_table.resizeColumnToContents(column)
15590
+ self.parent().clear_subgraphs()
15591
+ self.parent().network_graph_widget.set_graph(my_network.network)
15446
15592
  except Exception as e:
15447
15593
  print(f"Error showing network table: {e}")
15448
15594
  except ValueError:
@@ -15482,6 +15628,7 @@ class AlterDialog(QDialog):
15482
15628
  my_network.network_lists[0].pop(i)
15483
15629
  my_network.network_lists[1].pop(i)
15484
15630
  my_network.network_lists[2].pop(i)
15631
+ my_network.network_lists = my_network.network_lists
15485
15632
 
15486
15633
  try:
15487
15634
  if hasattr(my_network, 'network_lists'):
@@ -15490,6 +15637,8 @@ class AlterDialog(QDialog):
15490
15637
  # Adjust column widths to content
15491
15638
  for column in range(model.columnCount(None)):
15492
15639
  self.parent().network_table.resizeColumnToContents(column)
15640
+ self.parent().clear_subgraphs()
15641
+ self.parent().network_graph_widget.set_graph(my_network.network)
15493
15642
  except Exception as e:
15494
15643
  print(f"Error showing network table: {e}")
15495
15644
 
@@ -15544,7 +15693,7 @@ class ModifyDialog(QDialog):
15544
15693
  self.edgeweight = QPushButton("Remove weights")
15545
15694
  self.edgeweight.setCheckable(True)
15546
15695
  self.edgeweight.setChecked(False)
15547
- layout.addRow("Remove network weights?:", self.edgeweight)
15696
+ layout.addRow("Remove network weights (Represent Duplicate Connections)?:", self.edgeweight)
15548
15697
 
15549
15698
  # prune checkbox (default false)
15550
15699
  self.prune = QPushButton("Prune Same Type")
@@ -15588,7 +15737,7 @@ class ModifyDialog(QDialog):
15588
15737
  def show_alter_dialog(self):
15589
15738
 
15590
15739
  dialog = AlterDialog(self.parent())
15591
- dialog.exec()
15740
+ dialog.show()
15592
15741
 
15593
15742
  def run_changes(self):
15594
15743
 
@@ -15696,6 +15845,8 @@ class ModifyDialog(QDialog):
15696
15845
  # Adjust column widths to content
15697
15846
  for column in range(model.columnCount(None)):
15698
15847
  self.parent().network_table.resizeColumnToContents(column)
15848
+ self.parent().clear_subgraphs()
15849
+ self.parent().network_graph_widget.set_graph(my_network.network)
15699
15850
  except Exception as e:
15700
15851
  print(f"Error showing network table: {e}")
15701
15852
 
@@ -15773,6 +15924,8 @@ class CentroidDialog(QDialog):
15773
15924
  down_factor = downsample
15774
15925
  )
15775
15926
  self.parent().network_graph_widget.centroids = my_network.node_centroids
15927
+ self.parent().selection_graph_widget.centroids = my_network.node_centroids
15928
+
15776
15929
 
15777
15930
  elif chan == 2:
15778
15931
  my_network.calculate_edge_centroids(
@@ -15785,6 +15938,7 @@ class CentroidDialog(QDialog):
15785
15938
  down_factor = downsample
15786
15939
  )
15787
15940
  self.parent().network_graph_widget.centroids = my_network.node_centroids
15941
+ self.parent().selection_graph_widget.centroids = my_network.node_centroids
15788
15942
 
15789
15943
  except:
15790
15944
  pass
@@ -16108,17 +16262,38 @@ class CalcAllDialog(QDialog):
16108
16262
 
16109
16263
  from . import network_analysis
16110
16264
 
16111
- new_lists = network_analysis.combine_lists_to_sublists(temp_network.network_lists)
16112
- old_lists = network_analysis.combine_lists_to_sublists(my_network.network_lists)
16113
-
16114
- filtered_new_lists = [pair for pair in new_lists if pair not in old_lists]
16115
-
16116
- list1, list2, list3 = zip(*filtered_new_lists)
16265
+ new_lists = network_analysis.combine_lists_to_sublists_no_edges([temp_network.network_lists[0], temp_network.network_lists[1]])
16266
+ old_lists = network_analysis.combine_lists_to_sublists_no_edges([my_network.network_lists[0], my_network.network_lists[1]])
16267
+ ref_lists = network_analysis.combine_lists_to_sublists(my_network.network_lists)
16268
+ old_dict = {}
16269
+ for i, pair in enumerate(old_lists):
16270
+ # Store both orientations of the pair
16271
+ old_dict[tuple(pair)] = i
16272
+ old_dict[tuple(reversed(pair))] = i
16273
+
16274
+ output_lists = []
16275
+ used_indices = set()
16276
+
16277
+ for pair in new_lists:
16278
+ pair_tuple = tuple(pair)
16279
+ if pair_tuple in old_dict:
16280
+ idx = old_dict[pair_tuple]
16281
+ if idx not in used_indices:
16282
+ output_lists.append(ref_lists[idx])
16283
+ used_indices.add(idx)
16284
+
16285
+ # Clean up old_lists and ref_lists by removing used items
16286
+ # Delete in reverse order to maintain indices
16287
+ for idx in sorted(used_indices, reverse=True):
16288
+ del ref_lists[idx]
16289
+ del old_lists[idx]
16290
+
16291
+ list1, list2, list3 = zip(*output_lists)
16117
16292
 
16118
16293
  # Convert them back to lists (zip returns tuples by default)
16119
- filtered_new_lists = [list(list1), list(list2), list(list3)]
16294
+ output_lists = [list(list1), list(list2), list(list3)]
16120
16295
 
16121
- my_network.network_lists = filtered_new_lists
16296
+ my_network.network_lists = output_lists
16122
16297
  del temp_network
16123
16298
 
16124
16299
  if edge_node and not labeled_branches:
@@ -16139,6 +16314,8 @@ class CalcAllDialog(QDialog):
16139
16314
 
16140
16315
  self.parent().clear_subgraphs()
16141
16316
  self.parent().network_graph_widget.set_graph(my_network.network)
16317
+ self.parent().xy_scale_label.setText(f"xy_scale: {my_network.xy_scale:.2e} ")
16318
+ self.parent().z_scale_label.setText(f"z_scale: {my_network.z_scale:.2e} ")
16142
16319
  # Then handle overlays
16143
16320
  if overlays:
16144
16321
  if directory is None:
@@ -16355,7 +16532,8 @@ class ProxDialog(QDialog):
16355
16532
 
16356
16533
  my_network.xy_scale = xy_scale
16357
16534
  my_network.z_scale = z_scale
16358
-
16535
+ self.parent().xy_scale_label.setText(f"xy_scale: {my_network.xy_scale:.2e} ")
16536
+ self.parent().z_scale_label.setText(f"z_scale: {my_network.z_scale:.2e} ")
16359
16537
 
16360
16538
  if mode == 1:
16361
16539
  if len(np.unique(my_network.nodes)) < 3:
@@ -16464,628 +16642,6 @@ class ProxDialog(QDialog):
16464
16642
  import traceback
16465
16643
  print(traceback.format_exc())
16466
16644
 
16467
-
16468
- class HistogramSelector(QWidget):
16469
- def __init__(self, network_analysis_instance, stats_dict):
16470
- super().__init__()
16471
- self.network_analysis = network_analysis_instance
16472
- self.stats_dict = stats_dict
16473
- self.G = my_network.network
16474
- self.init_ui()
16475
-
16476
- def init_ui(self):
16477
- self.setWindowTitle('Network Analysis - Histogram Selector')
16478
- self.setGeometry(300, 300, 400, 700) # Increased height for more buttons
16479
-
16480
- layout = QVBoxLayout()
16481
-
16482
- # Title label
16483
- title_label = QLabel('Select Histogram to Generate:')
16484
- title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
16485
- title_label.setStyleSheet("font-size: 16px; font-weight: bold; margin: 10px;")
16486
- layout.addWidget(title_label)
16487
-
16488
- # Create buttons for each histogram type
16489
- self.create_button(layout, "Shortest Path Length Distribution", self.shortest_path_histogram)
16490
- self.create_button(layout, "Degree Centrality", self.degree_centrality_histogram)
16491
- self.create_button(layout, "Betweenness Centrality", self.betweenness_centrality_histogram)
16492
- self.create_button(layout, "Closeness Centrality", self.closeness_centrality_histogram)
16493
- self.create_button(layout, "Eigenvector Centrality", self.eigenvector_centrality_histogram)
16494
- self.create_button(layout, "Clustering Coefficient", self.clustering_coefficient_histogram)
16495
- self.create_button(layout, "Degree Distribution", self.degree_distribution_histogram)
16496
- self.create_button(layout, "Node Connectivity", self.node_connectivity_histogram)
16497
- self.create_button(layout, "Eccentricity", self.eccentricity_histogram)
16498
- self.create_button(layout, "K-Core Decomposition", self.kcore_histogram)
16499
- self.create_button(layout, "Triangle Count", self.triangle_count_histogram)
16500
- self.create_button(layout, "Load Centrality", self.load_centrality_histogram)
16501
- self.create_button(layout, "Communicability Betweenness Centrality", self.communicability_centrality_histogram)
16502
- self.create_button(layout, "Harmonic Centrality", self.harmonic_centrality_histogram)
16503
- self.create_button(layout, "Current Flow Betweenness", self.current_flow_betweenness_histogram)
16504
- self.create_button(layout, "Dispersion", self.dispersion_histogram)
16505
- self.create_button(layout, "Network Bridges", self.bridges_analysis)
16506
-
16507
- # Close button
16508
- close_button = QPushButton('Close')
16509
- close_button.clicked.connect(self.close)
16510
- close_button.setStyleSheet("QPushButton { background-color: #f44336; color: white; font-weight: bold; }")
16511
- layout.addWidget(close_button)
16512
-
16513
- self.setLayout(layout)
16514
-
16515
- def create_button(self, layout, text, callback):
16516
- button = QPushButton(text)
16517
- button.clicked.connect(callback)
16518
- button.setMinimumHeight(40)
16519
- button.setStyleSheet("""
16520
- QPushButton {
16521
- background-color: #4CAF50;
16522
- color: white;
16523
- border: none;
16524
- padding: 10px;
16525
- font-size: 14px;
16526
- font-weight: bold;
16527
- border-radius: 5px;
16528
- }
16529
- QPushButton:hover {
16530
- background-color: #45a049;
16531
- }
16532
- QPushButton:pressed {
16533
- background-color: #3d8b40;
16534
- }
16535
- """)
16536
- layout.addWidget(button)
16537
-
16538
-
16539
- def shortest_path_histogram(self):
16540
- try:
16541
- # Check if graph has multiple disconnected components
16542
- components = list(nx.connected_components(self.G))
16543
-
16544
- if len(components) > 1:
16545
- print(f"Warning: Graph has {len(components)} disconnected components. Computing shortest paths within each component separately.")
16546
-
16547
- # Initialize variables to collect data from all components
16548
- all_path_lengths = []
16549
- max_diameter = 0
16550
-
16551
- # Process each component separately
16552
- for i, component in enumerate(components):
16553
- subgraph = self.G.subgraph(component)
16554
-
16555
- if len(component) < 2:
16556
- # Skip single-node components (no paths to compute)
16557
- continue
16558
-
16559
- # Compute shortest paths for this component
16560
- shortest_path_lengths = dict(nx.all_pairs_shortest_path_length(subgraph))
16561
- component_diameter = max(nx.eccentricity(subgraph, sp=shortest_path_lengths).values())
16562
- max_diameter = max(max_diameter, component_diameter)
16563
-
16564
- # Collect path lengths from this component
16565
- for pls in shortest_path_lengths.values():
16566
- all_path_lengths.extend(list(pls.values()))
16567
-
16568
- # Remove self-paths (length 0) and create histogram
16569
- all_path_lengths = [pl for pl in all_path_lengths if pl > 0]
16570
-
16571
- if not all_path_lengths:
16572
- print("No paths found across components (only single-node components)")
16573
- return
16574
-
16575
- # Create combined histogram
16576
- path_lengths = np.zeros(max_diameter + 1, dtype=int)
16577
- pl, cnts = np.unique(all_path_lengths, return_counts=True)
16578
- path_lengths[pl] += cnts
16579
-
16580
- title_suffix = f" (across {len(components)} components)"
16581
-
16582
- else:
16583
- # Single component
16584
- shortest_path_lengths = dict(nx.all_pairs_shortest_path_length(self.G))
16585
- diameter = max(nx.eccentricity(self.G, sp=shortest_path_lengths).values())
16586
- path_lengths = np.zeros(diameter + 1, dtype=int)
16587
- for pls in shortest_path_lengths.values():
16588
- pl, cnts = np.unique(list(pls.values()), return_counts=True)
16589
- path_lengths[pl] += cnts
16590
- max_diameter = diameter
16591
- title_suffix = ""
16592
-
16593
- # Generate visualization and results (same for both cases)
16594
- freq_percent = 100 * path_lengths[1:] / path_lengths[1:].sum()
16595
- fig, ax = plt.subplots(figsize=(15, 8))
16596
- ax.bar(np.arange(1, max_diameter + 1), height=freq_percent)
16597
- ax.set_title(
16598
- f"Distribution of shortest path length in G{title_suffix}",
16599
- fontdict={"size": 35}, loc="center"
16600
- )
16601
- ax.set_xlabel("Shortest Path Length", fontdict={"size": 22})
16602
- ax.set_ylabel("Frequency (%)", fontdict={"size": 22})
16603
- plt.show()
16604
-
16605
- freq_dict = {freq: length for length, freq in enumerate(freq_percent, start=1)}
16606
- self.network_analysis.format_for_upperright_table(
16607
- freq_dict,
16608
- metric='Frequency (%)',
16609
- value='Shortest Path Length',
16610
- title=f"Distribution of shortest path length in G{title_suffix}"
16611
- )
16612
-
16613
- except Exception as e:
16614
- print(f"Error generating shortest path histogram: {e}")
16615
-
16616
- def degree_centrality_histogram(self):
16617
- try:
16618
- degree_centrality = nx.centrality.degree_centrality(self.G)
16619
- plt.figure(figsize=(15, 8))
16620
- plt.hist(degree_centrality.values(), bins=25)
16621
- plt.xticks(ticks=[0, 0.025, 0.05, 0.1, 0.15, 0.2])
16622
- plt.title("Degree Centrality Histogram ", fontdict={"size": 35}, loc="center")
16623
- plt.xlabel("Degree Centrality", fontdict={"size": 20})
16624
- plt.ylabel("Counts", fontdict={"size": 20})
16625
- plt.show()
16626
- self.stats_dict['Degree Centrality'] = degree_centrality
16627
- self.network_analysis.format_for_upperright_table(degree_centrality, metric='Node',
16628
- value='Degree Centrality',
16629
- title="Degree Centrality Table")
16630
- except Exception as e:
16631
- print(f"Error generating degree centrality histogram: {e}")
16632
-
16633
- def betweenness_centrality_histogram(self):
16634
- try:
16635
- # Check if graph has multiple disconnected components
16636
- components = list(nx.connected_components(self.G))
16637
-
16638
- if len(components) > 1:
16639
- print(f"Warning: Graph has {len(components)} disconnected components. Computing betweenness centrality within each component separately.")
16640
-
16641
- # Initialize dictionary to collect betweenness centrality from all components
16642
- combined_betweenness_centrality = {}
16643
-
16644
- # Process each component separately
16645
- for i, component in enumerate(components):
16646
- if len(component) < 2:
16647
- # For single-node components, betweenness centrality is 0
16648
- for node in component:
16649
- combined_betweenness_centrality[node] = 0.0
16650
- continue
16651
-
16652
- # Create subgraph for this component
16653
- subgraph = self.G.subgraph(component)
16654
-
16655
- # Compute betweenness centrality for this component
16656
- component_betweenness = nx.centrality.betweenness_centrality(subgraph)
16657
-
16658
- # Add to combined results
16659
- combined_betweenness_centrality.update(component_betweenness)
16660
-
16661
- betweenness_centrality = combined_betweenness_centrality
16662
- title_suffix = f" (across {len(components)} components)"
16663
-
16664
- else:
16665
- # Single component
16666
- betweenness_centrality = nx.centrality.betweenness_centrality(self.G)
16667
- title_suffix = ""
16668
-
16669
- # Generate visualization and results (same for both cases)
16670
- plt.figure(figsize=(15, 8))
16671
- plt.hist(betweenness_centrality.values(), bins=100)
16672
- plt.xticks(ticks=[0, 0.02, 0.1, 0.2, 0.3, 0.4, 0.5])
16673
- plt.title(
16674
- f"Betweenness Centrality Histogram{title_suffix}",
16675
- fontdict={"size": 35}, loc="center"
16676
- )
16677
- plt.xlabel("Betweenness Centrality", fontdict={"size": 20})
16678
- plt.ylabel("Counts", fontdict={"size": 20})
16679
- plt.show()
16680
- self.stats_dict['Betweenness Centrality'] = betweenness_centrality
16681
-
16682
- self.network_analysis.format_for_upperright_table(
16683
- betweenness_centrality,
16684
- metric='Node',
16685
- value='Betweenness Centrality',
16686
- title=f"Betweenness Centrality Table{title_suffix}"
16687
- )
16688
-
16689
- except Exception as e:
16690
- print(f"Error generating betweenness centrality histogram: {e}")
16691
-
16692
- def closeness_centrality_histogram(self):
16693
- try:
16694
- closeness_centrality = nx.centrality.closeness_centrality(self.G)
16695
- plt.figure(figsize=(15, 8))
16696
- plt.hist(closeness_centrality.values(), bins=60)
16697
- plt.title("Closeness Centrality Histogram ", fontdict={"size": 35}, loc="center")
16698
- plt.xlabel("Closeness Centrality", fontdict={"size": 20})
16699
- plt.ylabel("Counts", fontdict={"size": 20})
16700
- plt.show()
16701
- self.stats_dict['Closeness Centrality'] = closeness_centrality
16702
- self.network_analysis.format_for_upperright_table(closeness_centrality, metric='Node',
16703
- value='Closeness Centrality',
16704
- title="Closeness Centrality Table")
16705
- except Exception as e:
16706
- print(f"Error generating closeness centrality histogram: {e}")
16707
-
16708
- def eigenvector_centrality_histogram(self):
16709
- try:
16710
- eigenvector_centrality = nx.centrality.eigenvector_centrality(self.G)
16711
- plt.figure(figsize=(15, 8))
16712
- plt.hist(eigenvector_centrality.values(), bins=60)
16713
- plt.xticks(ticks=[0, 0.01, 0.02, 0.04, 0.06, 0.08])
16714
- plt.title("Eigenvector Centrality Histogram ", fontdict={"size": 35}, loc="center")
16715
- plt.xlabel("Eigenvector Centrality", fontdict={"size": 20})
16716
- plt.ylabel("Counts", fontdict={"size": 20})
16717
- plt.show()
16718
- self.stats_dict['Eigenvector Centrality'] = eigenvector_centrality
16719
- self.network_analysis.format_for_upperright_table(eigenvector_centrality, metric='Node',
16720
- value='Eigenvector Centrality',
16721
- title="Eigenvector Centrality Table")
16722
- except Exception as e:
16723
- print(f"Error generating eigenvector centrality histogram: {e}")
16724
-
16725
- def clustering_coefficient_histogram(self):
16726
- try:
16727
- clusters = nx.clustering(self.G)
16728
- plt.figure(figsize=(15, 8))
16729
- plt.hist(clusters.values(), bins=50)
16730
- plt.title("Clustering Coefficient Histogram ", fontdict={"size": 35}, loc="center")
16731
- plt.xlabel("Clustering Coefficient", fontdict={"size": 20})
16732
- plt.ylabel("Counts", fontdict={"size": 20})
16733
- plt.show()
16734
- self.stats_dict['Clustering Coefficient'] = clusters
16735
- self.network_analysis.format_for_upperright_table(clusters, metric='Node',
16736
- value='Clustering Coefficient',
16737
- title="Clustering Coefficient Table")
16738
- except Exception as e:
16739
- print(f"Error generating clustering coefficient histogram: {e}")
16740
-
16741
- def bridges_analysis(self):
16742
- try:
16743
- bridges = list(nx.bridges(self.G))
16744
- try:
16745
- # Get the existing DataFrame from the model
16746
- original_df = self.network_analysis.network_table.model()._data
16747
-
16748
- # Create boolean mask
16749
- mask = pd.Series([False] * len(original_df))
16750
-
16751
- for u, v in bridges:
16752
- # Check for both (u,v) and (v,u) orientations
16753
- bridge_mask = (
16754
- ((original_df.iloc[:, 0] == u) & (original_df.iloc[:, 1] == v)) |
16755
- ((original_df.iloc[:, 0] == v) & (original_df.iloc[:, 1] == u))
16756
- )
16757
- mask |= bridge_mask
16758
- # Filter the DataFrame to only include bridge connections
16759
- filtered_df = original_df[mask].copy()
16760
- df_dict = {i: row.tolist() for i, row in enumerate(filtered_df.values)}
16761
- self.network_analysis.format_for_upperright_table(df_dict, metric='Bridge ID', value = ['NodeA', 'NodeB', 'EdgeC'],
16762
- title="Bridges")
16763
- except:
16764
- self.network_analysis.format_for_upperright_table(bridges, metric='Node Pair',
16765
- title="Bridges")
16766
- except Exception as e:
16767
- print(f"Error generating bridges analysis: {e}")
16768
-
16769
- def degree_distribution_histogram(self):
16770
- """Raw degree distribution - very useful for understanding network topology"""
16771
- try:
16772
- degrees = [self.G.degree(n) for n in self.G.nodes()]
16773
- plt.figure(figsize=(15, 8))
16774
- plt.hist(degrees, bins=max(30, int(np.sqrt(len(degrees)))), alpha=0.7)
16775
- plt.title("Degree Distribution", fontdict={"size": 35}, loc="center")
16776
- plt.xlabel("Degree", fontdict={"size": 20})
16777
- plt.ylabel("Frequency", fontdict={"size": 20})
16778
- plt.yscale('log') # Often useful for degree distributions
16779
- plt.show()
16780
-
16781
- degree_dict = {node: deg for node, deg in self.G.degree()}
16782
- self.network_analysis.format_for_upperright_table(degree_dict, metric='Node',
16783
- value='Degree', title="Degree Distribution Table")
16784
- except Exception as e:
16785
- print(f"Error generating degree distribution histogram: {e}")
16786
-
16787
-
16788
- def node_connectivity_histogram(self):
16789
- """Local node connectivity - minimum number of nodes that must be removed to disconnect neighbors"""
16790
- try:
16791
- if self.G.number_of_nodes() > 500:
16792
- print("Note this analysis may be slow for large network (>500 nodes)")
16793
- #return
16794
-
16795
- connectivity = {}
16796
- for node in self.G.nodes():
16797
- neighbors = list(self.G.neighbors(node))
16798
- if len(neighbors) > 1:
16799
- connectivity[node] = nx.node_connectivity(self.G, neighbors[0], neighbors[1])
16800
- else:
16801
- connectivity[node] = 0
16802
-
16803
- plt.figure(figsize=(15, 8))
16804
- plt.hist(connectivity.values(), bins=20, alpha=0.7)
16805
- plt.title("Node Connectivity Distribution", fontdict={"size": 35}, loc="center")
16806
- plt.xlabel("Node Connectivity", fontdict={"size": 20})
16807
- plt.ylabel("Frequency", fontdict={"size": 20})
16808
- plt.show()
16809
- self.stats_dict['Node Connectivity'] = connectivity
16810
- self.network_analysis.format_for_upperright_table(connectivity, metric='Node',
16811
- value='Connectivity', title="Node Connectivity Table")
16812
- except Exception as e:
16813
- print(f"Error generating node connectivity histogram: {e}")
16814
-
16815
- def eccentricity_histogram(self):
16816
- """Eccentricity - maximum distance from a node to any other node"""
16817
- try:
16818
- if not nx.is_connected(self.G):
16819
- print("Graph is not connected. Using largest connected component.")
16820
- largest_cc = max(nx.connected_components(self.G), key=len)
16821
- G_cc = self.G.subgraph(largest_cc)
16822
- eccentricity = nx.eccentricity(G_cc)
16823
- else:
16824
- eccentricity = nx.eccentricity(self.G)
16825
-
16826
- plt.figure(figsize=(15, 8))
16827
- plt.hist(eccentricity.values(), bins=20, alpha=0.7)
16828
- plt.title("Eccentricity Distribution", fontdict={"size": 35}, loc="center")
16829
- plt.xlabel("Eccentricity", fontdict={"size": 20})
16830
- plt.ylabel("Frequency", fontdict={"size": 20})
16831
- plt.show()
16832
- self.stats_dict['Eccentricity'] = eccentricity
16833
- self.network_analysis.format_for_upperright_table(eccentricity, metric='Node',
16834
- value='Eccentricity', title="Eccentricity Table")
16835
- except Exception as e:
16836
- print(f"Error generating eccentricity histogram: {e}")
16837
-
16838
- def kcore_histogram(self):
16839
- """K-core decomposition - identifies cohesive subgroups"""
16840
- try:
16841
- kcore = nx.core_number(self.G)
16842
- plt.figure(figsize=(15, 8))
16843
- plt.hist(kcore.values(), bins=max(5, max(kcore.values())), alpha=0.7)
16844
- plt.title("K-Core Distribution", fontdict={"size": 35}, loc="center")
16845
- plt.xlabel("K-Core Number", fontdict={"size": 20})
16846
- plt.ylabel("Frequency", fontdict={"size": 20})
16847
- plt.show()
16848
- self.stats_dict['K-Core'] = kcore
16849
- self.network_analysis.format_for_upperright_table(kcore, metric='Node',
16850
- value='K-Core', title="K-Core Table")
16851
- except Exception as e:
16852
- print(f"Error generating k-core histogram: {e}")
16853
-
16854
- def triangle_count_histogram(self):
16855
- """Number of triangles each node participates in"""
16856
- try:
16857
- triangles = nx.triangles(self.G)
16858
- plt.figure(figsize=(15, 8))
16859
- plt.hist(triangles.values(), bins=30, alpha=0.7)
16860
- plt.title("Triangle Count Distribution", fontdict={"size": 35}, loc="center")
16861
- plt.xlabel("Number of Triangles", fontdict={"size": 20})
16862
- plt.ylabel("Frequency", fontdict={"size": 20})
16863
- plt.show()
16864
- self.stats_dict['Triangle Count'] = triangles
16865
- self.network_analysis.format_for_upperright_table(triangles, metric='Node',
16866
- value='Triangle Count', title="Triangle Count Table")
16867
- except Exception as e:
16868
- print(f"Error generating triangle count histogram: {e}")
16869
-
16870
- def load_centrality_histogram(self):
16871
- """Load centrality - fraction of shortest paths passing through each node"""
16872
- try:
16873
- if self.G.number_of_nodes() > 1000:
16874
- print("Note this analysis may be slow for large network (>1000 nodes)")
16875
- #return
16876
-
16877
- load_centrality = nx.load_centrality(self.G)
16878
- plt.figure(figsize=(15, 8))
16879
- plt.hist(load_centrality.values(), bins=50, alpha=0.7)
16880
- plt.title("Load Centrality Distribution", fontdict={"size": 35}, loc="center")
16881
- plt.xlabel("Load Centrality", fontdict={"size": 20})
16882
- plt.ylabel("Frequency", fontdict={"size": 20})
16883
- plt.show()
16884
- self.stats_dict['Load Centrality'] = load_centrality
16885
- self.network_analysis.format_for_upperright_table(load_centrality, metric='Node',
16886
- value='Load Centrality', title="Load Centrality Table")
16887
- except Exception as e:
16888
- print(f"Error generating load centrality histogram: {e}")
16889
-
16890
- def communicability_centrality_histogram(self):
16891
- """Communicability centrality - based on communicability between nodes"""
16892
- try:
16893
- if self.G.number_of_nodes() > 500:
16894
- print("Note this analysis may be slow for large network (>500 nodes)")
16895
- #return
16896
-
16897
- # Check if graph has multiple disconnected components
16898
- components = list(nx.connected_components(self.G))
16899
-
16900
- if len(components) > 1:
16901
- print(f"Warning: Graph has {len(components)} disconnected components. Computing communicability centrality within each component separately.")
16902
-
16903
- # Initialize dictionary to collect communicability centrality from all components
16904
- combined_comm_centrality = {}
16905
-
16906
- # Process each component separately
16907
- for i, component in enumerate(components):
16908
- if len(component) < 2:
16909
- # For single-node components, communicability betweenness centrality is 0
16910
- for node in component:
16911
- combined_comm_centrality[node] = 0.0
16912
- continue
16913
-
16914
- # Create subgraph for this component
16915
- subgraph = self.G.subgraph(component)
16916
-
16917
- # Compute communicability betweenness centrality for this component
16918
- try:
16919
- component_comm_centrality = nx.communicability_betweenness_centrality(subgraph)
16920
- # Add to combined results
16921
- combined_comm_centrality.update(component_comm_centrality)
16922
- except Exception as comp_e:
16923
- print(f"Error computing communicability centrality for component {i+1}: {comp_e}")
16924
- # Set centrality to 0 for nodes in this component if computation fails
16925
- for node in component:
16926
- combined_comm_centrality[node] = 0.0
16927
-
16928
- comm_centrality = combined_comm_centrality
16929
- title_suffix = f" (across {len(components)} components)"
16930
-
16931
- else:
16932
- # Single component
16933
- comm_centrality = nx.communicability_betweenness_centrality(self.G)
16934
- title_suffix = ""
16935
-
16936
- # Generate visualization and results (same for both cases)
16937
- plt.figure(figsize=(15, 8))
16938
- plt.hist(comm_centrality.values(), bins=50, alpha=0.7)
16939
- plt.title(
16940
- f"Communicability Betweenness Centrality Distribution{title_suffix}",
16941
- fontdict={"size": 35}, loc="center"
16942
- )
16943
- plt.xlabel("Communicability Betweenness Centrality", fontdict={"size": 20})
16944
- plt.ylabel("Frequency", fontdict={"size": 20})
16945
- self.stats_dict['Communicability Betweenness Centrality'] = comm_centrality
16946
- plt.show()
16947
-
16948
- self.network_analysis.format_for_upperright_table(
16949
- comm_centrality,
16950
- metric='Node',
16951
- value='Communicability Betweenness Centrality',
16952
- title=f"Communicability Betweenness Centrality Table{title_suffix}"
16953
- )
16954
-
16955
- except Exception as e:
16956
- print(f"Error generating communicability betweenness centrality histogram: {e}")
16957
-
16958
- def harmonic_centrality_histogram(self):
16959
- """Harmonic centrality - better than closeness for disconnected networks"""
16960
- try:
16961
- harmonic_centrality = nx.harmonic_centrality(self.G)
16962
- plt.figure(figsize=(15, 8))
16963
- plt.hist(harmonic_centrality.values(), bins=50, alpha=0.7)
16964
- plt.title("Harmonic Centrality Distribution", fontdict={"size": 35}, loc="center")
16965
- plt.xlabel("Harmonic Centrality", fontdict={"size": 20})
16966
- plt.ylabel("Frequency", fontdict={"size": 20})
16967
- plt.show()
16968
- self.stats_dict['Harmonic Centrality Distribution'] = harmonic_centrality
16969
- self.network_analysis.format_for_upperright_table(harmonic_centrality, metric='Node',
16970
- value='Harmonic Centrality',
16971
- title="Harmonic Centrality Table")
16972
- except Exception as e:
16973
- print(f"Error generating harmonic centrality histogram: {e}")
16974
-
16975
- def current_flow_betweenness_histogram(self):
16976
- """Current flow betweenness - models network as electrical circuit"""
16977
- try:
16978
- if self.G.number_of_nodes() > 500:
16979
- print("Note this analysis may be slow for large network (>500 nodes)")
16980
- #return
16981
-
16982
- # Check if graph has multiple disconnected components
16983
- components = list(nx.connected_components(self.G))
16984
-
16985
- if len(components) > 1:
16986
- print(f"Warning: Graph has {len(components)} disconnected components. Computing current flow betweenness centrality within each component separately.")
16987
-
16988
- # Initialize dictionary to collect current flow betweenness from all components
16989
- combined_current_flow = {}
16990
-
16991
- # Process each component separately
16992
- for i, component in enumerate(components):
16993
- if len(component) < 2:
16994
- # For single-node components, current flow betweenness centrality is 0
16995
- for node in component:
16996
- combined_current_flow[node] = 0.0
16997
- continue
16998
-
16999
- # Create subgraph for this component
17000
- subgraph = self.G.subgraph(component)
17001
-
17002
- # Compute current flow betweenness centrality for this component
17003
- try:
17004
- component_current_flow = nx.current_flow_betweenness_centrality(subgraph)
17005
- # Add to combined results
17006
- combined_current_flow.update(component_current_flow)
17007
- except Exception as comp_e:
17008
- print(f"Error computing current flow betweenness for component {i+1}: {comp_e}")
17009
- # Set centrality to 0 for nodes in this component if computation fails
17010
- for node in component:
17011
- combined_current_flow[node] = 0.0
17012
-
17013
- current_flow = combined_current_flow
17014
- title_suffix = f" (across {len(components)} components)"
17015
-
17016
- else:
17017
- # Single component
17018
- current_flow = nx.current_flow_betweenness_centrality(self.G)
17019
- title_suffix = ""
17020
-
17021
- # Generate visualization and results (same for both cases)
17022
- plt.figure(figsize=(15, 8))
17023
- plt.hist(current_flow.values(), bins=50, alpha=0.7)
17024
- plt.title(
17025
- f"Current Flow Betweenness Centrality Distribution{title_suffix}",
17026
- fontdict={"size": 35}, loc="center"
17027
- )
17028
- plt.xlabel("Current Flow Betweenness Centrality", fontdict={"size": 20})
17029
- plt.ylabel("Frequency", fontdict={"size": 20})
17030
- plt.show()
17031
- self.stats_dict['Current Flow Betweenness Centrality'] = current_flow
17032
- self.network_analysis.format_for_upperright_table(
17033
- current_flow,
17034
- metric='Node',
17035
- value='Current Flow Betweenness',
17036
- title=f"Current Flow Betweenness Table{title_suffix}"
17037
- )
17038
-
17039
- except Exception as e:
17040
- print(f"Error generating current flow betweenness histogram: {e}")
17041
-
17042
- def dispersion_histogram(self):
17043
- """Dispersion - measures how scattered a node's neighbors are"""
17044
- try:
17045
- if self.G.number_of_nodes() > 300: # Skip for large networks (very computationally expensive)
17046
- print("Note this analysis may be slow for large network (>300 nodes)")
17047
- #return
17048
-
17049
- # Calculate average dispersion for each node
17050
- dispersion_values = {}
17051
- nodes = list(self.G.nodes())
17052
-
17053
- for u in nodes:
17054
- if self.G.degree(u) < 2: # Need at least 2 neighbors for dispersion
17055
- dispersion_values[u] = 0
17056
- continue
17057
-
17058
- # Calculate dispersion for node u with all its neighbors
17059
- neighbors = list(self.G.neighbors(u))
17060
- if len(neighbors) < 2:
17061
- dispersion_values[u] = 0
17062
- continue
17063
-
17064
- # Get dispersion scores for this node with all neighbors
17065
- disp_scores = []
17066
- for v in neighbors:
17067
- try:
17068
- disp_score = nx.dispersion(self.G, u, v)
17069
- disp_scores.append(disp_score)
17070
- except:
17071
- continue
17072
-
17073
- # Average dispersion for this node
17074
- dispersion_values[u] = sum(disp_scores) / len(disp_scores) if disp_scores else 0
17075
-
17076
- plt.figure(figsize=(15, 8))
17077
- plt.hist(dispersion_values.values(), bins=30, alpha=0.7)
17078
- plt.title("Average Dispersion Distribution", fontdict={"size": 35}, loc="center")
17079
- plt.xlabel("Average Dispersion", fontdict={"size": 20})
17080
- plt.ylabel("Frequency", fontdict={"size": 20})
17081
- plt.show()
17082
- self.stats_dict['Dispersion'] = dispersion_values
17083
- self.network_analysis.format_for_upperright_table(dispersion_values, metric='Node',
17084
- value='Average Dispersion',
17085
- title="Average Dispersion Table")
17086
- except Exception as e:
17087
- print(f"Error generating dispersion histogram: {e}")
17088
-
17089
16645
  class TutorialSelectionDialog(QWidget):
17090
16646
  """Dialog for selecting which tutorial to run"""
17091
16647