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
|
@@ -8,6 +8,8 @@ from PyQt6.QtGui import QColor, QPen, QBrush
|
|
|
8
8
|
import pyqtgraph as pg
|
|
9
9
|
from pyqtgraph import ScatterPlotItem, PlotCurveItem, GraphicsLayoutWidget, ROI
|
|
10
10
|
import colorsys
|
|
11
|
+
import random
|
|
12
|
+
import copy
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
class GraphLoadThread(QThread):
|
|
@@ -15,7 +17,8 @@ class GraphLoadThread(QThread):
|
|
|
15
17
|
finished = pyqtSignal(object) # Emits the computed layout data
|
|
16
18
|
|
|
17
19
|
def __init__(self, graph, geometric, component, centroids, communities,
|
|
18
|
-
community_dict, identities, identity_dict, weight, z_size
|
|
20
|
+
community_dict, identities, identity_dict, weight, z_size,
|
|
21
|
+
shell, node_size, edge_size):
|
|
19
22
|
super().__init__()
|
|
20
23
|
self.graph = graph
|
|
21
24
|
self.geometric = geometric
|
|
@@ -27,6 +30,9 @@ class GraphLoadThread(QThread):
|
|
|
27
30
|
self.identity_dict = identity_dict
|
|
28
31
|
self.weight = weight
|
|
29
32
|
self.z_size = z_size
|
|
33
|
+
self.shell = shell
|
|
34
|
+
self.node_size = node_size
|
|
35
|
+
self.edge_size = edge_size
|
|
30
36
|
|
|
31
37
|
def run(self):
|
|
32
38
|
"""Compute layout and colors in background thread"""
|
|
@@ -34,11 +40,7 @@ class GraphLoadThread(QThread):
|
|
|
34
40
|
|
|
35
41
|
# Compute node positions
|
|
36
42
|
if not self.geometric and not self.component:
|
|
37
|
-
|
|
38
|
-
if n < 500:
|
|
39
|
-
result['pos'] = nx.spring_layout(self.graph, seed=42, iterations=50)
|
|
40
|
-
else:
|
|
41
|
-
result['pos'] = self._compute_fast_spring_layout()
|
|
43
|
+
result['pos'] = self._compute_fast_spring_layout()
|
|
42
44
|
elif self.geometric:
|
|
43
45
|
result['pos'] = self._compute_geometric_layout()
|
|
44
46
|
elif self.component:
|
|
@@ -72,14 +74,209 @@ class GraphLoadThread(QThread):
|
|
|
72
74
|
return {}
|
|
73
75
|
|
|
74
76
|
# For small graphs, use networkx (overhead is negligible)
|
|
75
|
-
if n <
|
|
77
|
+
if n < 500 and not self.shell:
|
|
76
78
|
return nx.spring_layout(self.graph, seed=42, iterations=50)
|
|
77
|
-
|
|
79
|
+
|
|
78
80
|
# Use fast vectorized implementation for larger graphs
|
|
79
81
|
try:
|
|
80
|
-
|
|
82
|
+
if not self.shell:
|
|
83
|
+
return self._spring_layout_numpy(nodes, n)
|
|
84
|
+
else:
|
|
85
|
+
return self._shell_layout_numpy_super(nodes, n)
|
|
81
86
|
except Exception as e:
|
|
82
|
-
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
def _shell_layout_numpy_super(self, nodes, n):
|
|
90
|
+
"""
|
|
91
|
+
Shell layout with physically separated connected components
|
|
92
|
+
"""
|
|
93
|
+
np.random.seed(42)
|
|
94
|
+
|
|
95
|
+
# Find connected components
|
|
96
|
+
components = list(nx.connected_components(self.graph))
|
|
97
|
+
|
|
98
|
+
if len(components) == 1:
|
|
99
|
+
# Single component - compute shell layout directly with numpy
|
|
100
|
+
comp_nodes = nodes
|
|
101
|
+
|
|
102
|
+
if n == 1:
|
|
103
|
+
return {nodes[0]: np.array([0.0, 0.0])}
|
|
104
|
+
|
|
105
|
+
# Create node to index mapping
|
|
106
|
+
node_to_idx = {node: i for i, node in enumerate(nodes)}
|
|
107
|
+
|
|
108
|
+
# Compute degree centrality using numpy
|
|
109
|
+
degrees = np.zeros(n)
|
|
110
|
+
for u, v in self.graph.edges():
|
|
111
|
+
if u in node_to_idx and v in node_to_idx:
|
|
112
|
+
degrees[node_to_idx[u]] += 1
|
|
113
|
+
degrees[node_to_idx[v]] += 1
|
|
114
|
+
|
|
115
|
+
# Find most central node (highest degree)
|
|
116
|
+
central_idx = np.argmax(degrees)
|
|
117
|
+
central_node = nodes[central_idx]
|
|
118
|
+
|
|
119
|
+
# Build adjacency list for BFS
|
|
120
|
+
adj_list = {node: [] for node in nodes}
|
|
121
|
+
for u, v in self.graph.edges():
|
|
122
|
+
if u in node_to_idx and v in node_to_idx:
|
|
123
|
+
adj_list[u].append(v)
|
|
124
|
+
adj_list[v].append(u)
|
|
125
|
+
|
|
126
|
+
# Compute shells using BFS from central node
|
|
127
|
+
visited = set()
|
|
128
|
+
shells = []
|
|
129
|
+
current_shell = [central_node]
|
|
130
|
+
visited.add(central_node)
|
|
131
|
+
|
|
132
|
+
while current_shell:
|
|
133
|
+
shells.append(current_shell[:])
|
|
134
|
+
next_shell = []
|
|
135
|
+
for node in current_shell:
|
|
136
|
+
for neighbor in adj_list[node]:
|
|
137
|
+
if neighbor not in visited:
|
|
138
|
+
visited.add(neighbor)
|
|
139
|
+
next_shell.append(neighbor)
|
|
140
|
+
current_shell = next_shell
|
|
141
|
+
|
|
142
|
+
# Position nodes in concentric circles
|
|
143
|
+
pos = {}
|
|
144
|
+
radius = 1.0
|
|
145
|
+
|
|
146
|
+
for shell_idx, shell in enumerate(shells):
|
|
147
|
+
if shell_idx == 0:
|
|
148
|
+
# Center node at origin
|
|
149
|
+
pos[shell[0]] = np.array([0.0, 0.0])
|
|
150
|
+
else:
|
|
151
|
+
# Arrange nodes in circle at radius * shell_idx
|
|
152
|
+
num_nodes = len(shell)
|
|
153
|
+
angles = np.linspace(0, 2 * np.pi, num_nodes, endpoint=False)
|
|
154
|
+
for i, node in enumerate(shell):
|
|
155
|
+
x = radius * shell_idx * np.cos(angles[i])
|
|
156
|
+
y = radius * shell_idx * np.sin(angles[i])
|
|
157
|
+
pos[node] = np.array([x, y])
|
|
158
|
+
|
|
159
|
+
# Center the layout
|
|
160
|
+
positions = np.array(list(pos.values()))
|
|
161
|
+
positions -= positions.mean(axis=0)
|
|
162
|
+
|
|
163
|
+
return {node: positions[list(pos.keys()).index(node)] for node in nodes}
|
|
164
|
+
|
|
165
|
+
# Multiple components - layout each component independently
|
|
166
|
+
component_layouts = []
|
|
167
|
+
component_bounds = []
|
|
168
|
+
|
|
169
|
+
for component in components:
|
|
170
|
+
comp_nodes = list(component)
|
|
171
|
+
comp_n = len(comp_nodes)
|
|
172
|
+
|
|
173
|
+
# Layout this component
|
|
174
|
+
comp_pos = self._layout_component_shell(comp_nodes, comp_n)
|
|
175
|
+
|
|
176
|
+
# Calculate bounding box
|
|
177
|
+
positions = np.array(list(comp_pos.values()))
|
|
178
|
+
min_coords = positions.min(axis=0)
|
|
179
|
+
max_coords = positions.max(axis=0)
|
|
180
|
+
size = max_coords - min_coords
|
|
181
|
+
|
|
182
|
+
component_layouts.append((comp_nodes, comp_pos))
|
|
183
|
+
component_bounds.append(size)
|
|
184
|
+
|
|
185
|
+
# Arrange components in a grid with spacing
|
|
186
|
+
num_components = len(components)
|
|
187
|
+
grid_cols = int(np.ceil(np.sqrt(num_components)))
|
|
188
|
+
|
|
189
|
+
# Calculate spacing based on largest component
|
|
190
|
+
max_width = max(bounds[0] for bounds in component_bounds)
|
|
191
|
+
max_height = max(bounds[1] for bounds in component_bounds)
|
|
192
|
+
spacing_x = max_width * 1.5 # 50% padding between components
|
|
193
|
+
spacing_y = max_height * 1.5
|
|
194
|
+
|
|
195
|
+
# Place components in grid
|
|
196
|
+
final_positions = {}
|
|
197
|
+
for idx, (comp_nodes, comp_pos) in enumerate(component_layouts):
|
|
198
|
+
grid_x = idx % grid_cols
|
|
199
|
+
grid_y = idx // grid_cols
|
|
200
|
+
|
|
201
|
+
# Calculate offset for this component
|
|
202
|
+
offset = np.array([grid_x * spacing_x, grid_y * spacing_y])
|
|
203
|
+
|
|
204
|
+
# Apply offset to all nodes in component
|
|
205
|
+
for node in comp_nodes:
|
|
206
|
+
final_positions[node] = comp_pos[node] + offset
|
|
207
|
+
|
|
208
|
+
# Center the entire layout
|
|
209
|
+
all_pos = np.array([final_positions[node] for node in nodes])
|
|
210
|
+
all_pos -= all_pos.mean(axis=0)
|
|
211
|
+
|
|
212
|
+
return {node: all_pos[i] for i, node in enumerate(nodes)}
|
|
213
|
+
|
|
214
|
+
def _layout_component_shell(self, nodes, n):
|
|
215
|
+
"""
|
|
216
|
+
Shell layout for a single component using numpy for centrality
|
|
217
|
+
"""
|
|
218
|
+
if n == 1:
|
|
219
|
+
return {nodes[0]: np.array([0.0, 0.0])}
|
|
220
|
+
|
|
221
|
+
# Create node to index mapping
|
|
222
|
+
node_to_idx = {node: i for i, node in enumerate(nodes)}
|
|
223
|
+
|
|
224
|
+
# Compute degree centrality using numpy
|
|
225
|
+
degrees = np.zeros(n)
|
|
226
|
+
for u, v in self.graph.edges():
|
|
227
|
+
if u in node_to_idx and v in node_to_idx:
|
|
228
|
+
degrees[node_to_idx[u]] += 1
|
|
229
|
+
degrees[node_to_idx[v]] += 1
|
|
230
|
+
|
|
231
|
+
# Find most central node (highest degree)
|
|
232
|
+
central_idx = np.argmax(degrees)
|
|
233
|
+
central_node = nodes[central_idx]
|
|
234
|
+
|
|
235
|
+
# Build adjacency list for BFS
|
|
236
|
+
adj_list = {node: [] for node in nodes}
|
|
237
|
+
for u, v in self.graph.edges():
|
|
238
|
+
if u in node_to_idx and v in node_to_idx:
|
|
239
|
+
adj_list[u].append(v)
|
|
240
|
+
adj_list[v].append(u)
|
|
241
|
+
|
|
242
|
+
# Compute shells using BFS from central node
|
|
243
|
+
visited = set()
|
|
244
|
+
shells = []
|
|
245
|
+
current_shell = [central_node]
|
|
246
|
+
visited.add(central_node)
|
|
247
|
+
|
|
248
|
+
while current_shell:
|
|
249
|
+
shells.append(current_shell[:])
|
|
250
|
+
next_shell = []
|
|
251
|
+
for node in current_shell:
|
|
252
|
+
for neighbor in adj_list[node]:
|
|
253
|
+
if neighbor not in visited:
|
|
254
|
+
visited.add(neighbor)
|
|
255
|
+
next_shell.append(neighbor)
|
|
256
|
+
current_shell = next_shell
|
|
257
|
+
|
|
258
|
+
# Position nodes in concentric circles
|
|
259
|
+
pos = {}
|
|
260
|
+
radius = 1.0
|
|
261
|
+
|
|
262
|
+
for shell_idx, shell in enumerate(shells):
|
|
263
|
+
if shell_idx == 0:
|
|
264
|
+
# Center node at origin
|
|
265
|
+
pos[shell[0]] = np.array([0.0, 0.0])
|
|
266
|
+
else:
|
|
267
|
+
# Arrange nodes in circle at radius * shell_idx
|
|
268
|
+
num_nodes = len(shell)
|
|
269
|
+
angles = np.linspace(0, 2 * np.pi, num_nodes, endpoint=False)
|
|
270
|
+
for i, node in enumerate(shell):
|
|
271
|
+
x = radius * shell_idx * np.cos(angles[i])
|
|
272
|
+
y = radius * shell_idx * np.sin(angles[i])
|
|
273
|
+
pos[node] = np.array([x, y])
|
|
274
|
+
|
|
275
|
+
# Center the layout
|
|
276
|
+
positions = np.array(list(pos.values()))
|
|
277
|
+
positions -= positions.mean(axis=0)
|
|
278
|
+
|
|
279
|
+
return {node: positions[list(pos.keys()).index(node)] for node in nodes}
|
|
83
280
|
|
|
84
281
|
def _spring_layout_numpy_super(self, nodes, n, iterations=50):
|
|
85
282
|
"""
|
|
@@ -331,7 +528,7 @@ class GraphLoadThread(QThread):
|
|
|
331
528
|
return [{
|
|
332
529
|
'x': np.array(x_coords),
|
|
333
530
|
'y': np.array(y_coords),
|
|
334
|
-
'thickness':
|
|
531
|
+
'thickness': self.edge_size
|
|
335
532
|
}]
|
|
336
533
|
|
|
337
534
|
# Weight-based rendering - batch by thickness
|
|
@@ -345,8 +542,8 @@ class GraphLoadThread(QThread):
|
|
|
345
542
|
|
|
346
543
|
# Define thickness bins (e.g., 10 discrete thickness levels)
|
|
347
544
|
num_bins = 10
|
|
348
|
-
thickness_min =
|
|
349
|
-
thickness_max = 3.0 # Maximum thickness cap
|
|
545
|
+
thickness_min = self.edge_size/2
|
|
546
|
+
thickness_max = 3.0 * self.edge_size # Maximum thickness cap
|
|
350
547
|
|
|
351
548
|
# Batch edges by thickness bin
|
|
352
549
|
edge_batches = {} # {thickness: [(x_coords, y_coords), ...]}
|
|
@@ -356,7 +553,7 @@ class GraphLoadThread(QThread):
|
|
|
356
553
|
if weight_range > 0:
|
|
357
554
|
normalized = (weight - min_weight) / weight_range
|
|
358
555
|
else:
|
|
359
|
-
normalized =
|
|
556
|
+
normalized = self.edge_size/2
|
|
360
557
|
|
|
361
558
|
# Calculate thickness with cap
|
|
362
559
|
thickness = thickness_min + normalized * (thickness_max - thickness_min)
|
|
@@ -421,7 +618,7 @@ class GraphLoadThread(QThread):
|
|
|
421
618
|
|
|
422
619
|
def _compute_all_node_sizes_vectorized(self, nodes):
|
|
423
620
|
if not self.geometric or not self.centroids or not self.z_size:
|
|
424
|
-
return [
|
|
621
|
+
return [self.node_size] * len(nodes)
|
|
425
622
|
|
|
426
623
|
# GLOBAL z range (matches original behavior)
|
|
427
624
|
all_z = np.array([
|
|
@@ -495,9 +692,7 @@ class GraphLoadThread(QThread):
|
|
|
495
692
|
|
|
496
693
|
unique_communities = sorted(set(my_dict.values()))
|
|
497
694
|
community_sizes = Counter(my_dict.values())
|
|
498
|
-
sorted_communities =
|
|
499
|
-
key=lambda x: community_sizes[x],
|
|
500
|
-
reverse=True)
|
|
695
|
+
sorted_communities = random.Random(42).sample(unique_communities, len(unique_communities))
|
|
501
696
|
colors_rgb = self._generate_distinct_colors_rgb(len(unique_communities))
|
|
502
697
|
color_map = {comm: colors_rgb[i] for i, comm in enumerate(sorted_communities)}
|
|
503
698
|
if 0 in unique_communities:
|
|
@@ -539,7 +734,8 @@ class NetworkGraphWidget(QWidget):
|
|
|
539
734
|
|
|
540
735
|
def __init__(self, parent=None, weight=False, geometric=False, component = False,
|
|
541
736
|
centroids=None, communities=False, community_dict=None,
|
|
542
|
-
identities=False, identity_dict=None, labels=False, z_size = False
|
|
737
|
+
identities=False, identity_dict=None, labels=False, z_size = False,
|
|
738
|
+
shell = False, node_size = 10, black_edges = False, edge_size = 1, popout = False):
|
|
543
739
|
super().__init__(parent)
|
|
544
740
|
|
|
545
741
|
self.parent_window = parent
|
|
@@ -553,6 +749,11 @@ class NetworkGraphWidget(QWidget):
|
|
|
553
749
|
self.identity_dict = identity_dict or {}
|
|
554
750
|
self.labels = labels
|
|
555
751
|
self.z_size = z_size
|
|
752
|
+
self.shell = shell
|
|
753
|
+
self.node_size = node_size
|
|
754
|
+
self.black_edges = black_edges
|
|
755
|
+
self.edge_size = edge_size
|
|
756
|
+
self.popout = popout
|
|
556
757
|
|
|
557
758
|
# Graph data
|
|
558
759
|
self.graph = None
|
|
@@ -564,6 +765,7 @@ class NetworkGraphWidget(QWidget):
|
|
|
564
765
|
self.label_items = {}
|
|
565
766
|
self.label_data = [] # Store label data for on-demand rendering
|
|
566
767
|
self.selected_nodes = set()
|
|
768
|
+
self.node_click = False
|
|
567
769
|
self.rendered = False
|
|
568
770
|
|
|
569
771
|
# CACHING for fast updates
|
|
@@ -629,7 +831,6 @@ class NetworkGraphWidget(QWidget):
|
|
|
629
831
|
self.plot.addItem(self.loading_text)
|
|
630
832
|
|
|
631
833
|
# Enable mouse tracking for area selection
|
|
632
|
-
self.plot.scene().sigMouseClicked.connect(self._on_plot_clicked)
|
|
633
834
|
self.plot.scene().sigMouseMoved.connect(self._on_mouse_moved)
|
|
634
835
|
|
|
635
836
|
# Disable default mouse interaction - will enable only in pan mode
|
|
@@ -644,6 +845,7 @@ class NetworkGraphWidget(QWidget):
|
|
|
644
845
|
|
|
645
846
|
# Connect click events
|
|
646
847
|
self.scatter.sigClicked.connect(self._on_node_clicked)
|
|
848
|
+
self.plot.scene().sigMouseClicked.connect(self._on_plot_clicked)
|
|
647
849
|
|
|
648
850
|
# Connect view change for level-of-detail updates
|
|
649
851
|
self.plot.sigRangeChanged.connect(self._on_view_changed)
|
|
@@ -702,10 +904,7 @@ class NetworkGraphWidget(QWidget):
|
|
|
702
904
|
|
|
703
905
|
unique_identities = sorted(set(self.identity_dict.values()))
|
|
704
906
|
community_sizes = Counter(self.identity_dict.values())
|
|
705
|
-
sorted_communities =
|
|
706
|
-
key=lambda x: community_sizes[x],
|
|
707
|
-
reverse=True)
|
|
708
|
-
|
|
907
|
+
sorted_communities = random.Random(42).sample(unique_identities, len(unique_identities))
|
|
709
908
|
colors_rgb = _generate_distinct_colors_rgb(len(unique_identities))
|
|
710
909
|
color_map = {comm: colors_rgb[i] for i, comm in enumerate(sorted_communities)}
|
|
711
910
|
if 0 in unique_identities:
|
|
@@ -715,10 +914,7 @@ class NetworkGraphWidget(QWidget):
|
|
|
715
914
|
|
|
716
915
|
unique_identities = sorted(set(self.community_dict.values()))
|
|
717
916
|
community_sizes = Counter(self.community_dict.values())
|
|
718
|
-
sorted_communities =
|
|
719
|
-
key=lambda x: community_sizes[x],
|
|
720
|
-
reverse=True)
|
|
721
|
-
|
|
917
|
+
sorted_communities = random.Random(42).sample(unique_identities, len(unique_identities))
|
|
722
918
|
colors_rgb = _generate_distinct_colors_rgb(len(unique_identities))
|
|
723
919
|
color_map = {comm: colors_rgb[i] for i, comm in enumerate(sorted_communities)}
|
|
724
920
|
if 0 in unique_identities:
|
|
@@ -852,6 +1048,7 @@ class NetworkGraphWidget(QWidget):
|
|
|
852
1048
|
self.clear_btn.setToolTip("Clear Graph")
|
|
853
1049
|
self.clear_btn.setMaximumSize(32, 32)
|
|
854
1050
|
self.clear_btn.clicked.connect(self._clear_graph)
|
|
1051
|
+
|
|
855
1052
|
|
|
856
1053
|
# Add buttons to layout
|
|
857
1054
|
panel_layout.addWidget(self.select_btn)
|
|
@@ -861,6 +1058,14 @@ class NetworkGraphWidget(QWidget):
|
|
|
861
1058
|
panel_layout.addWidget(self.refresh_btn)
|
|
862
1059
|
panel_layout.addWidget(self.settings_btn)
|
|
863
1060
|
panel_layout.addWidget(self.clear_btn)
|
|
1061
|
+
|
|
1062
|
+
if self.popout:
|
|
1063
|
+
self.popout_btn = QPushButton("⤴")
|
|
1064
|
+
self.popout_btn.setToolTip("Full Screen")
|
|
1065
|
+
self.popout_btn.setMaximumSize(32, 32)
|
|
1066
|
+
self.popout_btn.clicked.connect(self._popout_graph)
|
|
1067
|
+
panel_layout.addWidget(self.popout_btn)
|
|
1068
|
+
|
|
864
1069
|
panel_layout.addStretch()
|
|
865
1070
|
|
|
866
1071
|
panel.setLayout(panel_layout)
|
|
@@ -906,6 +1111,7 @@ class NetworkGraphWidget(QWidget):
|
|
|
906
1111
|
|
|
907
1112
|
# Clear existing visualization
|
|
908
1113
|
self._clear_graph()
|
|
1114
|
+
self.get_properties()
|
|
909
1115
|
|
|
910
1116
|
if hasattr(self, 'loading_text') and self.loading_text is not None:
|
|
911
1117
|
self.plot.removeItem(self.loading_text)
|
|
@@ -939,7 +1145,8 @@ class NetworkGraphWidget(QWidget):
|
|
|
939
1145
|
self.load_thread = GraphLoadThread(
|
|
940
1146
|
self.graph, self.geometric, self.component, self.centroids,
|
|
941
1147
|
self.communities, self.community_dict,
|
|
942
|
-
self.identities, self.identity_dict, self.weight, self.z_size
|
|
1148
|
+
self.identities, self.identity_dict, self.weight, self.z_size,
|
|
1149
|
+
self.shell, self.node_size, self.edge_size
|
|
943
1150
|
)
|
|
944
1151
|
self.load_thread.finished.connect(self._on_graph_loaded)
|
|
945
1152
|
self.load_thread.start()
|
|
@@ -992,6 +1199,7 @@ class NetworkGraphWidget(QWidget):
|
|
|
992
1199
|
self.select_nodes(self.parent_window.clicked_values['nodes'])
|
|
993
1200
|
|
|
994
1201
|
|
|
1202
|
+
|
|
995
1203
|
def _render_prepared_data(self, result):
|
|
996
1204
|
"""Render pre-computed data (minimal main thread work)"""
|
|
997
1205
|
# Clear old items
|
|
@@ -1002,6 +1210,11 @@ class NetworkGraphWidget(QWidget):
|
|
|
1002
1210
|
for label_item in self.label_items.values():
|
|
1003
1211
|
self.plot.removeItem(label_item)
|
|
1004
1212
|
self.label_items.clear()
|
|
1213
|
+
|
|
1214
|
+
if self.black_edges:
|
|
1215
|
+
edge_color = (0, 0, 0)
|
|
1216
|
+
else:
|
|
1217
|
+
edge_color = (150, 150, 150, 100)
|
|
1005
1218
|
|
|
1006
1219
|
# Render edges - batched by weight for efficiency
|
|
1007
1220
|
edge_batches = result['edge_pens']
|
|
@@ -1010,7 +1223,7 @@ class NetworkGraphWidget(QWidget):
|
|
|
1010
1223
|
edge_line = PlotCurveItem(
|
|
1011
1224
|
x=batch['x'],
|
|
1012
1225
|
y=batch['y'],
|
|
1013
|
-
pen=pg.mkPen(color=
|
|
1226
|
+
pen=pg.mkPen(color=edge_color, width=batch['thickness']),
|
|
1014
1227
|
connect='finite' # Break lines at NaN
|
|
1015
1228
|
)
|
|
1016
1229
|
self.plot.addItem(edge_line)
|
|
@@ -1176,62 +1389,6 @@ class NetworkGraphWidget(QWidget):
|
|
|
1176
1389
|
if self.label_data:
|
|
1177
1390
|
self._update_labels_for_zoom()
|
|
1178
1391
|
|
|
1179
|
-
def _render_edges(self, edges):
|
|
1180
|
-
"""Render edges with optional weight-based thickness (OPTIMIZED: single combined line)"""
|
|
1181
|
-
# Remove old edges
|
|
1182
|
-
for item in self.edge_items:
|
|
1183
|
-
self.plot.removeItem(item)
|
|
1184
|
-
self.edge_items.clear()
|
|
1185
|
-
|
|
1186
|
-
if not edges:
|
|
1187
|
-
return
|
|
1188
|
-
|
|
1189
|
-
# Get weight range for normalization if using weights
|
|
1190
|
-
if self.weight:
|
|
1191
|
-
weights = [w for _, _, w in edges]
|
|
1192
|
-
if len(weights) > 0:
|
|
1193
|
-
min_weight = min(weights)
|
|
1194
|
-
max_weight = max(weights)
|
|
1195
|
-
weight_range = max_weight - min_weight if max_weight > min_weight else 1
|
|
1196
|
-
else:
|
|
1197
|
-
weight_range = 1
|
|
1198
|
-
min_weight = 0
|
|
1199
|
-
|
|
1200
|
-
# Combine all edges into single line with NaN separators
|
|
1201
|
-
x_combined = []
|
|
1202
|
-
y_combined = []
|
|
1203
|
-
thicknesses = []
|
|
1204
|
-
|
|
1205
|
-
for x, y, weight in edges:
|
|
1206
|
-
if self.weight and 'weight_range' in locals() and weight_range > 0:
|
|
1207
|
-
# Normalize weight to thickness range (0.5 - 3.0)
|
|
1208
|
-
normalized = (weight - min_weight) / weight_range
|
|
1209
|
-
thickness = 0.5 + normalized * 2.5
|
|
1210
|
-
else:
|
|
1211
|
-
thickness = 1.0
|
|
1212
|
-
|
|
1213
|
-
thicknesses.append(thickness)
|
|
1214
|
-
|
|
1215
|
-
# Add edge coordinates with NaN separator
|
|
1216
|
-
x_combined.extend([x[0], x[1], np.nan])
|
|
1217
|
-
y_combined.extend([y[0], y[1], np.nan])
|
|
1218
|
-
|
|
1219
|
-
# Use average thickness
|
|
1220
|
-
avg_thickness = np.mean(thicknesses) if thicknesses else 1.0
|
|
1221
|
-
|
|
1222
|
-
# Create single line with all edges
|
|
1223
|
-
edge_line = PlotCurveItem(
|
|
1224
|
-
x=np.array(x_combined),
|
|
1225
|
-
y=np.array(y_combined),
|
|
1226
|
-
pen=pg.mkPen(color=(150, 150, 150, 100), width=avg_thickness),
|
|
1227
|
-
connect='finite'
|
|
1228
|
-
)
|
|
1229
|
-
self.plot.addItem(edge_line)
|
|
1230
|
-
self.edge_items.append(edge_line)
|
|
1231
|
-
|
|
1232
|
-
# Ensure nodes are on top
|
|
1233
|
-
self.scatter.setZValue(10)
|
|
1234
|
-
|
|
1235
1392
|
def _hex_to_rgb(self, hex_color):
|
|
1236
1393
|
"""Convert hex color to RGB tuple"""
|
|
1237
1394
|
hex_color = hex_color.lstrip('#')
|
|
@@ -1239,7 +1396,8 @@ class NetworkGraphWidget(QWidget):
|
|
|
1239
1396
|
|
|
1240
1397
|
def _on_plot_clicked(self, ev):
|
|
1241
1398
|
"""Handle clicks on the plot background"""
|
|
1242
|
-
if not self.selection_mode:
|
|
1399
|
+
if not self.selection_mode or self.node_click or not self.popout:
|
|
1400
|
+
self.node_click = False
|
|
1243
1401
|
return
|
|
1244
1402
|
|
|
1245
1403
|
# Only handle left button clicks
|
|
@@ -1249,12 +1407,6 @@ class NetworkGraphWidget(QWidget):
|
|
|
1249
1407
|
# Get the position in scene coordinates
|
|
1250
1408
|
scene_pos = ev.scenePos()
|
|
1251
1409
|
|
|
1252
|
-
# Check if click was on a node (scatter will handle it)
|
|
1253
|
-
items = self.plot.scene().items(scene_pos)
|
|
1254
|
-
for item in items:
|
|
1255
|
-
if item == self.scatter:
|
|
1256
|
-
# Click was on scatter plot, let node handler deal with it
|
|
1257
|
-
return
|
|
1258
1410
|
|
|
1259
1411
|
# Click was on background
|
|
1260
1412
|
modifiers = ev.modifiers()
|
|
@@ -1264,8 +1416,8 @@ class NetworkGraphWidget(QWidget):
|
|
|
1264
1416
|
# Deselect all nodes
|
|
1265
1417
|
self.selected_nodes.clear()
|
|
1266
1418
|
self._render_nodes()
|
|
1419
|
+
self.push_selection()
|
|
1267
1420
|
self.node_selected.emit([])
|
|
1268
|
-
|
|
1269
1421
|
# Ctrl+Click on background does nothing (as requested)
|
|
1270
1422
|
|
|
1271
1423
|
def _on_mouse_moved(self, pos):
|
|
@@ -1284,6 +1436,7 @@ class NetworkGraphWidget(QWidget):
|
|
|
1284
1436
|
def _start_area_selection(self, scene_pos):
|
|
1285
1437
|
"""Start area selection with rectangle"""
|
|
1286
1438
|
self.is_area_selecting = True
|
|
1439
|
+
self.node_click = False
|
|
1287
1440
|
|
|
1288
1441
|
# Convert to view coordinates
|
|
1289
1442
|
view_pos = self.plot.vb.mapSceneToView(scene_pos)
|
|
@@ -1456,6 +1609,7 @@ class NetworkGraphWidget(QWidget):
|
|
|
1456
1609
|
if delta_x > 10 or delta_y > 10: # Moved significantly
|
|
1457
1610
|
self.click_timer.stop()
|
|
1458
1611
|
if not self.is_area_selecting:
|
|
1612
|
+
#if not self.was_in_selection_before_wheel or self.zoom_mode:
|
|
1459
1613
|
self._start_area_selection(self.last_mouse_pos)
|
|
1460
1614
|
|
|
1461
1615
|
|
|
@@ -1514,7 +1668,7 @@ class NetworkGraphWidget(QWidget):
|
|
|
1514
1668
|
"""End temporary panning mode"""
|
|
1515
1669
|
self.temp_pan_active = False
|
|
1516
1670
|
# Disable mouse if we're in selection mode
|
|
1517
|
-
if self.selection_mode:
|
|
1671
|
+
if self.selection_mode or self.zoom_mode:
|
|
1518
1672
|
self.plot.setMouseEnabled(x=False, y=False)
|
|
1519
1673
|
|
|
1520
1674
|
def _handle_wheel_in_selection(self, event):
|
|
@@ -1530,12 +1684,12 @@ class NetworkGraphWidget(QWidget):
|
|
|
1530
1684
|
self.wheel_timer.setSingleShot(True)
|
|
1531
1685
|
self.wheel_timer.timeout.connect(self._end_wheel_zoom)
|
|
1532
1686
|
|
|
1533
|
-
# Restart timer
|
|
1534
|
-
self.wheel_timer.start(
|
|
1687
|
+
# Restart timer
|
|
1688
|
+
self.wheel_timer.start(1)
|
|
1535
1689
|
|
|
1536
1690
|
def _end_wheel_zoom(self):
|
|
1537
1691
|
"""End wheel zoom and return to selection mode"""
|
|
1538
|
-
if self.was_in_selection_before_wheel and self.selection_mode:
|
|
1692
|
+
if self.was_in_selection_before_wheel and (self.selection_mode or self.zoom_mode):
|
|
1539
1693
|
self.plot.setMouseEnabled(x=False, y=False)
|
|
1540
1694
|
self.was_in_selection_before_wheel = False
|
|
1541
1695
|
|
|
@@ -1586,7 +1740,10 @@ class NetworkGraphWidget(QWidget):
|
|
|
1586
1740
|
x_max, y_max = pos_array.max(axis=0)
|
|
1587
1741
|
|
|
1588
1742
|
# Add padding
|
|
1589
|
-
|
|
1743
|
+
if self.shell or self.component:
|
|
1744
|
+
padding = 0.75
|
|
1745
|
+
else:
|
|
1746
|
+
padding = 0.1
|
|
1590
1747
|
x_range = x_max - x_min
|
|
1591
1748
|
y_range = y_max - y_min
|
|
1592
1749
|
|
|
@@ -1679,6 +1836,32 @@ class NetworkGraphWidget(QWidget):
|
|
|
1679
1836
|
|
|
1680
1837
|
self.loading_text.setPos(0, 0) # Center of view
|
|
1681
1838
|
self.plot.addItem(self.loading_text)
|
|
1839
|
+
|
|
1840
|
+
def _popout_graph(self):
|
|
1841
|
+
|
|
1842
|
+
temp_graph_widget = NetworkGraphWidget(
|
|
1843
|
+
parent=self.parent_window,
|
|
1844
|
+
weight=self.weight,
|
|
1845
|
+
geometric=self.geometric,
|
|
1846
|
+
component = self.component,
|
|
1847
|
+
centroids=self.centroids,
|
|
1848
|
+
communities=self.communities,
|
|
1849
|
+
community_dict=self.community_dict,
|
|
1850
|
+
labels=self.labels,
|
|
1851
|
+
identities = self.identities,
|
|
1852
|
+
identity_dict = self.identity_dict,
|
|
1853
|
+
z_size = self.z_size,
|
|
1854
|
+
shell = self.shell,
|
|
1855
|
+
node_size = self.node_size,
|
|
1856
|
+
black_edges = self.black_edges,
|
|
1857
|
+
edge_size = self.edge_size
|
|
1858
|
+
)
|
|
1859
|
+
|
|
1860
|
+
temp_graph_widget.set_graph(self.graph)
|
|
1861
|
+
temp_graph_widget.show_in_window(title="Network Graph", width=1000, height=800)
|
|
1862
|
+
temp_graph_widget.load_graph()
|
|
1863
|
+
self.parent_window.temp_graph_widgets.append(temp_graph_widget)
|
|
1864
|
+
|
|
1682
1865
|
|
|
1683
1866
|
def select_nodes(self, nodes, add_to_selection=False):
|
|
1684
1867
|
"""
|
|
@@ -1734,10 +1917,12 @@ class NetworkGraphWidget(QWidget):
|
|
|
1734
1917
|
# Clear previous selection and select only this node
|
|
1735
1918
|
self.selected_nodes.clear()
|
|
1736
1919
|
self.selected_nodes.add(clicked_node)
|
|
1920
|
+
|
|
1737
1921
|
self.push_selection()
|
|
1738
1922
|
|
|
1739
1923
|
# Update visual representation
|
|
1740
1924
|
self._render_nodes()
|
|
1925
|
+
self.node_click = True
|
|
1741
1926
|
|
|
1742
1927
|
# Emit signal with all selected nodes
|
|
1743
1928
|
self.node_selected.emit(list(self.selected_nodes))
|
|
@@ -1777,6 +1962,8 @@ class NetworkGraphWidget(QWidget):
|
|
|
1777
1962
|
print(f"Found node {val} at [Z,Y,X] -> {centroid}")
|
|
1778
1963
|
self.push_selection()
|
|
1779
1964
|
except:
|
|
1965
|
+
import traceback
|
|
1966
|
+
traceback.print_exc()
|
|
1780
1967
|
pass
|
|
1781
1968
|
|
|
1782
1969
|
|
|
@@ -1847,6 +2034,11 @@ class NetworkGraphWidget(QWidget):
|
|
|
1847
2034
|
)
|
|
1848
2035
|
|
|
1849
2036
|
|
|
2037
|
+
def get_properties(self):
|
|
2038
|
+
|
|
2039
|
+
self.parent_window.update_graph_fields()
|
|
2040
|
+
|
|
2041
|
+
|
|
1850
2042
|
def create_context_menu(self, event):
|
|
1851
2043
|
# Get the index at the clicked position
|
|
1852
2044
|
# Create context menu
|
|
@@ -1898,7 +2090,7 @@ class NetworkGraphWidget(QWidget):
|
|
|
1898
2090
|
|
|
1899
2091
|
def update_params(self, weight=None, geometric=None, component = None, centroids=None,
|
|
1900
2092
|
communities=None, community_dict=None,
|
|
1901
|
-
identities=None, identity_dict=None, labels=None, z_size = None):
|
|
2093
|
+
identities=None, identity_dict=None, labels=None, z_size = None, shell = None, node_size = 10):
|
|
1902
2094
|
"""Update visualization parameters"""
|
|
1903
2095
|
if weight is not None:
|
|
1904
2096
|
self.weight = weight
|
|
@@ -1920,6 +2112,10 @@ class NetworkGraphWidget(QWidget):
|
|
|
1920
2112
|
self.labels = labels
|
|
1921
2113
|
if z_size is not None:
|
|
1922
2114
|
self.z_size = z_size
|
|
2115
|
+
if shell is not None:
|
|
2116
|
+
self.shell = shell
|
|
2117
|
+
if node_size is not None:
|
|
2118
|
+
self.node_size = node_size
|
|
1923
2119
|
|
|
1924
2120
|
def _on_view_changed(self):
|
|
1925
2121
|
"""Handle view range changes for level-of-detail adjustments"""
|
|
@@ -1978,6 +2174,11 @@ class NetworkGraphWidget(QWidget):
|
|
|
1978
2174
|
edge_alpha = min(150, int(100 + self.current_zoom_factor * 10))
|
|
1979
2175
|
else:
|
|
1980
2176
|
edge_alpha = 100
|
|
2177
|
+
|
|
2178
|
+
if self.black_edges:
|
|
2179
|
+
edge_color = (0, 0, 0)
|
|
2180
|
+
else:
|
|
2181
|
+
edge_color = (150, 150, 150, edge_alpha)
|
|
1981
2182
|
|
|
1982
2183
|
# Update edge rendering (batched edge items)
|
|
1983
2184
|
if self.edge_items:
|
|
@@ -1985,7 +2186,7 @@ class NetworkGraphWidget(QWidget):
|
|
|
1985
2186
|
current_pen = edge_item.opts['pen']
|
|
1986
2187
|
if current_pen is not None:
|
|
1987
2188
|
width = current_pen.widthF()
|
|
1988
|
-
new_pen = pg.mkPen(color=
|
|
2189
|
+
new_pen = pg.mkPen(color=edge_color, width=width)
|
|
1989
2190
|
edge_item.setPen(new_pen)
|
|
1990
2191
|
|
|
1991
2192
|
# Update labels based on zoom level
|
nettracer3d/segmenter.py
CHANGED
|
@@ -1380,7 +1380,7 @@ class InteractiveSegmenter:
|
|
|
1380
1380
|
return chunks
|
|
1381
1381
|
|
|
1382
1382
|
def train_batch(self, foreground_array, speed=True, use_gpu=False, use_two=False, mem_lock=False, saving=False):
|
|
1383
|
-
"""
|
|
1383
|
+
"""Train model for batch of arrays"""
|
|
1384
1384
|
|
|
1385
1385
|
if not saving:
|
|
1386
1386
|
print("Training model...")
|