nettracer3d 1.2.4__py3-none-any.whl → 1.2.7__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.
@@ -601,42 +601,35 @@ def _find_centroids_old(nodes, node_list = None, down_factor = None):
601
601
 
602
602
 
603
603
  def _find_centroids(nodes, node_list=None, down_factor=None):
604
- """Internal use version to get centroids without saving"""
605
-
604
+ """Parallel version using sum accumulation instead of storing coordinates"""
606
605
 
607
- def compute_indices_in_chunk(chunk, y_offset):
608
- """
609
- Alternative approach using np.where for even better performance on sparse arrays.
610
- """
611
- indices_dict_chunk = {}
606
+ def compute_sums_in_chunk(chunk, y_offset):
607
+ """Accumulate sums and counts - much less memory than storing coords"""
608
+ sums_dict = {}
609
+ counts_dict = {}
612
610
 
613
- # Get all coordinates where chunk is non-zero
614
611
  z_coords, y_coords, x_coords = np.where(chunk != 0)
615
612
 
616
613
  if len(z_coords) == 0:
617
- return indices_dict_chunk
614
+ return sums_dict, counts_dict
618
615
 
619
- # Adjust Y coordinates
620
616
  y_coords_adjusted = y_coords + y_offset
621
-
622
- # Get labels at these coordinates
623
617
  labels = chunk[z_coords, y_coords, x_coords]
624
-
625
- # Group by unique labels
626
618
  unique_labels = np.unique(labels)
627
619
 
628
620
  for label in unique_labels:
629
- if label == 0: # Skip background
621
+ if label == 0:
630
622
  continue
631
623
  mask = (labels == label)
632
- # Stack coordinates into the expected format [z, y, x]
633
- indices_dict_chunk[label] = np.column_stack((
634
- z_coords[mask],
635
- y_coords_adjusted[mask],
636
- x_coords[mask]
637
- ))
624
+ # Just store sums and counts - O(1) memory per label
625
+ sums_dict[label] = np.array([
626
+ z_coords[mask].sum(dtype=np.float64),
627
+ y_coords_adjusted[mask].sum(dtype=np.float64),
628
+ x_coords[mask].sum(dtype=np.float64)
629
+ ])
630
+ counts_dict[label] = mask.sum()
638
631
 
639
- return indices_dict_chunk
632
+ return sums_dict, counts_dict
640
633
 
641
634
  def chunk_3d_array(array, num_chunks):
642
635
  """Split the 3D array into smaller chunks along the y-axis."""
@@ -644,49 +637,44 @@ def _find_centroids(nodes, node_list=None, down_factor=None):
644
637
  return y_slices
645
638
 
646
639
  # Handle input processing
647
- if isinstance(nodes, str): # Open into numpy array if filepath
640
+ if isinstance(nodes, str):
648
641
  nodes = tifffile.imread(nodes)
649
- if len(np.unique(nodes)) == 2: # Label if binary
642
+ if len(np.unique(nodes)) == 2:
650
643
  structure_3d = np.ones((3, 3, 3), dtype=int)
651
644
  nodes, num_nodes = ndimage.label(nodes)
652
645
 
653
646
  if down_factor is not None:
654
647
  nodes = downsample(nodes, down_factor)
655
- else:
656
- down_factor = 1
657
648
 
658
- indices_dict = {}
649
+ sums_total = {}
650
+ counts_total = {}
659
651
  num_cpus = mp.cpu_count()
660
652
 
661
- # Chunk the 3D array along the y-axis
662
653
  node_chunks = chunk_3d_array(nodes, num_cpus)
663
-
664
- # Calculate Y offset for each chunk
665
654
  chunk_sizes = [chunk.shape[1] for chunk in node_chunks]
666
655
  y_offsets = np.cumsum([0] + chunk_sizes[:-1])
667
656
 
668
- # Parallel computation using the optimized single-pass approach
669
657
  with ThreadPoolExecutor(max_workers=num_cpus) as executor:
