nettracer3d 0.7.7__py3-none-any.whl → 0.7.8__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.
nettracer3d/excelotron.py CHANGED
@@ -1599,7 +1599,13 @@ class ExcelToDictGUI(QMainWindow):
1599
1599
  remapped_data = self.identity_remap_widget.get_remapped_identities(filtered_data)
1600
1600
  result_dict[key_name] = remapped_data
1601
1601
  elif key_name == 'Numerical IDs':
1602
- # Apply same filtering to Numerical IDs
1602
+
1603
+ # Check if user actually dropped a numerical IDs column
1604
+ if widget_id not in self.dict_columns or 'data' not in self.dict_columns[widget_id]:
1605
+ # Auto-generate sequential IDs and assign to column_data
1606
+ column_data = np.array(list(range(1, len(self.df) + 1)))
1607
+
1608
+ # Now use the exact same logic as if user provided the data
1603
1609
  identity_column_data = None
1604
1610
  # Find the identity column data
1605
1611
  for other_widget_id in self.dict_columns:
@@ -1620,11 +1626,55 @@ class ExcelToDictGUI(QMainWindow):
1620
1626
  result_dict[key_name] = filtered_numerical_ids
1621
1627
  else:
1622
1628
  result_dict[key_name] = column_data.tolist()
1629
+
1630
+
1623
1631
  else:
1624
1632
  result_dict[key_name] = column_data.tolist()
1625
1633
  else:
1626
1634
  result_dict[key_name] = column_data.tolist()
1627
1635
  break
1636
+
1637
+ for i in range(self.dict_layout.count()):
1638
+ item = self.dict_layout.itemAt(i)
1639
+ if item and item.widget() and hasattr(item.widget(), 'widget_id'):
1640
+ widget = item.widget()
1641
+ widget_id = widget.widget_id
1642
+ key_name = widget.header_input.text().strip()
1643
+
1644
+ # Skip if already processed (has dropped data) or no key name
1645
+ if widget_id in self.dict_columns or not key_name:
1646
+ continue
1647
+
1648
+ # Handle auto-generation for Node Identities template
1649
+ if property_name == 'Node Identities' and key_name == 'Numerical IDs':
1650
+
1651
+ # Find the identity column data
1652
+ identity_column_data = None
1653
+ for other_widget_id in self.dict_columns:
1654
+ for j in range(self.dict_layout.count()):
1655
+ item_j = self.dict_layout.itemAt(j)
1656
+ if item_j and item_j.widget() and hasattr(item_j.widget(), 'widget_id'):
1657
+ if item_j.widget().widget_id == other_widget_id:
1658
+ other_key_name = item_j.widget().header_input.text().strip()
1659
+ if other_key_name == 'Identity Column':
1660
+ identity_column_data = self.dict_columns[other_widget_id]['data']
1661
+ break
1662
+ if identity_column_data is not None:
1663
+ break
1664
+
1665
+ if identity_column_data is not None:
1666
+ # Auto-generate sequential IDs
1667
+ auto_generated_ids = np.array(list(range(1, len(self.df) + 1)))
1668
+
1669
+ filtered_indices = self.identity_remap_widget.get_filtered_indices(identity_column_data.tolist())
1670
+
1671
+ filtered_numerical_ids = [auto_generated_ids[i] for i in filtered_indices]
1672
+
1673
+ result_dict[key_name] = filtered_numerical_ids
1674
+ else:
1675
+ # Fallback: generate sequential IDs for all rows
1676
+ result_dict[key_name] = list(range(1, len(self.df) + 1))
1677
+
1628
1678
 
1629
1679
  if not result_dict:
1630
1680
  QMessageBox.warning(self, "Warning", "No valid dictionary keys defined")
