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.
- nettracer3d/community_extractor.py +3 -2
- nettracer3d/endpoint_joiner.py +286 -0
- nettracer3d/filaments.py +348 -106
- nettracer3d/histos.py +1182 -0
- nettracer3d/modularity.py +14 -96
- nettracer3d/neighborhoods.py +3 -2
- nettracer3d/nettracer.py +91 -50
- nettracer3d/nettracer_gui.py +359 -803
- nettracer3d/network_analysis.py +12 -5
- nettracer3d/network_graph_widget.py +302 -101
- nettracer3d/segmenter.py +1 -1
- nettracer3d/segmenter_GPU.py +0 -1
- nettracer3d/tutorial.py +41 -25
- {nettracer3d-1.3.1.dist-info → nettracer3d-1.3.6.dist-info}/METADATA +4 -6
- nettracer3d-1.3.6.dist-info/RECORD +32 -0
- {nettracer3d-1.3.1.dist-info → nettracer3d-1.3.6.dist-info}/WHEEL +1 -1
- nettracer3d-1.3.1.dist-info/RECORD +0 -30
- {nettracer3d-1.3.1.dist-info → nettracer3d-1.3.6.dist-info}/entry_points.txt +0 -0
- {nettracer3d-1.3.1.dist-info → nettracer3d-1.3.6.dist-info}/licenses/LICENSE +0 -0
- {nettracer3d-1.3.1.dist-info → nettracer3d-1.3.6.dist-info}/top_level.txt +0 -0
nettracer3d/nettracer_gui.py
CHANGED
|
@@ -239,14 +239,14 @@ class ImageViewerWindow(QMainWindow):
|
|
|
239
239
|
|
|
240
240
|
|
|
241
241
|
self.toggle_scale = QPushButton("📏")
|
|
242
|
-
self.toggle_scale.setFixedSize(
|
|
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(
|
|
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=
|
|
562
|
+
weight=True,
|
|
563
563
|
geometric=False,
|
|
564
564
|
centroids=None,
|
|
565
|
-
communities=
|
|
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=
|
|
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
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
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[
|
|
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.
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
6215
|
-
|
|
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
|
-
#
|
|
6499
|
-
|
|
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
|
|
6559
|
+
"Select Location and Name for Network3D Output Folder",
|
|
6502
6560
|
"",
|
|
6503
|
-
|
|
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
|
-
|
|
6516
|
-
|
|
6517
|
-
|
|
6518
|
-
|
|
6519
|
-
|
|
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
|
-
|
|
6525
|
-
|
|
6526
|
-
|
|
6527
|
-
|
|
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
|
-
|
|
6539
|
-
|
|
6540
|
-
|
|
6541
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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 =
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
9291
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9353
|
-
|
|
9354
|
-
|
|
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:
|
|
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)
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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 (
|
|
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("
|
|
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.
|
|
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
|
|
13943
|
-
|
|
13944
|
-
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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.
|
|
16112
|
-
old_lists = network_analysis.
|
|
16113
|
-
|
|
16114
|
-
|
|
16115
|
-
|
|
16116
|
-
|
|
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
|
-
|
|
16294
|
+
output_lists = [list(list1), list(list2), list(list3)]
|
|
16120
16295
|
|
|
16121
|
-
my_network.network_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
|
|