670
- futures = {executor.submit(compute_indices_in_chunk, chunk, y_offset): chunk_id
671
- for chunk_id, (chunk, y_offset) in enumerate(zip(node_chunks, y_offsets))}
658
+ futures = [executor.submit(compute_sums_in_chunk, chunk, y_offset)
659
+ for chunk, y_offset in zip(node_chunks, y_offsets)]
672
660
 
673
661
  for future in as_completed(futures):
674
- indices_chunk = future.result()
675
- # Merge indices for each label
676
- for label, indices in indices_chunk.items():
677
- if label in indices_dict:
678
- indices_dict[label] = np.vstack((indices_dict[label], indices))
662
+ sums_chunk, counts_chunk = future.result()
663
+
664
+ # Merge is now just addition - O(1) instead of vstack
665
+ for label in sums_chunk:
666
+ if label in sums_total:
667
+ sums_total[label] += sums_chunk[label]
668
+ counts_total[label] += counts_chunk[label]
679
669
  else:
680
- indices_dict[label] = indices
681
-
682
- # Compute centroids from collected indices
683
- centroid_dict = {}
684
- for label, indices in indices_dict.items():
685
- centroid = np.round(np.mean(indices, axis=0)).astype(int)
686
- centroid_dict[label] = centroid
687
-
688
- # Remove background label if it exists
689
- centroid_dict.pop(0, None)
670
+ sums_total[label] = sums_chunk[label]
671
+ counts_total[label] = counts_chunk[label]
672
+
673
+ # Compute centroids from accumulated sums
674
+ centroid_dict = {
675
+ label: np.round(sums_total[label] / counts_total[label]).astype(int)
676
+ for label in sums_total if label != 0
677
+ }
690
678
 
691
679
  return centroid_dict
692
680
 