@@ -202,6 +202,137 @@ def visualize_cluster_composition_umap(cluster_data: Dict[int, np.ndarray],
202
202
 
203
203
  return embedding
204
204
 
205
+ def create_community_heatmap(community_intensity, node_community, node_centroids, is_3d=True,
206
+ figsize=(12, 8), point_size=50, alpha=0.7, colorbar_label="Community Intensity"):
207
+ """
208
+ Create a 2D or 3D heatmap showing nodes colored by their community intensities.
209
+
210
+ Parameters:
211
+ -----------
212
+ community_intensity : dict
213
+ Dictionary mapping community IDs to intensity values
214
+ Keys can be np.int64 or regular ints
215
+
216
+ node_community : dict
217
+ Dictionary mapping node IDs to community IDs
218
+
219
+ node_centroids : dict
220
+ Dictionary mapping node IDs to centroids
221
+ Centroids should be [Z, Y, X] for 3D or [1, Y, X] for pseudo-3D
222
+
223
+ is_3d : bool, default=True
224
+ If True, create 3D plot. If False, create 2D plot.
225
+
226
+ figsize : tuple, default=(12, 8)
227
+ Figure size (width, height)
228
+
229
+ point_size : int, default=50
230
+ Size of scatter plot points
231
+
232
+ alpha : float, default=0.7
233
+ Transparency of points (0-1)
234
+
235
+ colorbar_label : str, default="Community Intensity"
236
+ Label for the colorbar
237
+
238
+ Returns:
239
+ --------
240
+ fig, ax : matplotlib figure and axis objects
241
+ """
242
+
243
+ # Convert numpy int64 keys to regular ints for consistency
244
+ community_intensity_clean = {}
245
+ for k, v in community_intensity.items():
246
+ if hasattr(k, 'item'): # numpy scalar
247
+ community_intensity_clean[k.item()] = v
248
+ else:
249
+ community_intensity_clean[k] = v
250
+
251
+ # Prepare data for plotting
252
+ node_positions = []
253
+ node_intensities = []
254
+
255
+ for node_id, centroid in node_centroids.items():
256
+ try:
257
+ # Get community for this node
258
+ community_id = node_community[node_id]
259
+
260
+ # Convert community_id to regular int if it's numpy
261
+ if hasattr(community_id, 'item'):
262
+ community_id = community_id.item()
263
+
264
+ # Get intensity for this community
265
+ intensity = community_intensity_clean[community_id]
266
+
267
+ node_positions.append(centroid)
268
+ node_intensities.append(intensity)
269
+ except:
270
+ pass
271
+
272
+ # Convert to numpy arrays
273
+ positions = np.array(node_positions)
274
+ intensities = np.array(node_intensities)
275
+
276
+ # Determine min and max intensities for color scaling
277
+ min_intensity = np.min(intensities)
278
+ max_intensity = np.max(intensities)
279
+
280
+ # Create figure
281
+ fig = plt.figure(figsize=figsize)
282
+
283
+ if is_3d:
284
+ # 3D plot
285
+ ax = fig.add_subplot(111, projection='3d')
286
+
287
+ # Extract coordinates (assuming [Z, Y, X] format)
288
+ z_coords = positions[:, 0]
289
+ y_coords = positions[:, 1]
290
+ x_coords = positions[:, 2]
291
+
292
+ # Create scatter plot
293
+ scatter = ax.scatter(x_coords, y_coords, z_coords,
294
+ c=intensities, s=point_size, alpha=alpha,
295
+ cmap='RdBu_r', vmin=min_intensity, vmax=max_intensity)
296
+
297
+ ax.set_xlabel('X')
298
+ ax.set_ylabel('Y')
299
+ ax.set_zlabel('Z')
300
+ ax.set_title('3D Community Intensity Heatmap')
301
+
302
+ else:
303
+ # 2D plot (using Y, X coordinates, ignoring Z/first dimension)
304
+ ax = fig.add_subplot(111)
305
+
306
+ # Extract Y, X coordinates
307
+ y_coords = positions[:, 1]
308
+ x_coords = positions[:, 2]
309
+
310
+ # Create scatter plot
311
+ scatter = ax.scatter(x_coords, y_coords,
312
+ c=intensities, s=point_size, alpha=alpha,
313
+ cmap='RdBu_r', vmin=min_intensity, vmax=max_intensity)
314
+
315
+ ax.set_xlabel('X')
316
+ ax.set_ylabel('Y')
317
+ ax.set_title('2D Community Intensity Heatmap')
318
+ ax.grid(True, alpha=0.3)
319
+
320
+ # Set origin to top-left (invert Y-axis)
321
+ ax.invert_yaxis()
322
+
323
+ # Add colorbar
324
+ cbar = plt.colorbar(scatter, ax=ax, shrink=0.8)
325
+ cbar.set_label(colorbar_label)
326
+
327
+ # Add text annotations for min/max values
328
+ cbar.ax.text(1.05, 0, f'Min: {min_intensity:.3f}\n(Blue)',
329
+ transform=cbar.ax.transAxes, va='bottom')
330
+ cbar.ax.text(1.05, 1, f'Max: {max_intensity:.3f}\n(Red)',
331
+ transform=cbar.ax.transAxes, va='top')
332
+
333
+ plt.tight_layout()
334
+ plt.show()
335
+
205
336
 
206
337
 
207
338
  # Example usage:
nettracer3d/nettracer.py CHANGED
@@ -3831,6 +3831,41 @@ class Network_3D:
3831
3831
  self._nodes = self._nodes.astype(np.uint16)
3832
3832
 
3833
3833
 
3834
+ def com_by_size(self):
3835
+ """Reassign communities based on size, starting with 1 for largest."""
3836
+
3837
+ from collections import Counter
3838
+
3839
+ # Convert all community values to regular ints (handles numpy scalars)
3840
+ clean_communities = {
3841
+ node: comm.item() if hasattr(comm, 'item') else comm
3842
+ for node, comm in self.communities.items()
3843
+ }
3844
+
3845
+ # Count community sizes and create mapping in one go
3846
+ community_sizes = Counter(clean_communities.values())
3847
+
3848
+ # Create old->new mapping: sort by size (desc), then by community ID for ties
3849
+ old_to_new = {
3850
+ old_comm: new_comm
3851
+ for new_comm, (old_comm, _) in enumerate(
3852
+ sorted(community_sizes.items(), key=lambda x: (-x[1], x[0])),
3853
+ start=1
3854
+ )
3855
+ }
3856
+
3857
+ # Apply mapping
3858
+ self.communities = {
3859
+ node: old_to_new[comm]
3860
+ for node, comm in clean_communities.items()
3861
+ }
3862
+
3863
+
3864
+
3865
+
3866
+
3867
+
3868
+
3834
3869
  def com_to_node(self, targets = None):
3835
3870
 
3836
3871
  def invert_dict(d):
@@ -4982,7 +5017,7 @@ class Network_3D:
4982
5017
 
4983
5018
  return array
4984
5019
 
4985
- def community_cells(self, size = 32):
5020
+ def community_cells(self, size = 32, xy_scale = 1, z_scale = 1):
4986
5021
 
4987
5022
  def invert_dict(d):
4988
5023
  inverted = {}
@@ -4991,10 +5026,61 @@ class Network_3D:
4991
5026
  inverted[value] = key
4992
5027
  return inverted
4993
5028
 
4994
- com_dict = proximity.partition_objects_into_cells(self.node_centroids, size)
5029
+ size_x = int(size * xy_scale)
5030
+ size_z = int(size * z_scale)
5031
+
5032
+ if size_x == size_z:
5033
+
5034
+ com_dict = proximity.partition_objects_into_cells(self.node_centroids, size_x)
5035
+
5036
+ else:
5037
+
5038
+ com_dict = proximity.partition_objects_into_cells(self.node_centroids, (size_z, size_x, size_x))
4995
5039
 
4996
5040
  self.communities = invert_dict(com_dict)
4997
5041
 
5042
+ def community_heatmap(self, num_nodes = None, is3d = True):
5043
+
5044
+ import math
5045
+
5046
+ def invert_dict(d):
5047
+ inverted = {}
5048
+ for key, value in d.items():
5049
+ inverted.setdefault(value, []).append(key)
5050
+ return inverted
5051
+
5052
+ if num_nodes == None:
5053
+
5054
+ try:
5055
+ num_nodes = len(self.network.nodes())
5056
+ except:
5057
+ try:
5058
+ num_nodes = len(self.node_centroids.keys())
5059
+ except:
5060
+ try:
5061
+ num_nodes = len(self.node_identities.keys())
5062
+ except:
5063
+ try:
5064
+ unique = np.unique(self.nodes)
5065
+ num_nodes = len(unique)
5066
+ if unique[0] == 0:
5067
+ num_nodes -= 1
5068
+ except:
5069
+ return
5070
+
5071
+ coms = invert_dict(self.communities)
5072
+
5073
+ rand_dens = num_nodes / len(coms.keys())
5074
+
5075
+ heat_dict = {}
5076
+
5077
+ for com, nodes in coms.items():
5078
+ heat_dict[com] = math.log(len(nodes)/rand_dens)
5079
+
5080
+ from . import neighborhoods
5081
+ neighborhoods.create_community_heatmap(heat_dict, self.communities, self.node_centroids, is_3d=is3d)
5082
+
5083
+ return heat_dict
4998
5084
 
4999
5085
 
5000
5086
 
@@ -205,6 +205,7 @@ class ImageViewerWindow(QMainWindow):
205
205
  self.high_button.setChecked(True)
206
206
  buttons_layout.addWidget(self.high_button)
207
207
  self.highlight = True
208
+ self.needs_mini = False
208
209
 
209
210
  self.pen_button = QPushButton("🖊️")
210
211
  self.pen_button.setCheckable(True)
@@ -394,11 +395,14 @@ class ImageViewerWindow(QMainWindow):
394
395
 
395
396
  #self.canvas.mpl_connect('button_press_event', self.on_mouse_click)
396
397
 
397
- # Initialize measurement points tracking
398
+ # Initialize measurement tracking
398
399
  self.measurement_points = [] # List to store point pairs
399
- self.current_point = None # Store first point of current pair
400
+ self.angle_measurements = [] # NEW: List to store angle trios
401
+ self.current_point = None # Store first point of current pair/trio
402
+ self.current_second_point = None # Store second point when building trio
400
403
  self.current_pair_index = 0 # Track pair numbering
401
-
404
+ self.current_trio_index = 0 # Track trio numbering
405
+ self.measurement_mode = "distance" # "distance" or "angle" mode
402
406
 
403
407
  # Add these new methods for handling neighbors and components (FOR RIGHT CLICKIGN)
404
408
  self.show_neighbors_clicked = None
@@ -664,6 +668,18 @@ class ImageViewerWindow(QMainWindow):
664
668
  edge_indices (list): List of edge indices to highlight
665
669
  """
666
670
 
671
+ if not self.high_button.isChecked():
672
+
673
+ if len(self.clicked_values['edges']) > 0:
674
+ self.format_for_upperright_table(self.clicked_values['edges'], title = 'Selected Edges')
675
+ self.needs_mini = True
676
+ if len(self.clicked_values['nodes']) > 0:
677
+ self.format_for_upperright_table(self.clicked_values['nodes'], title = 'Selected Nodes')
678
+ self.needs_mini = True
679
+
680
+ return
681
+
682
+
667
683
  def process_chunk(chunk_data, indices_to_check):
668
684
  """Process a single chunk of the array to create highlight mask"""
669
685
  mask = np.isin(chunk_data, indices_to_check)
@@ -828,21 +844,38 @@ class ImageViewerWindow(QMainWindow):
828
844
  override_obj.triggered.connect(self.handle_override)
829
845
  context_menu.addMenu(highlight_menu)
830
846
 
831
- # Create measure menu
832
- measure_menu = QMenu("Measure", self)
833
-
847
+ # Create measurement submenu
848
+ measure_menu = context_menu.addMenu("Measurements")
849
+
850
+ # Distance measurement options
851
+ distance_menu = measure_menu.addMenu("Distance")
834
852
  if self.current_point is None:
835
- # If no point is placed, show option to place first point
836
- show_point_menu = measure_menu.addAction("Place Measurement Point")
853
+ show_point_menu = distance_menu.addAction("Place First Point")
837
854
  show_point_menu.triggered.connect(
838
- lambda: self.place_point(x_idx, y_idx, self.current_slice))
855
+ lambda: self.place_distance_point(x_idx, y_idx, self.current_slice))
839
856
  else:
840
- # If first point is placed, show option to place second point
841
- show_point_menu = measure_menu.addAction("Place Second Point")
857
+ show_point_menu = distance_menu.addAction("Place Second Point")
842
858
  show_point_menu.triggered.connect(
843
- lambda: self.place_point(x_idx, y_idx, self.current_slice))
844
-
845
- show_remove_menu = measure_menu.addAction("Remove Measurement Points")
859
+ lambda: self.place_distance_point(x_idx, y_idx, self.current_slice))
860
+
861
+ # Angle measurement options
862
+ angle_menu = measure_menu.addMenu("Angle")
863
+ if self.current_point is None:
864
+ angle_first = angle_menu.addAction("Place First Point (A)")
865
+ angle_first.triggered.connect(
866
+ lambda: self.place_angle_point(x_idx, y_idx, self.current_slice))
867
+ elif self.current_second_point is None:
868
+ angle_second = angle_menu.addAction("Place Second Point (B - Vertex)")
869
+ angle_second.triggered.connect(
870
+ lambda: self.place_angle_point(x_idx, y_idx, self.current_slice))
871
+ else:
872
+ angle_third = angle_menu.addAction("Place Third Point (C)")
873
+ angle_third.triggered.connect(
874
+ lambda: self.place_angle_point(x_idx, y_idx, self.current_slice))
875
+
876
+ show_remove_menu = measure_menu.addAction("Remove All Measurements")
877
+ show_remove_menu.triggered.connect(self.handle_remove_all_measurements)
878
+
846
879
  context_menu.addMenu(measure_menu)
847
880
 
848
881
  # Connect actions to callbacks
@@ -860,7 +893,6 @@ class ImageViewerWindow(QMainWindow):
860
893
  if self.highlight_overlay is not None or self.mini_overlay_data is not None:
861
894
  highlight_select = context_menu.addAction("Add highlight in network selection")
862
895
  highlight_select.triggered.connect(self.handle_highlight_select)
863
- show_remove_menu.triggered.connect(self.handle_remove_points)
864
896
 
865
897
  cursor_pos = QCursor.pos()
866
898
  context_menu.exec(cursor_pos)
@@ -869,24 +901,25 @@ class ImageViewerWindow(QMainWindow):
869
901
  pass
870
902
 
871
903
 
872
- def place_point(self, x, y, z):
873
- """Place a measurement point at the specified coordinates."""
904
+ def place_distance_point(self, x, y, z):
905
+ """Place a measurement point for distance measurement."""
874
906
  if self.current_point is None:
875
907
  # This is the first point
876
908
  self.current_point = (x, y, z)
877
909
  self.ax.plot(x, y, 'yo', markersize=8)
878
- # Add pair index label above the point
879
- self.ax.text(x, y+5, str(self.current_pair_index),
880
- color='white', ha='center', va='bottom')
910
+ self.ax.text(x, y+5, f"D{self.current_pair_index}",
911
+ color='yellow', ha='center', va='bottom')
881
912
  self.canvas.draw()
882
-
913
+ self.measurement_mode = "distance"
883
914
  else:
884
915
  # This is the second point
885
916
  x1, y1, z1 = self.current_point
886
917
  x2, y2, z2 = x, y, z
887
918
 
888
919
  # Calculate distance
889
- distance = np.sqrt(((x2-x1)*my_network.xy_scale)**2 + ((y2-y1)*my_network.xy_scale)**2 + ((z2-z1)*my_network.z_scale)**2)
920
+ distance = np.sqrt(((x2-x1)*my_network.xy_scale)**2 +
921
+ ((y2-y1)*my_network.xy_scale)**2 +
922
+ ((z2-z1)*my_network.z_scale)**2)
890
923
  distance2 = np.sqrt(((x2-x1))**2 + ((y2-y1))**2 + ((z2-z1))**2)
891
924
 
892
925
  # Store the point pair
@@ -900,9 +933,8 @@ class ImageViewerWindow(QMainWindow):
900
933
 
901
934
  # Draw second point and line
902
935
  self.ax.plot(x2, y2, 'yo', markersize=8)
903
- # Add pair index label above the second point
904
- self.ax.text(x2, y2+5, str(self.current_pair_index),
905
- color='white', ha='center', va='bottom')
936
+ self.ax.text(x2, y2+5, f"D{self.current_pair_index}",
937
+ color='yellow', ha='center', va='bottom')
906
938
  if z1 == z2: # Only draw line if points are on same slice
907
939
  self.ax.plot([x1, x2], [y1, y2], 'r--', alpha=0.5)
908
940
  self.canvas.draw()
@@ -913,46 +945,194 @@ class ImageViewerWindow(QMainWindow):
913
945
  # Reset for next pair
914
946
  self.current_point = None
915
947
  self.current_pair_index += 1
948
+ self.measurement_mode = "distance"
949
+
950
+ def place_angle_point(self, x, y, z):
951
+ """Place a measurement point for angle measurement."""
952
+ if self.current_point is None:
953
+ # First point (A)
954
+ self.current_point = (x, y, z)
955
+ self.ax.plot(x, y, 'go', markersize=8)
956
+ self.ax.text(x, y+5, f"A{self.current_trio_index}",
957
+ color='green', ha='center', va='bottom')
958
+ self.canvas.draw()
959
+ self.measurement_mode = "angle"
960
+
961
+ elif self.current_second_point is None:
962
+ # Second point (B - vertex)
963
+ self.current_second_point = (x, y, z)
964
+ x1, y1, z1 = self.current_point
965
+
966
+ self.ax.plot(x, y, 'go', markersize=8)
967
+ self.ax.text(x, y+5, f"B{self.current_trio_index}",
968
+ color='green', ha='center', va='bottom')
969
+
970
+ # Draw line from A to B
971
+ if z1 == z:
972
+ self.ax.plot([x1, x], [y1, y], 'g--', alpha=0.7)
973
+ self.canvas.draw()
974
+
975
+ else:
976
+ # Third point (C)
977
+ x1, y1, z1 = self.current_point # Point A
978
+ x2, y2, z2 = self.current_second_point # Point B (vertex)
979
+ x3, y3, z3 = x, y, z # Point C
980
+
981
+ # Calculate angles and distances
982
+ angle_data = self.calculate_3d_angle(
983
+ (x1, y1, z1), (x2, y2, z2), (x3, y3, z3)
984
+ )
985
+
986
+ # Store the trio
987
+ self.angle_measurements.append({
988
+ 'trio_index': self.current_trio_index,
989
+ 'point_a': (x1, y1, z1),
990
+ 'point_b': (x2, y2, z2), # vertex
991
+ 'point_c': (x3, y3, z3),
992
+ **angle_data
993
+ })
994
+
995
+ # Also add the two distances as separate pairs
996
+ dist_ab = np.sqrt(((x2-x1)*my_network.xy_scale)**2 +
997
+ ((y2-y1)*my_network.xy_scale)**2 +
998
+ ((z2-z1)*my_network.z_scale)**2)
999
+ dist_bc = np.sqrt(((x3-x2)*my_network.xy_scale)**2 +
1000
+ ((y3-y2)*my_network.xy_scale)**2 +
1001
+ ((z3-z2)*my_network.z_scale)**2)
1002
+
1003
+ dist_ab_voxel = np.sqrt((x2-x1)**2 + (y2-y1)**2 + (z2-z1)**2)
1004
+ dist_bc_voxel = np.sqrt((x3-x2)**2 + (y3-y2)**2 + (z3-z2)**2)
1005
+
1006
+ self.measurement_points.extend([
1007
+ {
1008
+ 'pair_index': f"A{self.current_trio_index}-B{self.current_trio_index}",
1009
+ 'point1': (x1, y1, z1),
1010
+ 'point2': (x2, y2, z2),
1011
+ 'distance': dist_ab,
1012
+ 'distance2': dist_ab_voxel
1013
+ },
1014
+ {
1015
+ 'pair_index': f"B{self.current_trio_index}-C{self.current_trio_index}",
1016
+ 'point1': (x2, y2, z2),
1017
+ 'point2': (x3, y3, z3),
1018
+ 'distance': dist_bc,
1019
+ 'distance2': dist_bc_voxel
1020
+ }
1021
+ ])
1022
+
1023
+ # Draw third point and line
1024
+ self.ax.plot(x3, y3, 'go', markersize=8)
1025
+ self.ax.text(x3, y3+5, f"C{self.current_trio_index}",
1026
+ color='green', ha='center', va='bottom')
1027
+
1028
+ if z2 == z3: # Draw line from B to C if on same slice
1029
+ self.ax.plot([x2, x3], [y2, y3], 'g--', alpha=0.7)
1030
+ self.canvas.draw()
1031
+
1032
+ # Update measurement display
1033
+ self.update_measurement_display()
1034
+
1035
+ # Reset for next trio
1036
+ self.current_point = None
1037
+ self.current_second_point = None
1038
+ self.current_trio_index += 1
1039
+ self.measurement_mode = "angle"
1040
+
1041
+ def calculate_3d_angle(self, point_a, point_b, point_c):
1042
+ """Calculate 3D angle at vertex B between points A-B-C."""
1043
+ x1, y1, z1 = point_a
1044
+ x2, y2, z2 = point_b # vertex
1045
+ x3, y3, z3 = point_c
1046
+
1047
+ # Apply scaling
1048
+ scaled_a = np.array([x1 * my_network.xy_scale, y1 * my_network.xy_scale, z1 * my_network.z_scale])
1049
+ scaled_b = np.array([x2 * my_network.xy_scale, y2 * my_network.xy_scale, z2 * my_network.z_scale])
1050
+ scaled_c = np.array([x3 * my_network.xy_scale, y3 * my_network.xy_scale, z3 * my_network.z_scale])
1051
+
1052
+ # Create vectors from vertex B
1053
+ vec_ba = scaled_a - scaled_b
1054
+ vec_bc = scaled_c - scaled_b
1055
+
1056
+ # Calculate angle using dot product
1057
+ dot_product = np.dot(vec_ba, vec_bc)
1058
+ magnitude_ba = np.linalg.norm(vec_ba)
1059
+ magnitude_bc = np.linalg.norm(vec_bc)
1060
+
1061
+ # Avoid division by zero
1062
+ if magnitude_ba == 0 or magnitude_bc == 0:
1063
+ return {'angle_degrees': 0}
1064
+
1065
+ cos_angle = dot_product / (magnitude_ba * magnitude_bc)
1066
+ cos_angle = np.clip(cos_angle, -1.0, 1.0) # Handle numerical errors
1067
+
1068
+ angle_radians = np.arccos(cos_angle)
1069
+ angle_degrees = np.degrees(angle_radians)
1070
+
1071
+ return {'angle_degrees': angle_degrees}
916
1072
 
917
- def handle_remove_points(self):
918
- """Remove all measurement points."""
1073
+ def handle_remove_all_measurements(self):
1074
+ """Remove all measurement points and angles."""
919
1075
  self.measurement_points = []
1076
+ self.angle_measurements = []
920
1077
  self.current_point = None
1078
+ self.current_second_point = None
921
1079
  self.current_pair_index = 0
1080
+ self.current_trio_index = 0
1081
+ self.measurement_mode = "distance"
922
1082
  self.update_display()
923
1083
  self.update_measurement_display()
924
1084
 
925
- # Modify the update_measurement_display method:
926
1085
  def update_measurement_display(self):
927
1086
  """Update the measurement information display in the top right widget."""
1087
+ # Distance measurements
928
1088
  if not self.measurement_points:
929
- # Create empty DataFrame with no specific headers
930
- df = pd.DataFrame()
1089
+ distance_df = pd.DataFrame()
931
1090
  else:
932
- # Create data for DataFrame with measurement-specific headers
933
- data = []
1091
+ distance_data = []
934
1092
  for point in self.measurement_points:
935
1093
  x1, y1, z1 = point['point1']
936
1094
  x2, y2, z2 = point['point2']
937
- data.append({
1095
+ distance_data.append({
938
1096
  'Pair ID': point['pair_index'],
939
1097
  'Point 1 (X,Y,Z)': f"({x1:.1f}, {y1:.1f}, {z1})",
940
1098
  'Point 2 (X,Y,Z)': f"({x2:.1f}, {y2:.1f}, {z2})",
941
1099
  'Scaled Distance': f"{point['distance']:.2f}",
942
1100
  'Voxel Distance': f"{point['distance2']:.2f}"
943
1101
  })
944
- df = pd.DataFrame(data)
1102
+ distance_df = pd.DataFrame(distance_data)
945
1103
 
946
- # Create new table for measurements
947
- table = CustomTableView(self)
948
- table.setModel(PandasModel(df))
1104
+ # Angle measurements
1105
+ if not self.angle_measurements:
1106
+ angle_df = pd.DataFrame()
1107
+ else:
1108
+ angle_data = []
1109
+ for angle in self.angle_measurements:
1110
+ xa, ya, za = angle['point_a']
1111
+ xb, yb, zb = angle['point_b']
1112
+ xc, yc, zc = angle['point_c']
1113
+ angle_data.append({
1114
+ 'Trio ID': f"A{angle['trio_index']}-B{angle['trio_index']}-C{angle['trio_index']}",
1115
+ 'Point A (X,Y,Z)': f"({xa:.1f}, {ya:.1f}, {za})",
1116
+ 'Point B (X,Y,Z)': f"({xb:.1f}, {yb:.1f}, {zb})",
1117
+ 'Point C (X,Y,Z)': f"({xc:.1f}, {yc:.1f}, {zc})",
1118
+ 'Angle (°)': f"{angle['angle_degrees']:.1f}"
1119
+ })
1120
+ angle_df = pd.DataFrame(angle_data)
949
1121
 
950
- # Add to tabbed widget
951
- self.tabbed_data.add_table("Measurements", table)
1122
+ # Create tables
1123
+ if not distance_df.empty:
1124
+ distance_table = CustomTableView(self)
1125
+ distance_table.setModel(PandasModel(distance_df))
1126
+ self.tabbed_data.add_table("Distance Measurements", distance_table)
1127
+ for column in range(distance_table.model().columnCount(None)):
1128
+ distance_table.resizeColumnToContents(column)
952
1129
 
953
- # Adjust column widths to content
954
- for column in range(table.model().columnCount(None)):
955
- table.resizeColumnToContents(column)
1130
+ if not angle_df.empty:
1131
+ angle_table = CustomTableView(self)
1132
+ angle_table.setModel(PandasModel(angle_df))
1133
+ self.tabbed_data.add_table("Angle Measurements", angle_table)
1134
+ for column in range(angle_table.model().columnCount(None)):
1135
+ angle_table.resizeColumnToContents(column)
956
1136
 
957
1137
 
958
1138
  def show_network_table(self):
@@ -1732,6 +1912,12 @@ class ImageViewerWindow(QMainWindow):
1732
1912
  self.highlight = self.high_button.isChecked()
1733
1913
  current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
1734
1914
  current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
1915
+
1916
+ if self.high_button.isChecked():
1917
+ if self.highlight_overlay is None and ((len(self.clicked_values['nodes']) + len(self.clicked_values['edges'])) > 0):
1918
+ if self.needs_mini:
1919
+ self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
1920
+ self.needs_mini = False
1735
1921
 
1736
1922
  self.update_display(preserve_zoom=(current_xlim, current_ylim))
1737
1923
 
@@ -2704,6 +2890,8 @@ class ImageViewerWindow(QMainWindow):
2704
2890
  network_menu = analysis_menu.addMenu("Network")
2705
2891
  netshow_action = network_menu.addAction("Show Network")
2706
2892
  netshow_action.triggered.connect(self.show_netshow_dialog)
2893
+ report_action = network_menu.addAction("Generic Network Report")
2894
+ report_action.triggered.connect(self.handle_report)
2707
2895
  partition_action = network_menu.addAction("Community Partition + Generic Community Stats")
2708
2896
  partition_action.triggered.connect(self.show_partition_dialog)
2709
2897
  com_identity_action = network_menu.addAction("Identity Makeup of Network Communities (and UMAP)")
@@ -2723,8 +2911,10 @@ class ImageViewerWindow(QMainWindow):
2723
2911
  degree_dist_action.triggered.connect(self.show_degree_dist_dialog)
2724
2912
  neighbor_id_action = stats_menu.addAction("Identity Distribution of Neighbors")
2725
2913
  neighbor_id_action.triggered.connect(self.show_neighbor_id_dialog)
2726
- ripley_action = stats_menu.addAction("Clustering Analysis")
2914
+ ripley_action = stats_menu.addAction("Ripley Clustering Analysis")
2727
2915
  ripley_action.triggered.connect(self.show_ripley_dialog)
2916
+ heatmap_action = stats_menu.addAction("Community Cluster Heatmap")
2917
+ heatmap_action.triggered.connect(self.show_heatmap_dialog)
2728
2918
  vol_action = stats_menu.addAction("Calculate Volumes")
2729
2919
  vol_action.triggered.connect(self.volumes)
2730
2920
  rad_action = stats_menu.addAction("Calculate Radii")
@@ -4396,6 +4586,44 @@ class ImageViewerWindow(QMainWindow):
4396
4586
  dialog = NetShowDialog(self)
4397
4587
  dialog.exec()
4398
4588
 
4589
+ def handle_report(self):
4590
+
4591
+ def invert_dict(d):
4592
+ inverted = {}
4593
+ for key, value in d.items():
4594
+ inverted.setdefault(value, []).append(key)
4595
+ return inverted
4596
+
4597
+ stats = {}
4598
+
4599
+ try:
4600
+ # Basic graph properties
4601
+ stats['num_nodes'] = my_network.network.number_of_nodes()
4602
+ stats['num_edges'] = my_network.network.number_of_edges()
4603
+ except:
4604
+ pass
4605
+
4606
+ try:
4607
+ idens = invert_dict(my_network.node_identities)
4608
+
4609
+ for iden, nodes in idens.items():
4610
+ stats[f'num_nodes_{iden}'] = len(nodes)
4611
+ except:
4612
+ pass
4613
+
4614
+ try:
4615
+
4616
+ coms = invert_dict(my_network.communities)
4617
+
4618
+ for com, nodes in coms.items():
4619
+ stats[f'num_nodes_community_{com}'] = len(nodes)
4620
+ except:
4621
+ pass
4622
+
4623
+ self.format_for_upperright_table(stats, title = 'Network Report')
4624
+
4625
+
4626
+
4399
4627
  def show_partition_dialog(self):
4400
4628
  dialog = PartitionDialog(self)
4401
4629
  dialog.exec()
@@ -4431,6 +4659,10 @@ class ImageViewerWindow(QMainWindow):
4431
4659
  dialog = RipleyDialog(self)
4432
4660
  dialog.exec()
4433
4661
 
4662
+ def show_heatmap_dialog(self):
4663
+ dialog = HeatmapDialog(self)
4664
+ dialog.exec()
4665
+
4434
4666
  def show_random_dialog(self):
4435
4667
  dialog = RandomDialog(self)
4436
4668
  dialog.exec()
@@ -5660,8 +5892,7 @@ class ArbitraryDialog(QDialog):
5660
5892
 
5661
5893
  except Exception as e:
5662
5894
  QMessageBox.critical(self, "Error", f"Error processing selections: {str(e)}")
5663
- import traceback
5664
- print(traceback.format_exc())
5895
+
5665
5896
 
5666
5897
  class Show3dDialog(QDialog):
5667
5898
  def __init__(self, parent=None):
@@ -6223,7 +6454,7 @@ class ComNeighborDialog(QDialog):
6223
6454
  layout.addRow("Min Community Size to be grouped (Smaller communities will be placed in neighborhood 0 - does not apply if empty)", self.limit)
6224
6455
 
6225
6456
  # Add Run button
6226
- run_button = QPushButton("Get Neighborhoods (Note this overwrites current communities - save your coms first)")
6457
+ run_button = QPushButton("Get Communities")
6227
6458
  run_button.clicked.connect(self.run)
6228
6459
  layout.addWidget(run_button)
6229
6460
 
@@ -6277,6 +6508,12 @@ class ComCellDialog(QDialog):
6277
6508
  self.size = QLineEdit("")
6278
6509
  layout.addRow("Cell Size:", self.size)
6279
6510
 
6511
+ self.xy_scale = QLineEdit(f"{my_network.xy_scale}")
6512
+ layout.addRow("xy scale:", self.xy_scale)
6513
+
6514
+ self.z_scale = QLineEdit(f"{my_network.z_scale}")
6515
+ layout.addRow("z scale:", self.z_scale)
6516
+
6280
6517
  # Add Run button
6281
6518
  run_button = QPushButton("Get Neighborhoods (Note this overwrites current communities - save your coms first)")
6282
6519
  run_button.clicked.connect(self.run)
@@ -6286,7 +6523,9 @@ class ComCellDialog(QDialog):
6286
6523
 
6287
6524
  try:
6288
6525
 
6289
- size = int(self.size.text()) if self.size.text().strip() else None
6526
+ size = float(self.size.text()) if self.size.text().strip() else None
6527
+ xy_scale = float(self.xy_scale.text()) if self.xy_scale.text().strip() else 1
6528
+ z_scale = float(self.z_scale.text()) if self.z_scale.text().strip() else 1
6290
6529
 
6291
6530
  if size is None:
6292
6531
  return
@@ -6296,7 +6535,7 @@ class ComCellDialog(QDialog):
6296
6535
  if my_network.node_centroids is None:
6297
6536
  return
6298
6537
 
6299
- my_network.community_cells(size = size)
6538
+ my_network.community_cells(size = size, xy_scale = xy_scale, z_scale = z_scale)
6300
6539
 
6301
6540
  self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'CommunityID')
6302
6541
 
@@ -6577,6 +6816,60 @@ class RipleyDialog(QDialog):
6577
6816
  print(traceback.format_exc())
6578
6817
  print(f"Error: {e}")
6579
6818
 
6819
+ class HeatmapDialog(QDialog):
6820
+
6821
+ def __init__(self, parent = None):
6822
+
6823
+ super().__init__(parent)
6824
+ self.setWindowTitle("Heatmap Parameters")
6825
+ self.setModal(True)
6826
+
6827
+ layout = QFormLayout(self)
6828
+
6829
+ self.nodecount = QLineEdit("")
6830
+ layout.addRow("(Optional) Total Number of Nodes?:", self.nodecount)
6831
+
6832
+
6833
+ # stats checkbox (default True)
6834
+ self.is3d = QPushButton("3D")
6835
+ self.is3d.setCheckable(True)
6836
+ self.is3d.setChecked(True)
6837
+ layout.addRow("Use 3D Plot (uncheck for 2D)?:", self.is3d)
6838
+
6839
+
6840
+ # Add Run button
6841
+ run_button = QPushButton("Run")
6842
+ run_button.clicked.connect(self.run)
6843
+ layout.addWidget(run_button)
6844
+
6845
+ def run(self):
6846
+
6847
+ nodecount = int(self.nodecount.text()) if self.nodecount.text().strip() else None
6848
+
6849
+ is3d = self.is3d.isChecked()
6850
+
6851
+
6852
+ if my_network.communities is None:
6853
+ if my_network.network is not None:
6854
+ self.parent().show_partition_dialog()
6855
+ else:
6856
+ self.parent().handle_com_cell()
6857
+ if my_network.communities is None:
6858
+ return
6859
+
6860
+ heat_dict = my_network.community_heatmap(num_nodes = nodecount, is3d = is3d)
6861
+
6862
+ self.parent().format_for_upperright_table(heat_dict, metric='Community', value='ln(Predicted Community Nodecount/Actual)', title="Community Heatmap")
6863
+
6864
+ self.accept()
6865
+
6866
+
6867
+
6868
+
6869
+
6870
+
6871
+
6872
+
6580
6873
  class RandomDialog(QDialog):
6581
6874
 
6582
6875
  def __init__(self, parent=None):
@@ -10361,6 +10654,12 @@ class ModifyDialog(QDialog):
10361
10654
  self.isolate.setChecked(False)
10362
10655
  layout.addRow("Isolate connections between two specific node types (if assigned)?:", self.isolate)
10363
10656
 
10657
+ # isolate checkbox (default false)
10658
+ self.com_sizes = QPushButton("Communities By Size")
10659
+ self.com_sizes.setCheckable(True)
10660
+ self.com_sizes.setChecked(False)
10661
+ layout.addRow("Rearrange Community IDs by size?:", self.com_sizes)
10662
+
10364
10663
  # Community collapse checkbox (default False)
10365
10664
  self.comcollapse = QPushButton("Communities -> nodes")
10366
10665
  self.comcollapse.setCheckable(True)
@@ -10403,6 +10702,7 @@ class ModifyDialog(QDialog):
10403
10702
  isolate = self.isolate.isChecked()
10404
10703
  comcollapse = self.comcollapse.isChecked()
10405
10704
  remove = self.remove.isChecked()
10705
+ com_size = self.com_sizes.isChecked()
10406
10706
 
10407
10707
 
10408
10708
  if isolate and my_network.node_identities is not None:
@@ -10453,6 +10753,14 @@ class ModifyDialog(QDialog):
10453
10753
  self.parent().format_for_upperright_table(my_network.node_centroids, 'NodeID', ['Z', 'Y', 'X'], 'Node Centroids')
10454
10754
  except:
10455
10755
  pass
10756
+ if com_size:
10757
+ if my_network.communities is None:
10758
+ self.parent().show_partition_dialog()
10759
+ if my_network.communities is None:
10760
+ return
10761
+ my_network.com_by_size()
10762
+ self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'Community', 'Node Communities')
10763
+
10456
10764
  if comcollapse:
10457
10765
  if my_network.communities is None:
10458
10766
  self.parent().show_partition_dialog()
@@ -10482,6 +10790,8 @@ class ModifyDialog(QDialog):
10482
10790
  self.accept()
10483
10791
 
10484
10792
  except Exception as e:
10793
+ import traceback
10794
+ print(traceback.format_exc())
10485
10795
  print(f"An error occurred: {e}")
10486
10796
 
10487
10797
 
@@ -10612,85 +10922,107 @@ class CalcAllDialog(QDialog):
10612
10922
  prev_fastdil = False
10613
10923
  prev_overlays = False
10614
10924
  prev_updates = True
10615
-
10925
+
10616
10926
  def __init__(self, parent=None):
10617
10927
  super().__init__(parent)
10618
- self.setWindowTitle("Calculate All Parameters")
10928
+ self.setWindowTitle("Calculate Connectivity Network Parameters")
10619
10929
  self.setModal(True)
10620
10930
 
10621
- layout = QFormLayout(self)
10931
+ # Main layout
10932
+ main_layout = QVBoxLayout(self)
10622
10933
 
10623
- # Directory (empty by default)
10624
- self.directory = QLineEdit(self.prev_directory)
10625
- self.directory.setPlaceholderText("Leave empty for 'my_network'")
10626
- layout.addRow("Output Directory:", self.directory)
10934
+ # Important Parameters Group
10935
+ important_group = QGroupBox("Important Parameters")
10936
+ important_layout = QFormLayout(important_group)
10627
10937
 
10628
- # Load previous values for all inputs
10629
10938
  self.xy_scale = QLineEdit(f'{my_network.xy_scale}')
10630
- layout.addRow("xy_scale:", self.xy_scale)
10939
+ important_layout.addRow("xy_scale:", self.xy_scale)
10631
10940
 
10632
10941
  self.z_scale = QLineEdit(f'{my_network.z_scale}')
10633
- layout.addRow("z_scale:", self.z_scale)
10634
-
10942
+ important_layout.addRow("z_scale:", self.z_scale)
10943
+
10635
10944
  self.search = QLineEdit(self.prev_search)
10636
10945
  self.search.setPlaceholderText("Leave empty for None")
10637
- layout.addRow("Node Search (float):", self.search)
10638
-
10946
+ important_layout.addRow("Node Search (float):", self.search)
10947
+
10639
10948
  self.diledge = QLineEdit(self.prev_diledge)
10640
10949
  self.diledge.setPlaceholderText("Leave empty for None")
10641
- layout.addRow("Edge Reconnection Distance (float):", self.diledge)
10642
-
10643
- self.down_factor = QLineEdit(self.prev_down_factor)
10644
- self.down_factor.setPlaceholderText("Leave empty for None")
10645
- layout.addRow("Downsample for Centroids (int):", self.down_factor)
10646
-
10647
- self.GPU_downsample = QLineEdit(self.prev_GPU_downsample)
10648
- self.GPU_downsample.setPlaceholderText("Leave empty for None")
10649
- layout.addRow("Downsample for Distance Transform (GPU) (int):", self.GPU_downsample)
10650
-
10950
+ important_layout.addRow("Edge Reconnection Distance (float):", self.diledge)
10951
+
10952
+ self.label_nodes = QPushButton("Label")
10953
+ self.label_nodes.setCheckable(True)
10954
+ self.label_nodes.setChecked(self.prev_label_nodes)
10955
+ important_layout.addRow("Re-Label Nodes (WARNING - OVERRIDES ANY CURRENT LABELS):", self.label_nodes)
10956
+
10957
+ main_layout.addWidget(important_group)
10958
+
10959
+ # Optional Parameters Group
10960
+ optional_group = QGroupBox("Optional Parameters")
10961
+ optional_layout = QFormLayout(optional_group)
10962
+
10651
10963
  self.other_nodes = QLineEdit(self.prev_other_nodes)
10652
10964
  self.other_nodes.setPlaceholderText("Leave empty for None")
10653
- layout.addRow("Filepath or directory containing additional node images:", self.other_nodes)
10654
-
10965
+ optional_layout.addRow("Filepath or directory containing additional node images:", self.other_nodes)
10966
+
10655
10967
  self.remove_trunk = QLineEdit(self.prev_remove_trunk)
10656
10968
  self.remove_trunk.setPlaceholderText("Leave empty for 0")
10657
- layout.addRow("Times to remove edge trunks (int): ", self.remove_trunk)
10658
-
10659
- # Load previous button states
10660
- self.gpu = QPushButton("GPU")
10661
- self.gpu.setCheckable(True)
10662
- self.gpu.setChecked(self.prev_gpu)
10663
- layout.addRow("Use GPU:", self.gpu)
10664
-
10665
- self.label_nodes = QPushButton("Label")
10666
- self.label_nodes.setCheckable(True)
10667
- self.label_nodes.setChecked(self.prev_label_nodes)
10668
- layout.addRow("Re-Label Nodes (WARNING - OVERRIDES ANY CURRENT LABELS):", self.label_nodes)
10669
-
10969
+ optional_layout.addRow("Times to remove edge trunks (int):", self.remove_trunk)
10970
+
10670
10971
  self.inners = QPushButton("Inner Edges")
10671
10972
  self.inners.setCheckable(True)
10672
10973
  self.inners.setChecked(self.prev_inners)
10673
- layout.addRow("Use Inner Edges:", self.inners)
10674
-
10974
+ optional_layout.addRow("Use Inner Edges:", self.inners)
10975
+
10976
+ main_layout.addWidget(optional_group)
10977
+
10978
+ # Speed Up Options Group
10979
+ speedup_group = QGroupBox("Speed Up Options")
10980
+ speedup_layout = QFormLayout(speedup_group)
10981
+
10982
+ self.down_factor = QLineEdit(self.prev_down_factor)
10983
+ self.down_factor.setPlaceholderText("Leave empty for None")
10984
+ speedup_layout.addRow("Downsample for Centroids (int):", self.down_factor)
10985
+
10986
+ self.GPU_downsample = QLineEdit(self.prev_GPU_downsample)
10987
+ self.GPU_downsample.setPlaceholderText("Leave empty for None")
10988
+ speedup_layout.addRow("Downsample for Distance Transform (GPU) (int):", self.GPU_downsample)
10989
+
10990
+ self.gpu = QPushButton("GPU")
10991
+ self.gpu.setCheckable(True)
10992
+ self.gpu.setChecked(self.prev_gpu)
10993
+ speedup_layout.addRow("Use GPU:", self.gpu)
10994
+
10675
10995
  self.fastdil = QPushButton("Fast Dilate")
10676
10996
  self.fastdil.setCheckable(True)
10677
10997
  self.fastdil.setChecked(self.prev_fastdil)
10678
- layout.addRow("Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.fastdil)
10679
-
10998
+ speedup_layout.addRow("Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.fastdil)
10999
+
11000
+ main_layout.addWidget(speedup_group)
11001
+
11002
+ # Output Options Group
11003
+ output_group = QGroupBox("Output Options")
11004
+ output_layout = QFormLayout(output_group)
11005
+
11006
+ self.directory = QLineEdit(self.prev_directory)
11007
+ self.directory.setPlaceholderText("Will Have to Save Manually If Empty")
11008
+ output_layout.addRow("Output Directory:", self.directory)
11009
+
10680
11010
  self.overlays = QPushButton("Overlays")
10681
11011
  self.overlays.setCheckable(True)
10682
11012
  self.overlays.setChecked(self.prev_overlays)
10683
- layout.addRow("Generate Overlays:", self.overlays)
10684
-
11013
+ output_layout.addRow("Generate Overlays:", self.overlays)
11014
+
10685
11015
  self.update = QPushButton("Update")
10686
11016
  self.update.setCheckable(True)
10687
11017
  self.update.setChecked(self.prev_updates)
10688
- layout.addRow("Update Node/Edge in NetTracer3D:", self.update)
11018
+ output_layout.addRow("Update Node/Edge in NetTracer3D:", self.update)
11019
+
11020
+ main_layout.addWidget(output_group)
10689
11021
 
10690
11022
  # Add Run button
10691
11023
  run_button = QPushButton("Run Calculate All")
10692
11024
  run_button.clicked.connect(self.run_calc_all)
10693
- layout.addRow(run_button)
11025
+ main_layout.addWidget(run_button)
10694
11026
 
10695
11027
  def run_calc_all(self):
10696
11028
 
@@ -10867,65 +11199,87 @@ class CalcAllDialog(QDialog):
10867
11199
  f"Error running calculate all: {str(e)}"
10868
11200
  )
10869
11201
 
10870
- class ProxDialog(QDialog):
10871
11202
 
11203
+ class ProxDialog(QDialog):
10872
11204
  def __init__(self, parent=None):
10873
11205
  super().__init__(parent)
10874
11206
  self.setWindowTitle("Calculate Proximity Network")
10875
11207
  self.setModal(True)
10876
-
10877
- layout = QFormLayout(self)
10878
-
10879
- # Directory (empty by default)
10880
- self.directory = QLineEdit('')
10881
- self.directory.setPlaceholderText("Leave empty for 'my_network'")
10882
- layout.addRow("Output Directory:", self.directory)
10883
-
11208
+
11209
+ # Main layout
11210
+ main_layout = QVBoxLayout(self)
11211
+
11212
+ # Important Parameters Group
11213
+ important_group = QGroupBox("Important Parameters")
11214
+ important_layout = QFormLayout(important_group)
11215
+
10884
11216
  self.search = QLineEdit()
10885
11217
  self.search.setPlaceholderText("search")
10886
- layout.addRow("Search Region Distance? (enter true value corresponding to scaling, ie in microns):", self.search)
10887
-
10888
- # Load previous values for all inputs
11218
+ important_layout.addRow("Search Region Distance? (enter true value corresponding to scaling, ie in microns):", self.search)
11219
+
10889
11220
  self.xy_scale = QLineEdit(f"{my_network.xy_scale}")
10890
- layout.addRow("xy_scale:", self.xy_scale)
11221
+ important_layout.addRow("xy_scale:", self.xy_scale)
10891
11222
 
10892
11223
  self.z_scale = QLineEdit(f"{my_network.z_scale}")
10893
- layout.addRow("z_scale:", self.z_scale)
10894
-
10895
- # Add mode selection dropdown
11224
+ important_layout.addRow("z_scale:", self.z_scale)
11225
+
11226
+ main_layout.addWidget(important_group)
11227
+
11228
+ # Mode Group
11229
+ mode_group = QGroupBox("Mode")
11230
+ mode_layout = QFormLayout(mode_group)
11231
+
10896
11232
  self.mode_selector = QComboBox()
10897
11233
  self.mode_selector.addItems(["From Centroids (fast but ignores shape - use for small or spherical objects - search STARTS at centroid)", "From Morphological Shape (slower but preserves shape - use for oddly shaped objects - search STARTS at object border)"])
10898
11234
  self.mode_selector.setCurrentIndex(0) # Default to Mode 1
10899
- layout.addRow("Execution Mode:", self.mode_selector)
10900
-
10901
- self.fastdil = QPushButton("Fast Dilate")
10902
- self.fastdil.setCheckable(True)
10903
- self.fastdil.setChecked(False)
10904
- layout.addRow("(If using morphological) Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.fastdil)
10905
-
11235
+ mode_layout.addRow("Execution Mode:", self.mode_selector)
11236
+
10906
11237
  if my_network.node_identities is not None:
10907
11238
  self.id_selector = QComboBox()
10908
11239
  # Add all options from id dictionary
10909
11240
  self.id_selector.addItems(['None'] + list(set(my_network.node_identities.values())))
10910
11241
  self.id_selector.setCurrentIndex(0) # Default to Mode 1
10911
- layout.addRow("Create Networks only from a specific node identity?:", self.id_selector)
11242
+ mode_layout.addRow("Create Networks only from a specific node identity?:", self.id_selector)
10912
11243
  else:
10913
11244
  self.id_selector = None
10914
-
11245
+
11246
+ main_layout.addWidget(mode_group)
11247
+
11248
+ # Output Options Group
11249
+ output_group = QGroupBox("Output Options")
11250
+ output_layout = QFormLayout(output_group)
11251
+
11252
+ self.directory = QLineEdit('')
11253
+ self.directory.setPlaceholderText("Leave empty for 'my_network'")
11254
+ output_layout.addRow("Output Directory:", self.directory)
11255
+
10915
11256
  self.overlays = QPushButton("Overlays")
10916
11257
  self.overlays.setCheckable(True)
10917
11258
  self.overlays.setChecked(True)
10918
- layout.addRow("Generate Overlays:", self.overlays)
10919
-
11259
+ output_layout.addRow("Generate Overlays:", self.overlays)
11260
+
10920
11261
  self.populate = QPushButton("Populate Nodes from Centroids?")
10921
11262
  self.populate.setCheckable(True)
10922
11263
  self.populate.setChecked(False)
10923
- layout.addRow("If using centroid search:", self.populate)
10924
-
11264
+ output_layout.addRow("If using centroid search:", self.populate)
11265
+
11266
+ main_layout.addWidget(output_group)
11267
+
11268
+ # Speed Up Options Group
11269
+ speedup_group = QGroupBox("Speed Up Options")
11270
+ speedup_layout = QFormLayout(speedup_group)
11271
+
11272
+ self.fastdil = QPushButton("Fast Dilate")
11273
+ self.fastdil.setCheckable(True)
11274
+ self.fastdil.setChecked(False)
11275
+ speedup_layout.addRow("(If using morphological) Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.fastdil)
11276
+
11277
+ main_layout.addWidget(speedup_group)
11278
+
10925
11279
  # Add Run button
10926
11280
  run_button = QPushButton("Run Proximity Network")
10927
11281
  run_button.clicked.connect(self.prox)
10928
- layout.addRow(run_button)
11282
+ main_layout.addWidget(run_button)
10929
11283
 
10930
11284
  def prox(self):
10931
11285
 
nettracer3d/proximity.py CHANGED
@@ -723,7 +723,7 @@ def partition_objects_into_cells(object_centroids, cell_size):
723
723
  cell_indices[1] * num_cells[2] +
724
724
  cell_indices[2])
725
725
 
726
- cell_assignments[int(cell_number)].append(label)
726
+ cell_assignments[int(cell_number)].append(int(label))
727
727
 
728
728
  # Convert defaultdict to regular dict and sort keys
729
729
  return dict(sorted(cell_assignments.items()))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nettracer3d
3
- Version: 0.7.7
3
+ Version: 0.7.8
4
4
  Summary: Scripts for intializing and analyzing networks from segmentations of three dimensional images.
5
5
  Author-email: Liam McLaughlin <liamm@wustl.edu>
6
6
  Project-URL: Documentation, https://nettracer3d.readthedocs.io/en/latest/
@@ -73,6 +73,14 @@ NetTracer3D is free to use/fork for academic/nonprofit use so long as citation i
73
73
 
74
74
  NetTracer3D was developed by Liam McLaughlin while working under Dr. Sanjay Jain at Washington University School of Medicine.
75
75
 
76
- -- Version 0.7.7 Updates --
77
-
78
- * See documentation once updated
76
+ -- Version 0.7.8 (and 0.7.7) Updates --
77
+
78
+ * Bug Fixes
79
+ * Added the excel helper loader for better automated loading from QuPath exports specifically
80
+ * Added the ability to cluster communities into broader neighborhoods (with KMeans) based on their compositions.
81
+ * Added heatmap and UMAP graph displays based on community compositions.
82
+ * Added the ability to show heatmaps of nodes based on their density within their communities
83
+ * Added the ability to cluster nodes into communities based on spatial grouping in arbitrarily-sized cells (rather than just using the network)
84
+ * Added function to crop the current image
85
+ * More options under 'Modify Network'
86
+ * 'Show 3D' method now can render a bounding box.
@@ -1,23 +1,23 @@
1
1
  nettracer3d/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  nettracer3d/community_extractor.py,sha256=BrONCLRLYUdMfLe_018AQi0k0J7xwVahc_lmsOO5Pwo,23086
3
- nettracer3d/excelotron.py,sha256=jTAwMKIvyLgQBhDSHAhlTH02c9rQAKMck4ic5lE2KM4,68224
3
+ nettracer3d/excelotron.py,sha256=lS5vnpoOGZWp7fdqVpTPqeC-mUKrfwDrWHfx4PQ7Uzg,71384
4
4
  nettracer3d/modularity.py,sha256=O9OeKbjD3v6gSFz9K2GzP6LsxlpQaPfeJbM1pyIEigw,21788
5
5
  nettracer3d/morphology.py,sha256=jyDjYzrZ4LvI5jOyw8DLsxmo-i5lpqHsejYpW7Tq7Mo,19786
6
- nettracer3d/neighborhoods.py,sha256=oCnvfGSjWdpPUdzUn_WhQVSIptGIYSVI0rmzWP_CfE0,7795
7
- nettracer3d/nettracer.py,sha256=RR3UFReS86AHQXHBZkzwQfhtA8pFtDK9K41pvxY5lbg,223147
8
- nettracer3d/nettracer_gui.py,sha256=8ZaReS5KbY4hgIWn65FHkBlTyrWwwowbsr09DATq3g4,454229
6
+ nettracer3d/neighborhoods.py,sha256=kkKR8m6Gjw34cDd_mytAIwLxqvuNBtQb2hU4JuBY9pI,12301
7
+ nettracer3d/nettracer.py,sha256=M1KFIPg7WCzm8BXQOreuEVhgjg0PpLKRg4Y88DyVuK8,225843
8
+ nettracer3d/nettracer_gui.py,sha256=WTcN-tOk4vzDLhnVN4un5PEkC90GDp3sxZZhgLifMOM,468787
9
9
  nettracer3d/network_analysis.py,sha256=h-5yzUWdE0hcWYy8wcBA5LV1bRhdqiMnKbQLrRzb1Sw,41443
10
10
  nettracer3d/network_draw.py,sha256=F7fw6Pcf4qWOhdKwLmhwqWdschbDlHzwCVolQC9imeU,14117
11
11
  nettracer3d/node_draw.py,sha256=k3sCTfUCJs3aH1C1q1gTNxDz9EAQbBd1hsUIJajxRx8,9823
12
- nettracer3d/proximity.py,sha256=A81hNug8G4Rxx8OizhhzcnAP2ozoZghl52q3Kgefw3I,27991
12
+ nettracer3d/proximity.py,sha256=5n8qxqxmmMtq5bqVpSkqw3EefuZIyGdLybVs18D3ZNg,27996
13
13
  nettracer3d/run.py,sha256=xYeaAc8FCx8MuzTGyL3NR3mK7WZzffAYAH23bNRZYO4,127
14
14
  nettracer3d/segmenter.py,sha256=BD9vxnblDKXfmR8hLP_iVqqfVngH6opz4Q7V6sxc2KM,60062
15
15
  nettracer3d/segmenter_GPU.py,sha256=Fqr0Za6X2ss4rfaziqOhvhBfbGDPHkHw6fVxs39lZaU,53862
16
16
  nettracer3d/simple_network.py,sha256=Ft_81VhVQ3rqoXvuYnsckXuxCcQSJfakhOfkFaclxZY,9340
17
17
  nettracer3d/smart_dilate.py,sha256=DOEOQq9ig6-AO4MpqAG0CqrGDFqw5_UBeqfSedqHk28,25933
18
- nettracer3d-0.7.7.dist-info/licenses/LICENSE,sha256=gM207DhJjWrxLuEWXl0Qz5ISbtWDmADfjHp3yC2XISs,888
19
- nettracer3d-0.7.7.dist-info/METADATA,sha256=VvJgZUXLw2U4nMlLO2fuMvEc9Pt3EVaeS3jrXpC7e5c,4141
20
- nettracer3d-0.7.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
21
- nettracer3d-0.7.7.dist-info/entry_points.txt,sha256=Nx1rr_0QhJXDBHAQg2vcqCzLMKBzSHfwy3xwGkueVyc,53
22
- nettracer3d-0.7.7.dist-info/top_level.txt,sha256=zsYy9rZwirfCEOubolhee4TyzqBAL5gSUeFMzhFTX8c,12
23
- nettracer3d-0.7.7.dist-info/RECORD,,
18
+ nettracer3d-0.7.8.dist-info/licenses/LICENSE,sha256=gM207DhJjWrxLuEWXl0Qz5ISbtWDmADfjHp3yC2XISs,888
19
+ nettracer3d-0.7.8.dist-info/METADATA,sha256=GFylr691RVFXgRMrrEhOEW6ySL5vUW-8-bK8qQ4hr9A,4797
20
+ nettracer3d-0.7.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
21
+ nettracer3d-0.7.8.dist-info/entry_points.txt,sha256=Nx1rr_0QhJXDBHAQg2vcqCzLMKBzSHfwy3xwGkueVyc,53
22
+ nettracer3d-0.7.8.dist-info/top_level.txt,sha256=zsYy9rZwirfCEOubolhee4TyzqBAL5gSUeFMzhFTX8c,12
23
+ nettracer3d-0.7.8.dist-info/RECORD,,