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

@@ -549,54 +549,117 @@ def convert_node_colors_to_names(node_to_color: Dict[int, Tuple[int, int, int]],
549
549
 
550
550
  num_entries = len(node_to_color)
551
551
 
552
- # Calculate dynamic spacing based on number of entries
553
- entry_height = 0.8
554
- total_height = num_entries * entry_height + 1.5 # Extra space for title and margins
552
+ # Calculate text widths to determine optimal figure size
553
+ sorted_nodes = sorted(node_to_color.keys())
554
+
555
+ # Create a temporary figure to measure text widths
556
+ temp_fig, temp_ax = plt.subplots(figsize=(1, 1))
557
+
558
+ max_node_width = 0
559
+ max_color_width = 0
560
+
561
+ for node in sorted_nodes:
562
+ color_name = node_to_names[node]
563
+
564
+ # Measure node ID text width
565
+ node_text = temp_ax.text(0, 0, str(node), fontsize=12, fontweight='bold')
566
+ node_bbox = node_text.get_window_extent(renderer=temp_fig.canvas.get_renderer())
567
+ node_width = node_bbox.width
568
+ max_node_width = max(max_node_width, node_width)
569
+
570
+ # Measure color name text width
571
+ color_text = temp_ax.text(0, 0, color_name.replace('_', ' ').title(), fontsize=11)
572
+ color_bbox = color_text.get_window_extent(renderer=temp_fig.canvas.get_renderer())
573
+ color_width = color_bbox.width
574
+ max_color_width = max(max_color_width, color_width)
575
+
576
+ plt.close(temp_fig)
577
+
578
+ # Convert pixel widths to figure units (approximate conversion)
579
+ # This is a rough conversion - matplotlib uses 72 DPI by default
580
+ dpi = 72
581
+ max_node_width_fig = max_node_width / dpi
582
+ max_color_width_fig = max_color_width / dpi
583
+
584
+ # Calculate optimal figure dimensions
585
+ entry_height = 0.6 # Reduced for tighter spacing
586
+ margin = 0.3
587
+ swatch_width = 0.8
588
+ spacing = 0.2
589
+
590
+ # Calculate total width needed
591
+ total_width = (margin + max_node_width_fig + spacing +
592
+ swatch_width + spacing + max_color_width_fig + margin)
555
593
 
556
- # Create figure and axis with proper scaling
557
- fig, ax = plt.subplots(figsize=figsize)
558
- ax.set_xlim(0, 10)
594
+ # Ensure minimum width for readability
595
+ total_width = max(total_width, 4.0)
596
+
597
+ # Calculate total height
598
+ title_height = 0.8
599
+ total_height = num_entries * entry_height + title_height + 2 * margin
600
+
601
+ # Create the actual figure with calculated dimensions
602
+ fig, ax = plt.subplots(figsize=(total_width, total_height))
603
+
604
+ # Set axis limits to match our calculated dimensions
605
+ ax.set_xlim(0, total_width)
559
606
  ax.set_ylim(0, total_height)
560
607
  ax.axis('off')
561
608
 
562
609
  # Title
563
- ax.text(5, total_height - 0.5, 'Color Legend',
564
- fontsize=16, fontweight='bold', ha='center')
565
-
566
- # Sort nodes for consistent display
567
- sorted_nodes = sorted(node_to_color.keys())
610
+ ax.text(total_width/2, total_height - margin - 0.2, 'Color Legend',
611
+ fontsize=14, fontweight='bold', ha='center', va='top')
568
612
 
569
613
  # Create legend entries
570
614
  for i, node in enumerate(sorted_nodes):
571
- y_pos = total_height - (i + 1) * entry_height - 0.8
615
+ y_pos = total_height - title_height - margin - (i + 1) * entry_height + entry_height/2
572
616
  rgb = node_to_color[node]
573
617
  color_name = node_to_names[node]
574
618
 
575
619
  # Normalize RGB values for matplotlib (0-1 range)
576
620
  norm_rgb = tuple(c/255.0 for c in rgb)
577
621
 
578
- # Draw color swatch (using actual RGB values)
579
- swatch = Rectangle((1.0, y_pos - 0.15), 0.8, 0.3,
580
- facecolor=norm_rgb, edgecolor='black', linewidth=1)
581
- ax.add_patch(swatch)
622
+ # Position calculations
623
+ node_x = margin
624
+ swatch_x = margin + max_node_width_fig + spacing
625
+ color_x = swatch_x + swatch_width + spacing
582
626
 
583
- # Node ID (exactly as it appears in dict keys)
584
- ax.text(0.2, y_pos, str(node), fontsize=12, fontweight='bold',
627
+ # Node ID (left-aligned)
628
+ ax.text(node_x, y_pos, str(node), fontsize=12, fontweight='bold',
585
629
  va='center', ha='left')
586
630
 
587
- # Color name (mapped name, nicely formatted)
588
- ax.text(2.2, y_pos, color_name.replace('_', ' ').title(),
631
+ # Draw color swatch
632
+ swatch_y = y_pos - entry_height/4
633
+ swatch = Rectangle((swatch_x, swatch_y), swatch_width, entry_height/2,
634
+ facecolor=norm_rgb, edgecolor='black', linewidth=1)
635
+ ax.add_patch(swatch)
636
+
637
+ # Color name
638
+ formatted_name = color_name.replace('_', ' ').title()
639
+ # Truncate very long color names to prevent layout issues
640
+ if len(formatted_name) > 25:
641
+ formatted_name = formatted_name[:22] + "..."
642
+
643
+ ax.text(color_x, y_pos, formatted_name,
589
644
  fontsize=11, va='center', ha='left')
590
645
 
591
- # Add border around the legend
592
- border = Rectangle((0.1, 0.1), 9.8, total_height - 0.2,
593
- fill=False, edgecolor='gray', linewidth=2)
646
+ # Add a subtle border around the entire legend
647
+ border_margin = 0.1
648
+ border = Rectangle((border_margin, border_margin),
649
+ total_width - 2*border_margin,
650
+ total_height - 2*border_margin,
651
+ fill=False, edgecolor='lightgray', linewidth=1.5)
594
652
  ax.add_patch(border)
595
653
 
596
- plt.tight_layout()
654
+ # Remove any extra whitespace
655
+ plt.tight_layout(pad=0.1)
656
+
657
+ # Adjust the figure to eliminate whitespace
658
+ ax.margins(0)
659
+ fig.subplots_adjust(left=0, right=1, top=1, bottom=0)
597
660
 
598
661
  if save_path:
599
- plt.savefig(save_path, dpi=300, bbox_inches='tight')
662
+ plt.savefig(save_path, dpi=300, bbox_inches='tight', pad_inches=0.05)
600
663
 
601
664
  plt.show()
602
665
 
nettracer3d/nettracer.py CHANGED
@@ -1267,31 +1267,6 @@ def dilate_2D(array, search, scaling = 1):
1267
1267
 
1268
1268
  return inv
1269
1269
 
1270
- def erode_2D(array, search, scaling=1):
1271
- """
1272
- Erode a 2D array using distance transform method.
1273
-
1274
- Parameters:
1275
- array -- Input 2D binary array
1276
- search -- Distance within which to erode
1277
- scaling -- Scaling factor (default: 1)
1278
-
1279
- Returns:
1280
- Eroded 2D array
1281
- """
1282
- # For erosion, we work directly with the foreground
1283
- # No need to invert the array
1284
-
1285
- # Compute distance transform on the foreground
1286
- dt = smart_dilate.compute_distance_transform_distance(array)
1287
-
1288
- # Apply scaling
1289
- dt = dt * scaling
1290
-
1291
- # Threshold to keep only points that are at least 'search' distance from the boundary
1292
- eroded = dt >= search
1293
-
1294
- return eroded
1295
1270
 
1296
1271
  def dilate_3D_dt(array, search_distance, xy_scaling=1.0, z_scaling=1.0):
1297
1272
  """
@@ -1353,7 +1328,42 @@ def dilate_3D_dt(array, search_distance, xy_scaling=1.0, z_scaling=1.0):
1353
1328
 
1354
1329
  return inv.astype(np.uint8)
1355
1330
 
1356
- def erode_3D_dt(array, search_distance, xy_scaling=1.0, z_scaling=1.0):
1331
+ def erode_2D(array, search, scaling=1, preserve_labels = False):
1332
+ """
1333
+ Erode a 2D array using distance transform method.
1334
+
1335
+ Parameters:
1336
+ array -- Input 2D binary array
1337
+ search -- Distance within which to erode
1338
+ scaling -- Scaling factor (default: 1)
1339
+
1340
+ Returns:
1341
+ Eroded 2D array
1342
+ """
1343
+ # For erosion, we work directly with the foreground
1344
+ # No need to invert the array
1345
+
1346
+ if preserve_labels:
1347
+ from skimage.segmentation import find_boundaries
1348
+ borders = find_boundaries(array, mode='thick')
1349
+ mask = array * invert_array(borders)
1350
+ mask = smart_dilate.compute_distance_transform_distance(mask)
1351
+ mask = mask * scaling
1352
+ mask = mask >= search
1353
+ array = mask * array
1354
+ else:
1355
+ # Compute distance transform on the foreground
1356
+ dt = smart_dilate.compute_distance_transform_distance(array)
1357
+
1358
+ # Apply scaling
1359
+ dt = dt * scaling
1360
+
1361
+ # Threshold to keep only points that are at least 'search' distance from the boundary
1362
+ array = dt > search
1363
+
1364
+ return array
1365
+
1366
+ def erode_3D_dt(array, search_distance, xy_scaling=1.0, z_scaling=1.0, preserve_labels = False):
1357
1367
  """
1358
1368
  Erode a 3D array using distance transform method. DT erosion produces perfect results
1359
1369
  with Euclidean geometry, but may be slower for large arrays.
@@ -1371,43 +1381,24 @@ def erode_3D_dt(array, search_distance, xy_scaling=1.0, z_scaling=1.0):
1371
1381
 
1372
1382
  if array.shape[0] == 1:
1373
1383
  # Handle 2D case
1374
- return erode_2D(array, search_distance, scaling=xy_scaling)
1375
-
1376
- # For erosion, we work directly with the foreground (no inversion needed)
1377
-
1378
- """
1379
- # Determine which dimension needs resampling
1380
- if (z_scaling > xy_scaling):
1381
- # Z dimension needs to be stretched
1382
- zoom_factor = [z_scaling/xy_scaling, 1, 1] # Scale factor for [z, y, x]
1383
- rev_factor = [xy_scaling/z_scaling, 1, 1]
1384
- cardinal = xy_scaling
1385
- elif (xy_scaling > z_scaling):
1386
- # XY dimensions need to be stretched
1387
- zoom_factor = [1, xy_scaling/z_scaling, xy_scaling/z_scaling] # Scale factor for [z, y, x]
1388
- rev_factor = [1, z_scaling/xy_scaling, z_scaling/xy_scaling] # Scale factor for [z, y, x]
1389
- cardinal = z_scaling
1390
- else:
1391
- # Already uniform scaling, no need to resample
1392
- zoom_factor = None
1393
- rev_factor = None
1394
- cardinal = xy_scaling
1384
+ return erode_2D(array, search_distance, scaling=xy_scaling, preserve_labels = True)
1395
1385
 
1396
- # Resample the mask if needed
1397
- if zoom_factor:
1398
- array = ndimage.zoom(array, zoom_factor, order=0) # Use order=0 for binary masks
1399
- """
1400
-
1401
- print("Computing a distance transform for a perfect erosion...")
1402
1386
 
1403
- array = smart_dilate.compute_distance_transform_distance(array, sampling = [z_scaling, xy_scaling, xy_scaling])
1404
-
1405
- # Apply scaling factor
1406
- #array = array * cardinal
1407
-
1408
- # Threshold the distance transform to get eroded result
1409
- # For erosion, we keep only the points that are at least search_distance from the boundary
1410
- array = array >= search_distance
1387
+ if preserve_labels:
1388
+
1389
+
1390
+ from skimage.segmentation import find_boundaries
1391
+
1392
+ borders = find_boundaries(array, mode='thick')
1393
+ mask = array * invert_array(borders)
1394
+ mask = smart_dilate.compute_distance_transform_distance(mask, sampling = [z_scaling, xy_scaling, xy_scaling])
1395
+ mask = mask >= search_distance
1396
+ array = mask * array
1397
+ else:
1398
+ array = smart_dilate.compute_distance_transform_distance(array, sampling = [z_scaling, xy_scaling, xy_scaling])
1399
+ # Threshold the distance transform to get eroded result
1400
+ # For erosion, we keep only the points that are at least search_distance from the boundary
1401
+ array = array > search_distance
1411
1402
 
1412
1403
  # Resample back to original dimensions if needed
1413
1404
  #if rev_factor:
@@ -1574,7 +1565,7 @@ def dilate_3D_old(tiff_array, dilated_x=3, dilated_y=3, dilated_z=3):
1574
1565
 
1575
1566
 
1576
1567
  def erode_3D(tiff_array, eroded_x, eroded_y, eroded_z):
1577
- """Internal method to erode an array in 3D. Erosion this way is much faster than using a distance transform although the latter is theoretically more accurate.
1568
+ """Internal method to erode an array in 3D. Erosion this way is faster than using a distance transform although the latter is theoretically more accurate.
1578
1569
  Arguments are an array, and the desired pixel erosion amounts in X, Y, Z."""
1579
1570
 
1580
1571
  if tiff_array.shape[0] == 1:
@@ -2124,15 +2115,15 @@ def dilate(arrayimage, amount, xy_scale = 1, z_scale = 1, directory = None, fast
2124
2115
 
2125
2116
  return arrayimage
2126
2117
 
2127
- def erode(arrayimage, amount, xy_scale = 1, z_scale = 1, mode = 0):
2128
- if len(np.unique(arrayimage)) > 2: #binarize
2118
+ def erode(arrayimage, amount, xy_scale = 1, z_scale = 1, mode = 0, preserve_labels = False):
2119
+ if not preserve_labels and len(np.unique(arrayimage)) > 2: #binarize
2129
2120
  arrayimage = binarize(arrayimage)
2130
2121
  erode_xy, erode_z = dilation_length_to_pixels(xy_scale, z_scale, amount, amount)
2131
2122
 
2132
2123
  if mode == 0:
2133
2124
  arrayimage = (erode_3D(arrayimage, erode_xy, erode_xy, erode_z)) * 255
2134
2125
  else:
2135
- arrayimage = erode_3D_dt(arrayimage, amount, xy_scaling=xy_scale, z_scaling=z_scale)
2126
+ arrayimage = erode_3D_dt(arrayimage, amount, xy_scaling=xy_scale, z_scaling=z_scale, preserve_labels = preserve_labels)
2136
2127
 
2137
2128
  if np.max(arrayimage) == 1:
2138
2129
  arrayimage = arrayimage * 255
@@ -3847,7 +3838,7 @@ class Network_3D:
3847
3838
 
3848
3839
  self._search_region = self._nodes
3849
3840
 
3850
- def calculate_edges(self, binary_edges, diledge = None, inners = True, hash_inner_edges = True, search = None, remove_edgetrunk = 0, GPU = True, fast_dil = False, skeletonized = False):
3841
+ def calculate_edges(self, binary_edges, diledge = None, inners = True, search = None, remove_edgetrunk = 0, GPU = True, fast_dil = False, skeletonized = False):
3851
3842
  """
3852
3843
  Method to calculate the edges that are used to directly connect nodes. May be done with or without the search region, however using search_region is recommended.
3853
3844
  The search_region property must be set to use the search region, otherwise the nodes property must be set. Sets the edges property
@@ -3856,7 +3847,6 @@ class Network_3D:
3856
3847
  so some amount of dilation is recommended if there are any, but not so much to create overconnectivity. This is a value that needs to be tuned by the user.
3857
3848
  :param inners: (Optional - Val = True; boolean). Will use inner edges if True, will not if False. Inner edges are parts of the edge mask that exist within search regions. If search regions overlap,
3858
3849
  any edges that exist within the overlap will only assert connectivity if 'inners' is True.
3859
- :param hash_inner_edges: (Optional - Val = True; boolean). If False, all search regions that contain an edge object connecting multiple nodes will be assigned as connected.
3860
3850
  If True, an extra processing step is used to sort the correct connectivity amongst these search_regions. Can only be computed when search_regions property is set.
3861
3851
  :param search: (Optional - Val = None; int). Amount for nodes to search for connections, assuming the search_regions are not being used. Assigning a value to this param will utilize the secondary algorithm and not the search_regions.
3862
3852
  :param remove_edgetrunk: (Optional - Val = 0; int). Amount of times to remove the 'Trunk' from the edges. A trunk in this case is the largest (by vol) edge object remaining after nodes have broken up the edges.
@@ -3909,11 +3899,7 @@ class Network_3D:
3909
3899
  labelled_edges, num_edge = label_objects(outer_edges)
3910
3900
 
3911
3901
  if inners:
3912
-
3913
- if search is None and hash_inner_edges is True:
3914
- inner_edges = hash_inners(self._search_region, binary_edges, GPU = GPU)
3915
- else:
3916
- inner_edges = establish_inner_edges(search_region, binary_edges)
3902
+ inner_edges = hash_inners(self._search_region, binary_edges, GPU = GPU)
3917
3903
 
3918
3904
  del binary_edges
3919
3905
 
@@ -4045,7 +4031,7 @@ class Network_3D:
4045
4031
  self._network_lists = network_analysis.read_excel_to_lists(df)
4046
4032
  self._network, net_weights = network_analysis.weighted_network(df)
4047
4033
 
4048
- def calculate_all(self, nodes, edges, xy_scale = 1, z_scale = 1, down_factor = None, search = None, diledge = None, inners = True, hash_inners = True, remove_trunk = 0, ignore_search_region = False, other_nodes = None, label_nodes = True, directory = None, GPU = True, fast_dil = True, skeletonize = False, GPU_downsample = None):
4034
+ def calculate_all(self, nodes, edges, xy_scale = 1, z_scale = 1, down_factor = None, search = None, diledge = None, inners = True, remove_trunk = 0, ignore_search_region = False, other_nodes = None, label_nodes = True, directory = None, GPU = True, fast_dil = True, skeletonize = False, GPU_downsample = None):
4049
4035
  """
4050
4036
  Method to calculate and save to mem all properties of a Network_3D object. In general, after initializing a Network_3D object, this method should be called on the node and edge masks that will be used to calculate the network.
4051
4037
  :param nodes: (Mandatory; String or ndarray). Filepath to segmented nodes mask or a numpy array containing the same.
@@ -4058,7 +4044,6 @@ class Network_3D:
4058
4044
  so some amount of dilation is recommended if there are any, but not so much to create overconnectivity. This is a value that needs to be tuned by the user.
4059
4045
  :param inners: (Optional - Val = True; boolean). Will use inner edges if True, will not if False. Inner edges are parts of the edge mask that exist within search regions. If search regions overlap,
4060
4046
  any edges that exist within the overlap will only assert connectivity if 'inners' is True.
4061
- :param hash_inners: (Optional - Val = True; boolean). If False, all search regions that contain an edge object connecting multiple nodes will be assigned as connected.
4062
4047
  If True, an extra processing step is used to sort the correct connectivity amongst these search_regions. Can only be computed when search_regions property is set.
4063
4048
  :param remove_trunk: (Optional - Val = 0; int). Amount of times to remove the 'Trunk' from the edges. A trunk in this case is the largest (by vol) edge object remaining after nodes have broken up the edges.
4064
4049
  Any 'Trunks' removed will be absent for connection calculations.
@@ -4120,7 +4105,7 @@ class Network_3D:
4120
4105
  except:
4121
4106
  pass
4122
4107
 
4123
- self.calculate_edges(edges, diledge = diledge, inners = inners, hash_inner_edges = hash_inners, search = search, remove_edgetrunk = remove_trunk, GPU = GPU, fast_dil = fast_dil, skeletonized = skeletonize) #Will have to be moved out if the second method becomes more directly implemented
4108
+ self.calculate_edges(edges, diledge = diledge, inners = inners, search = search, remove_edgetrunk = remove_trunk, GPU = GPU, fast_dil = fast_dil, skeletonized = skeletonize) #Will have to be moved out if the second method becomes more directly implemented
4124
4109
  else:
4125
4110
  self._edges, _ = label_objects(edges)
4126
4111
 
@@ -5104,13 +5089,16 @@ class Network_3D:
5104
5089
 
5105
5090
 
5106
5091
  for node in G.nodes():
5107
- nodeid = node_identities[node]
5108
- neighbors = list(G.neighbors(node))
5109
- for subnode in neighbors:
5110
- subnodeid = node_identities[subnode]
5111
- if subnodeid == root:
5112
- neighborhood_dict[nodeid] += 1
5113
- break
5092
+ try:
5093
+ nodeid = node_identities[node]
5094
+ neighbors = list(G.neighbors(node))
5095
+ for subnode in neighbors:
5096
+ subnodeid = node_identities[subnode]
5097
+ if subnodeid == root:
5098
+ neighborhood_dict[nodeid] += 1
5099
+ break
5100
+ except:
5101
+ pass
5114
5102
 
5115
5103
  title1 = f'Neighborhood Distribution of Nodes in Network from Nodes: {root}'
5116
5104
  title2 = f'Neighborhood Distribution of Nodes in Network from Nodes {root} as a proportion of total nodes of that ID'
@@ -5219,34 +5207,22 @@ class Network_3D:
5219
5207
  bounds = (min_coords, max_coords)
5220
5208
  dim_list = max_coords - min_coords
5221
5209
 
5222
-
5223
- if dim == 3:
5224
-
5225
- """
5226
- if self.xy_scale > self.z_scale: # xy will be 'expanded' more so its components will be arbitrarily further from the border than z ones
5227
- factor_xy = (self.z_scale/self.xy_scale) * factor # So 'factor' in the xy dim has to get smaller
5228
- factor_z = factor
5229
- elif self.z_scale > self.xy_scale: # Same idea
5230
- factor_z = (self.xy_scale/self.z_scale) * factor
5231
- factor_xy = factor
5232
- else:
5233
- factor_z = factor
5234
- factor_xy = factor
5235
- """
5236
-
5237
- for centroid in roots:
5238
-
5239
- if ((centroid[2] - min_coords[0]) > dim_list[0] * factor) and ((max_coords[0] - centroid[2]) > dim_list[0] * factor) and ((centroid[1] - min_coords[1]) > dim_list[1] * factor) and ((max_coords[1] - centroid[1]) > dim_list[1] * factor) and ((centroid[0] - min_coords[2]) > dim_list[2] * factor) and ((max_coords[2] - centroid[0]) > dim_list[2] * factor):
5210
+ for centroid in roots:
5211
+ # Assuming centroid is [z, y, x] based on your indexing
5212
+ z, y, x = centroid[0], centroid[1], centroid[2]
5213
+
5214
+ # Check x-dimension
5215
+ x_ok = (x - min_coords[0]) > dim_list[0] * factor and (max_coords[0] - x) > dim_list[0] * factor
5216
+ # Check y-dimension
5217
+ y_ok = (y - min_coords[1]) > dim_list[1] * factor and (max_coords[1] - y) > dim_list[1] * factor
5218
+
5219
+ if dim == 3: # 3D case
5220
+ # Check z-dimension
5221
+ z_ok = (z - min_coords[2]) > dim_list[2] * factor and (max_coords[2] - z) > dim_list[2] * factor
5222
+ if x_ok and y_ok and z_ok:
5240
5223
  new_list.append(centroid)
5241
-
5242
-
5243
- #if ((centroid[2] - min_coords[0]) > dim_list[0] * factor) and ((max_coords[0] - centroid[2]) > dim_list[0] * factor) and ((centroid[1] - min_coords[1]) > dim_list[1] * factor) and ((max_coords[1] - centroid[1]) > dim_list[1] * factor) and ((centroid[0] - min_coords[2]) > dim_list[2] * factor) and ((max_coords[2] - centroid[0]) > dim_list[2] * factor):
5244
- #new_list.append(centroid)
5245
- #print(f"dim_list: {dim_list}, centroid: {centroid}, min_coords: {min_coords}, max_coords: {max_coords}")
5246
- else:
5247
- for centroid in roots:
5248
-
5249
- if ((centroid[2] - min_coords[0]) > dim_list[0] * factor) and ((max_coords[0] - centroid[2]) > dim_list[0] * factor) and ((centroid[1] - min_coords[1]) > dim_list[1] * factor) and ((max_coords[1] - centroid[1]) > dim_list[1] * factor):
5224
+ else: # 2D case
5225
+ if x_ok and y_ok:
5250
5226
  new_list.append(centroid)
5251
5227
 
5252
5228
  else:
@@ -5299,8 +5275,6 @@ class Network_3D:
5299
5275
  print(f"Utilizing {len(roots)} root points. Note that low n values are unstable.")
5300
5276
  is_subset = True
5301
5277
 
5302
-
5303
-
5304
5278
  roots = proximity.convert_centroids_to_array(roots, xy_scale = self.xy_scale, z_scale = self.z_scale)
5305
5279
 
5306
5280
  n_subset = len(targs)
@@ -5608,7 +5582,10 @@ class Network_3D:
5608
5582
  for iden in idens:
5609
5583
  counter[id_dict[iden]] += 1
5610
5584
  except:
5611
- counter[id_dict[self.node_identities[node]]] += 1 # Keep them as arrays
5585
+ try:
5586
+ counter[id_dict[self.node_identities[node]]] += 1 # Keep them as arrays
5587
+ except:
5588
+ pass
5612
5589
 
5613
5590
  for i in range(len(counter)): # Translate them into proportions out of 1
5614
5591
 
@@ -5645,7 +5622,10 @@ class Network_3D:
5645
5622
  for iden in idents:
5646
5623
  iden_tracker[iden] += 1
5647
5624
  except:
5648
- iden_tracker[self.node_identities[node]] += 1
5625
+ try:
5626
+ iden_tracker[self.node_identities[node]] += 1
5627
+ except:
5628
+ pass
5649
5629
 
5650
5630
  i = 0
5651
5631