@@ -264,20 +264,20 @@ def draw_network_from_centroids(nodes, network, centroids, twod_bool, directory
264
264
 
265
265
  if twod_bool:
266
266
  output_stack = output_stack[0,:,:] | output_stack[0,:,:]
267
-
268
- if directory is None:
269
- try:
270
- tifffile.imwrite("drawn_network.tif", output_stack)
271
- except Exception as e:
272
- print("Could not save network lattice to active directory")
273
- print("Network lattice saved as drawn_network.tif")
274
-
275
- if directory is not None:
276
- try:
277
- tifffile.imwrite(f"{directory}/drawn_network.tif", output_stack)
278
- print(f"Network lattice saved to {directory}/drawn_network.tif")
279
- except Exception as e:
280
- print(f"Could not save network lattice to {directory}")
267
+
268
+ #if directory is None:
269
+ # try:
270
+ # tifffile.imwrite("drawn_network.tif", output_stack)
271
+ # except Exception as e:
272
+ # print("Could not save network lattice to active directory")
273
+ # print("Network lattice saved as drawn_network.tif")
274
+
275
+ #if directory is not None:
276
+ # try:
277
+ # tifffile.imwrite(f"{directory}/drawn_network.tif", output_stack)
278
+ # print(f"Network lattice saved to {directory}/drawn_network.tif")
279
+ #except Exception as e:
280
+ # print(f"Could not save network lattice to {directory}")
281
281
 
282
282
  return output_stack
283
283
 
@@ -340,6 +340,7 @@ def draw_network_from_centroids_GPU(nodes, network, centroids, twod_bool, direct
340
340
 
341
341
  output_stack = cp.asnumpy(output_stack)
342
342
 
343
+ """
343
344
  if directory is None:
344
345
  try:
345
346
  tifffile.imwrite("drawn_network.tif", output_stack)
@@ -353,7 +354,7 @@ def draw_network_from_centroids_GPU(nodes, network, centroids, twod_bool, direct
353
354
  print(f"Network lattice saved to {directory}/drawn_network.tif")
354
355
  except Exception as e:
355
356
  print(f"Could not save network lattice to {directory}")
356
-
357
+ """
357
358
 
358
359
  if __name__ == '__main__':
359
360
 
nettracer3d/node_draw.py CHANGED
@@ -145,10 +145,10 @@ def draw_from_centroids(nodes, num_nodes, centroids, twod_bool, directory=None):
145
145
  draw_array = draw_array[0,:,:] | draw_array[1,:,:]
146
146
 
147
147
  filename = f'{directory}/labelled_node_indices.tif' if directory else 'labelled_node_indices.tif'
148
- try:
149
- tifffile.imwrite(filename, draw_array)
150
- except Exception as e:
151
- print(f"Could not save node indices to {filename}")
148
+ #try:
149
+ # tifffile.imwrite(filename, draw_array)
150
+ #except Exception as e:
151
+ # print(f"Could not save node indices to {filename}")
152
152
 
153
153
  return draw_array
154
154
 
nettracer3d/proximity.py CHANGED
@@ -86,7 +86,7 @@ def _get_node_node_dict(label_array, label, dilate_xy, dilate_z, fastdil = False
86
86
  def process_label(args):
87
87
  """Modified to use pre-computed bounding boxes instead of argwhere"""
88
88
  nodes, label, dilate_xy, dilate_z, array_shape, bounding_boxes = args
89
- print(f"Processing node {label}")
89
+ #print(f"Processing node {label}")
90
90
 
91
91
  # Get the pre-computed bounding box for this label
92
92
  slice_obj = bounding_boxes[int(label)-1] # -1 because label numbers start at 1
@@ -213,83 +213,9 @@ def populate_array(centroids, clip=False, shape = None):
213
213
  else:
214
214
  return array
215
215
 
216
- def _process_chunk_centroids(args):
217
- """Process a chunk of neighbor indices for centroids mode"""
218
- chunk_data, idx_to_node, query_indices, tree, points, max_neighbors = args
219
- output = []
220
-
221
- for i, neighbors in chunk_data:
222
- query_idx = query_indices[i]
223
- query_value = idx_to_node[query_idx]
224
- query_point = points[query_idx]
225
-
226
- # Filter out self-reference
227
- filtered_neighbors = [n for n in neighbors if n != query_idx]
228
-
229
- # If max_neighbors is specified and we have more neighbors than allowed
230
- if max_neighbors is not None and len(filtered_neighbors) > max_neighbors:
231
- # Use KDTree to get distances efficiently - query for more than we need
232
- # to ensure we get the exact closest ones
233
- k = min(len(filtered_neighbors), max_neighbors + 1) # +1 in case query point is included
234
- distances, indices = tree.query(query_point, k=k)
235
-
236
- # Filter out self and limit to max_neighbors
237
- selected_neighbors = []
238
- for dist, idx in zip(distances, indices):
239
- if idx != query_idx and idx in filtered_neighbors:
240
- selected_neighbors.append(idx)
241
- if len(selected_neighbors) >= max_neighbors:
242
- break
243
-
244
- filtered_neighbors = selected_neighbors
245
-
246
- # Add all selected neighbors to output
247
- for neighbor_idx in filtered_neighbors:
248
- neighbor_value = idx_to_node[neighbor_idx]
249
- output.append([query_value, neighbor_value, 0])
250
-
251
- return output
252
-
253
- def _process_chunk_array(args):
254
- """Process a chunk of neighbor indices for array mode"""
255
- chunk_data, array, point_tuples, query_indices, tree, points, max_neighbors = args
256
- output = []
257
-
258
- for i, neighbors in chunk_data:
259
- query_idx = query_indices[i]
260
- query_value = array[point_tuples[query_idx]]
261
- query_point = points[query_idx]
262
-
263
- # Filter out self-reference
264
- filtered_neighbors = [n for n in neighbors if n != query_idx]
265
-
266
- # If max_neighbors is specified and we have more neighbors than allowed
267
- if max_neighbors is not None and len(filtered_neighbors) > max_neighbors:
268
- # Use KDTree to get distances efficiently - query for more than we need
269
- # to ensure we get the exact closest ones
270
- k = min(len(filtered_neighbors), max_neighbors + 1) # +1 in case query point is included
271
- distances, indices = tree.query(query_point, k=k)
272
-
273
- # Filter out self and limit to max_neighbors
274
- selected_neighbors = []
275
- for dist, idx in zip(distances, indices):
276
- if idx != query_idx and idx in filtered_neighbors:
277
- selected_neighbors.append(idx)
278
- if len(selected_neighbors) >= max_neighbors:
279
- break
280
-
281
- filtered_neighbors = selected_neighbors
282
-
283
- # Add all selected neighbors to output
284
- for neighbor_idx in filtered_neighbors:
285
- neighbor_value = array[point_tuples[neighbor_idx]]
286
- output.append([query_value, neighbor_value, 0])
287
-
288
- return output
289
-
290
- def find_neighbors_kdtree(radius, centroids=None, array=None, targets=None, n_jobs=None, chunk_size=None, max_neighbors=None):
216
+ def find_neighbors_kdtree(radius, centroids=None, array=None, targets=None, max_neighbors=None):
291
217
  """
292
- Find neighbors using KDTree with optional parallelization.
218
+ Find neighbors using KDTree.
293
219
 
294
220
  Parameters:
295
221
  -----------
@@ -301,10 +227,6 @@ def find_neighbors_kdtree(radius, centroids=None, array=None, targets=None, n_jo
301
227
  Array to search for nonzero points
302
228
  targets : list, optional
303
229
  Specific targets to query for neighbors
304
- n_jobs : int, optional
305
- Number of parallel jobs. If None, uses cpu_count(). Set to 1 to disable parallelization.
306
- chunk_size : int, optional
307
- Size of chunks for parallel processing. If None, auto-calculated based on data size.
308
230
  max_neighbors : int, optional
309
231
  Maximum number of nearest neighbors to return per query point within the radius.
310
232
  If None, returns all neighbors within radius (original behavior).
@@ -378,81 +300,45 @@ def find_neighbors_kdtree(radius, centroids=None, array=None, targets=None, n_jo
378
300
 
379
301
  print("Sorting Through Output...")
380
302
 
381
- # Determine parallelization parameters
382
- if n_jobs is None:
383
- n_jobs = cpu_count()
384
-
385
- # Skip parallelization for small datasets or when n_jobs=1
386
- if n_jobs == 1 or len(neighbor_indices) < 100:
387
- #if True:
388
- # Sequential processing (original logic with max_neighbors support)
389
- output = []
390
- for i, neighbors in enumerate(neighbor_indices):
391
- query_idx = query_indices[i]
392
- query_point = points[query_idx]
393
-
394
- # Filter out self-reference
395
- filtered_neighbors = [n for n in neighbors if n != query_idx]
303
+ # Sequential processing
304
+ output = []
305
+ for i, neighbors in enumerate(neighbor_indices):
306
+ query_idx = query_indices[i]
307
+ query_point = points[query_idx]
308
+
309
+ # Filter out self-reference
310
+ filtered_neighbors = [n for n in neighbors if n != query_idx]
311
+
312
+ # If max_neighbors is specified and we have more neighbors than allowed
313
+ if max_neighbors is not None and len(filtered_neighbors) > max_neighbors:
314
+ # Use KDTree to get distances efficiently - query for more than we need
315
+ # to ensure we get the exact closest ones
316
+ k = min(len(filtered_neighbors), max_neighbors + 1) # +1 in case query point is included
317
+ distances, indices = tree.query(query_point, k=k)
396
318
 
397
- # If max_neighbors is specified and we have more neighbors than allowed
398
- if max_neighbors is not None and len(filtered_neighbors) > max_neighbors:
399
- # Use KDTree to get distances efficiently - query for more than we need
400
- # to ensure we get the exact closest ones
401
- k = min(len(filtered_neighbors), max_neighbors + 1) # +1 in case query point is included
402
- distances, indices = tree.query(query_point, k=k)
403
-
404
- # Filter out self and limit to max_neighbors
405
- selected_neighbors = []
406
- for dist, idx in zip(distances, indices):
407
- if idx != query_idx and idx in filtered_neighbors:
408
- selected_neighbors.append(idx)
409
- if len(selected_neighbors) >= max_neighbors:
410
- break
411
-
412
- filtered_neighbors = selected_neighbors
319
+ # Filter out self and limit to max_neighbors
320
+ selected_neighbors = []
321
+ for dist, idx in zip(distances, indices):
322
+ if idx != query_idx and idx in filtered_neighbors:
323
+ selected_neighbors.append(idx)
324
+ if len(selected_neighbors) >= max_neighbors:
325
+ break
413
326
 
414
- # Process the selected neighbors
415
- if centroids:
416
- query_value = idx_to_node[query_idx]
417
- for neighbor_idx in filtered_neighbors:
418
- neighbor_value = idx_to_node[neighbor_idx]
419
- output.append([query_value, neighbor_value, 0])
420
- else:
421
- query_value = array[point_tuples[query_idx]]
422
- for neighbor_idx in filtered_neighbors:
423
- neighbor_value = array[point_tuples[neighbor_idx]]
424
- output.append([query_value, neighbor_value, 0])
425
- return output
426
-
427
- # Parallel processing
428
- if chunk_size is None:
429
- # Auto-calculate chunk size: aim for ~4x more chunks than processes
430
- chunk_size = max(1, len(neighbor_indices) // (n_jobs * 4))
431
-
432
- # Create chunks of (index, neighbors) pairs
433
- chunks = []
434
- for i in range(0, len(neighbor_indices), chunk_size):
435
- chunk = [(j, neighbor_indices[j]) for j in range(i, min(i + chunk_size, len(neighbor_indices)))]
436
- chunks.append(chunk)
437
-
438
- # Process chunks in parallel
439
- with Pool(processes=n_jobs) as pool:
327
+ filtered_neighbors = selected_neighbors
328
+
329
+ # Process the selected neighbors
440
330
  if centroids:
441
- # Prepare arguments for centroids mode
442
- chunk_args = [(chunk, idx_to_node, query_indices, tree, points, max_neighbors) for chunk in chunks]
443
- chunk_results = pool.map(_process_chunk_centroids, chunk_args)
331
+ query_value = idx_to_node[query_idx]
332
+ for neighbor_idx in filtered_neighbors:
333
+ neighbor_value = idx_to_node[neighbor_idx]
334
+ output.append([query_value, neighbor_value, 0])
444
335
  else:
445
- # Prepare arguments for array mode
446
- chunk_args = [(chunk, array, point_tuples, query_indices, tree, points, max_neighbors) for chunk in chunks]
447
- chunk_results = pool.map(_process_chunk_array, chunk_args)
448
-
449
- # Flatten results
450
- output = []
451
- for chunk_result in chunk_results:
452
- output.extend(chunk_result)
336
+ query_value = array[point_tuples[query_idx]]
337
+ for neighbor_idx in filtered_neighbors:
338
+ neighbor_value = array[point_tuples[neighbor_idx]]
339
+ output.append([query_value, neighbor_value, 0])
453
340
 
454
341
  print("Organizing Network...")
455
-
456
342
 
457
343
  return output
458
344
 
@@ -119,7 +119,7 @@ def geometric_positions(centroids, shape):
119
119
  return xy_pos, z_pos
120
120
 
121
121
 
122
- def show_simple_network(excel_file_path, geometric = False, geo_info = None, directory = None):
122
+ def show_simple_network(excel_file_path, geometric = False, geo_info = None, directory = None, show_labels = True):
123
123
 
124
124
  if type(excel_file_path) == str:
125
125
  master_list = read_excel_to_lists(excel_file_path)
@@ -142,11 +142,11 @@ def show_simple_network(excel_file_path, geometric = False, geo_info = None, dir
142
142
 
143
143
  pos, z_pos = geometric_positions(geo_info[0], geo_info[1])
144
144
  node_sizes_list = [z_pos[node] for node in G.nodes()]
145
- nx.draw(G, pos, with_labels=True, font_color='black', font_weight='bold', node_size= node_sizes_list, alpha=0.8, font_size = 12)
145
+ nx.draw(G, pos, with_labels=show_labels, font_color='black', font_weight='bold', node_size= node_sizes_list, alpha=0.8, font_size = 12)
146
146
  else:
147
147
  # Visualize the graph with different edge colors for each community
148
148
  pos = nx.spring_layout(G, iterations = 15)
149
- nx.draw(G, pos, with_labels=True, font_color='red', font_weight='bold', node_size=10)
149
+ nx.draw(G, pos, with_labels=show_labels, font_color='red', font_weight='bold', node_size=10)
150
150
 
151
151
  if directory is not None:
152
152
  plt.savefig(f'{directory}/network_plot.png')
@@ -154,7 +154,7 @@ def show_simple_network(excel_file_path, geometric = False, geo_info = None, dir
154
154
  plt.show()
155
155
 
156
156
 
157
- def show_identity_network(excel_file_path, node_identities, geometric=False, geo_info=None, directory=None):
157
+ def show_identity_network(excel_file_path, node_identities, geometric=False, geo_info=None, directory=None, show_labels = True):
158
158
  if type(node_identities) == str:
159
159
  # Read the Excel file into a DataFrame
160
160
  df = pd.read_excel(node_identities)
@@ -220,9 +220,16 @@ def show_identity_network(excel_file_path, node_identities, geometric=False, geo
220
220
  color_map = dict(zip(unique_categories, colors))
221
221
 
222
222
  # Node size handling
223
- node_dict = {node: 30 if identity_dict[node] == 'Edge' else 100
224
- for node in G.nodes()}
225
-
223
+ node_dict = {}
224
+ for node in G.nodes():
225
+ try:
226
+ if identity_dict[node] == 'Edge':
227
+ node_dict[node] = 30
228
+ else:
229
+ node_dict[node] = 100
230
+ except:
231
+ node_dict[node] = 100
232
+
226
233
  if geometric:
227
234
  # Handle geometric positioning
228
235
  for node in list(G.nodes()):
@@ -244,14 +251,26 @@ def show_identity_network(excel_file_path, node_identities, geometric=False, geo
244
251
  graph_ax = plt.gca()
245
252
 
246
253
  # Draw the network with enhanced font styling
247
- node_colors = [color_map[identity_dict[node]] for node in G.nodes()]
248
- nx.draw(G, pos, ax=graph_ax, with_labels=True, font_color='black',
254
+ misc = False
255
+ node_colors = []
256
+ for node in G.nodes():
257
+ try:
258
+ node_colors.append(color_map[identity_dict[node]])
259
+ except:
260
+ misc = True
261
+ node_colors.append((1, 1, 1))
262
+
263
+ #node_colors = [color_map[identity_dict[node]] for node in G.nodes()]
264
+ nx.draw(G, pos, ax=graph_ax, with_labels=show_labels, font_color='black',
249
265
  font_weight='bold', node_size=node_sizes_list,
250
266
  node_color=node_colors, alpha=0.8, font_size=11, font_family='sans-serif')
251
267
 
252
268
  # Create custom legend with multiple columns if needed
253
269
  legend_handles = [Patch(color=color, label=category)
254
270
  for category, color in color_map.items()]
271
+
272
+ if misc:
273
+ legend_handles.append(Patch(color = (1, 1, 1,), label = 'Unassigned'))
255
274
 
256
275
  # Adjust number of columns based on number of categories
257
276
  if len(unique_categories) > 20: