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

@@ -202,10 +202,12 @@ 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"):
205
+ def create_community_heatmap(community_intensity, node_community, node_centroids, shape=None, is_3d=True,
206
+ labeled_array=None, figsize=(12, 8), point_size=50, alpha=0.7,
207
+ colorbar_label="Community Intensity", title="Community Intensity Heatmap"):
207
208
  """
208
209
  Create a 2D or 3D heatmap showing nodes colored by their community intensities.
210
+ Can return either matplotlib plot or numpy RGB array for overlay purposes.
209
211
 
210
212
  Parameters:
211
213
  -----------
@@ -220,25 +222,39 @@ def create_community_heatmap(community_intensity, node_community, node_centroids
220
222
  Dictionary mapping node IDs to centroids
221
223
  Centroids should be [Z, Y, X] for 3D or [1, Y, X] for pseudo-3D
222
224
 
225
+ shape : tuple, optional
226
+ Shape of the output array in [Z, Y, X] format
227
+ If None, will be inferred from node_centroids
228
+
223
229
  is_3d : bool, default=True
224
- If True, create 3D plot. If False, create 2D plot.
230
+ If True, create 3D plot/array. If False, create 2D plot/array.
231
+
232
+ labeled_array : np.ndarray, optional
233
+ If provided, returns numpy RGB array overlay using this labeled array template
234
+ instead of matplotlib plot. Uses lookup table approach for efficiency.
225
235
 
226
236
  figsize : tuple, default=(12, 8)
227
- Figure size (width, height)
237
+ Figure size (width, height) - only used for matplotlib
228
238
 
229
239
  point_size : int, default=50
230
- Size of scatter plot points
240
+ Size of scatter plot points - only used for matplotlib
231
241
 
232
242
  alpha : float, default=0.7
233
- Transparency of points (0-1)
243
+ Transparency of points (0-1) - only used for matplotlib
234
244
 
235
245
  colorbar_label : str, default="Community Intensity"
236
- Label for the colorbar
246
+ Label for the colorbar - only used for matplotlib
247
+
248
+ title : str, default="Community Intensity Heatmap"
249
+ Title for the plot
237
250
 
238
251
  Returns:
239
252
  --------
240
- fig, ax : matplotlib figure and axis objects
253
+ If labeled_array is None: fig, ax (matplotlib figure and axis objects)
254
+ If labeled_array is provided: np.ndarray (RGB heatmap array with community intensity colors)
241
255
  """
256
+ import numpy as np
257
+ import matplotlib.pyplot as plt
242
258
 
243
259
  # Convert numpy int64 keys to regular ints for consistency
244
260
  community_intensity_clean = {}
@@ -254,6 +270,10 @@ def create_community_heatmap(community_intensity, node_community, node_centroids
254
270
 
255
271
  for node_id, centroid in node_centroids.items():
256
272
  try:
273
+ # Convert node_id to regular int if it's numpy
274
+ if hasattr(node_id, 'item'):
275
+ node_id = node_id.item()
276
+
257
277
  # Get community for this node
258
278
  community_id = node_community[node_id]
259
279
 
@@ -266,74 +286,392 @@ def create_community_heatmap(community_intensity, node_community, node_centroids
266
286
 
267
287
  node_positions.append(centroid)
268
288
  node_intensities.append(intensity)
269
- except:
289
+ except KeyError:
290
+ # Skip nodes that don't have community assignments or community intensities
270
291
  pass
271
292
 
272
293
  # Convert to numpy arrays
273
294
  positions = np.array(node_positions)
274
295
  intensities = np.array(node_intensities)
275
296
 
276
- # Determine min and max intensities for color scaling
277
- min_intensity = np.min(intensities)
278
- max_intensity = np.max(intensities)
297
+ # Determine shape if not provided
298
+ if shape is None:
299
+ if len(positions) > 0:
300
+ max_coords = np.max(positions, axis=0).astype(int)
301
+ shape = tuple(max_coords + 1)
302
+ else:
303
+ shape = (100, 100, 100) if is_3d else (1, 100, 100)
279
304
 
280
- # Create figure
281
- fig = plt.figure(figsize=figsize)
305
+ # Determine min and max intensities for scaling
306
+ if len(intensities) > 0:
307
+ min_intensity = np.min(intensities)
308
+ max_intensity = np.max(intensities)
309
+ else:
310
+ min_intensity, max_intensity = 0, 1
282
311
 
283
- if is_3d:
284
- # 3D plot
285
- ax = fig.add_subplot(111, projection='3d')
312
+ if labeled_array is not None:
313
+ # Create numpy RGB array output using labeled array and lookup table approach
314
+
315
+ # Create mapping from node ID to community intensity value
316
+ node_to_community_intensity = {}
317
+ for node_id, centroid in node_centroids.items():
318
+ # Convert node_id to regular int if it's numpy
319
+ if hasattr(node_id, 'item'):
320
+ node_id = node_id.item()
321
+
322
+ try:
323
+ # Get community for this node
324
+ community_id = node_community[node_id]
325
+
326
+ # Convert community_id to regular int if it's numpy
327
+ if hasattr(community_id, 'item'):
328
+ community_id = community_id.item()
329
+
330
+ # Get intensity for this community
331
+ if community_id in community_intensity_clean:
332
+ node_to_community_intensity[node_id] = community_intensity_clean[community_id]
333
+ except KeyError:
334
+ # Skip nodes that don't have community assignments
335
+ pass
336
+
337
+ # Create colormap function (RdBu_r - red for high, blue for low, yellow/white for middle)
338
+ def intensity_to_rgb(intensity, min_val, max_val):
339
+ """Convert intensity value to RGB using RdBu_r colormap logic"""
340
+ if max_val == min_val:
341
+ # All same value, use neutral color
342
+ return np.array([255, 255, 255], dtype=np.uint8) # White
343
+
344
+ # Normalize to -1 to 1 range (like RdBu_r colormap)
345
+ normalized = 2 * (intensity - min_val) / (max_val - min_val) - 1
346
+ normalized = np.clip(normalized, -1, 1)
347
+
348
+ if normalized > 0:
349
+ # Positive values: white to red
350
+ r = 255
351
+ g = int(255 * (1 - normalized))
352
+ b = int(255 * (1 - normalized))
353
+ else:
354
+ # Negative values: white to blue
355
+ r = int(255 * (1 + normalized))
356
+ g = int(255 * (1 + normalized))
357
+ b = 255
358
+
359
+ return np.array([r, g, b], dtype=np.uint8)
286
360
 
287
- # Extract coordinates (assuming [Z, Y, X] format)
288
- z_coords = positions[:, 0]
289
- y_coords = positions[:, 1]
290
- x_coords = positions[:, 2]
361
+ # Create lookup table for RGB colors
362
+ max_label = max(max(labeled_array.flat), max(node_to_community_intensity.keys()) if node_to_community_intensity else 0)
363
+ color_lut = np.zeros((max_label + 1, 3), dtype=np.uint8) # Default to black (0,0,0)
291
364
 
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)
365
+ # Fill lookup table with RGB colors based on community intensity
366
+ for node_id, intensity in node_to_community_intensity.items():
367
+ rgb_color = intensity_to_rgb(intensity, min_intensity, max_intensity)
368
+ color_lut[int(node_id)] = rgb_color
296
369
 
297
- ax.set_xlabel('X')
298
- ax.set_ylabel('Y')
299
- ax.set_zlabel('Z')
300
- ax.set_title('3D Community Intensity Heatmap')
370
+ # Apply lookup table to labeled array - single vectorized operation
371
+ if is_3d:
372
+ # Return full 3D RGB array [Z, Y, X, 3]
373
+ heatmap_array = color_lut[labeled_array]
374
+ else:
375
+ # Return 2D RGB array
376
+ if labeled_array.ndim == 3:
377
+ # Take middle slice for 2D representation
378
+ middle_slice = labeled_array.shape[0] // 2
379
+ heatmap_array = color_lut[labeled_array[middle_slice]]
380
+ else:
381
+ # Already 2D
382
+ heatmap_array = color_lut[labeled_array]
301
383
 
384
+ return heatmap_array
385
+
302
386
  else:
303
- # 2D plot (using Y, X coordinates, ignoring Z/first dimension)
304
- ax = fig.add_subplot(111)
387
+ # Create matplotlib plot
388
+ fig = plt.figure(figsize=figsize)
305
389
 
306
- # Extract Y, X coordinates
307
- y_coords = positions[:, 1]
308
- x_coords = positions[:, 2]
390
+ if is_3d:
391
+ # 3D plot
392
+ ax = fig.add_subplot(111, projection='3d')
393
+
394
+ # Extract coordinates (assuming [Z, Y, X] format)
395
+ z_coords = positions[:, 0]
396
+ y_coords = positions[:, 1]
397
+ x_coords = positions[:, 2]
398
+
399
+ # Create scatter plot
400
+ scatter = ax.scatter(x_coords, y_coords, z_coords,
401
+ c=intensities, s=point_size, alpha=alpha,
402
+ cmap='RdBu_r', vmin=min_intensity, vmax=max_intensity)
403
+
404
+ ax.set_xlabel('X')
405
+ ax.set_ylabel('Y')
406
+ ax.set_zlabel('Z')
407
+ ax.set_title(f'{title}')
408
+
409
+ # Set axis limits based on shape
410
+ ax.set_xlim(0, shape[2])
411
+ ax.set_ylim(0, shape[1])
412
+ ax.set_zlim(0, shape[0])
413
+
414
+ else:
415
+ # 2D plot (using Y, X coordinates, ignoring Z/first dimension)
416
+ ax = fig.add_subplot(111)
417
+
418
+ # Extract Y, X coordinates
419
+ y_coords = positions[:, 1]
420
+ x_coords = positions[:, 2]
421
+
422
+ # Create scatter plot
423
+ scatter = ax.scatter(x_coords, y_coords,
424
+ c=intensities, s=point_size, alpha=alpha,
425
+ cmap='RdBu_r', vmin=min_intensity, vmax=max_intensity)
426
+
427
+ ax.set_xlabel('X')
428
+ ax.set_ylabel('Y')
429
+ ax.set_title(f'{title}')
430
+ ax.grid(True, alpha=0.3)
431
+
432
+ # Set axis limits based on shape
433
+ ax.set_xlim(0, shape[2])
434
+ ax.set_ylim(0, shape[1])
435
+
436
+ # Set origin to top-left (invert Y-axis)
437
+ ax.invert_yaxis()
309
438
 
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)
439
+ # Add colorbar
440
+ cbar = plt.colorbar(scatter, ax=ax, shrink=0.8)
441
+ cbar.set_label(colorbar_label)
314
442
 
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)
443
+ # Add text annotations for min/max values
444
+ cbar.ax.text(1.05, 0, f'Min: {min_intensity:.3f}\n(Blue)',
445
+ transform=cbar.ax.transAxes, va='bottom')
446
+ cbar.ax.text(1.05, 1, f'Max: {max_intensity:.3f}\n(Red)',
447
+ transform=cbar.ax.transAxes, va='top')
319
448
 
320
- # Set origin to top-left (invert Y-axis)
321
- ax.invert_yaxis()
449
+ plt.tight_layout()
450
+ plt.show()
451
+
452
+
453
+ def create_node_heatmap(node_intensity, node_centroids, shape=None, is_3d=True,
454
+ labeled_array=None, figsize=(12, 8), point_size=50, alpha=0.7,
455
+ colorbar_label="Node Intensity", title="Node Clustering Intensity Heatmap"):
456
+ """
457
+ Create a 2D or 3D heatmap showing nodes colored by their individual intensities.
458
+ Can return either matplotlib plot or numpy array for overlay purposes.
322
459
 
323
- # Add colorbar
324
- cbar = plt.colorbar(scatter, ax=ax, shrink=0.8)
325
- cbar.set_label(colorbar_label)
460
+ Parameters:
461
+ -----------
462
+ node_intensity : dict
463
+ Dictionary mapping node IDs to intensity values
464
+ Keys can be np.int64 or regular ints
465
+
466
+ node_centroids : dict
467
+ Dictionary mapping node IDs to centroids
468
+ Centroids should be [Z, Y, X] for 3D or [1, Y, X] for pseudo-3D
469
+
470
+ shape : tuple, optional
471
+ Shape of the output array in [Z, Y, X] format
472
+ If None, will be inferred from node_centroids
473
+
474
+ is_3d : bool, default=True
475
+ If True, create 3D plot/array. If False, create 2D plot/array.
476
+
477
+ labeled_array : np.ndarray, optional
478
+ If provided, returns numpy array overlay using this labeled array template
479
+ instead of matplotlib plot. Uses lookup table approach for efficiency.
480
+
481
+ figsize : tuple, default=(12, 8)
482
+ Figure size (width, height) - only used for matplotlib
483
+
484
+ point_size : int, default=50
485
+ Size of scatter plot points - only used for matplotlib
486
+
487
+ alpha : float, default=0.7
488
+ Transparency of points (0-1) - only used for matplotlib
489
+
490
+ colorbar_label : str, default="Node Intensity"
491
+ Label for the colorbar - only used for matplotlib
492
+
493
+ Returns:
494
+ --------
495
+ If labeled_array is None: fig, ax (matplotlib figure and axis objects)
496
+ If labeled_array is provided: np.ndarray (heatmap array with intensity values)
497
+ """
498
+ import numpy as np
499
+ import matplotlib.pyplot as plt
500
+
501
+ # Convert numpy int64 keys to regular ints for consistency
502
+ node_intensity_clean = {}
503
+ for k, v in node_intensity.items():
504
+ if hasattr(k, 'item'): # numpy scalar
505
+ node_intensity_clean[k.item()] = v
506
+ else:
507
+ node_intensity_clean[k] = v
326
508
 
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')
509
+ # Prepare data for plotting/array creation
510
+ node_positions = []
511
+ node_intensities = []
332
512
 
333
- plt.tight_layout()
334
- plt.show()
335
-
336
-
513
+ for node_id, centroid in node_centroids.items():
514
+ try:
515
+ # Convert node_id to regular int if it's numpy
516
+ if hasattr(node_id, 'item'):
517
+ node_id = node_id.item()
518
+
519
+ # Get intensity for this node
520
+ intensity = node_intensity_clean[node_id]
521
+
522
+ node_positions.append(centroid)
523
+ node_intensities.append(intensity)
524
+ except KeyError:
525
+ # Skip nodes that don't have intensity values
526
+ pass
527
+
528
+ # Convert to numpy arrays
529
+ positions = np.array(node_positions)
530
+ intensities = np.array(node_intensities)
531
+
532
+ # Determine shape if not provided
533
+ if shape is None:
534
+ if len(positions) > 0:
535
+ max_coords = np.max(positions, axis=0).astype(int)
536
+ shape = tuple(max_coords + 1)
537
+ else:
538
+ shape = (100, 100, 100) if is_3d else (1, 100, 100)
539
+
540
+ # Determine min and max intensities for scaling
541
+ if len(intensities) > 0:
542
+ min_intensity = np.min(intensities)
543
+ max_intensity = np.max(intensities)
544
+ else:
545
+ min_intensity, max_intensity = 0, 1
546
+
547
+ if labeled_array is not None:
548
+ # Create numpy RGB array output using labeled array and lookup table approach
549
+
550
+ # Create mapping from node ID to intensity value (keep original float values)
551
+ node_to_intensity = {}
552
+ for node_id, centroid in node_centroids.items():
553
+ # Convert node_id to regular int if it's numpy
554
+ if hasattr(node_id, 'item'):
555
+ node_id = node_id.item()
556
+
557
+ # Only include nodes that have intensity values
558
+ if node_id in node_intensity_clean:
559
+ node_to_intensity[node_id] = node_intensity_clean[node_id]
560
+
561
+ # Create colormap function (RdBu_r - red for high, blue for low, yellow/white for middle)
562
+ def intensity_to_rgb(intensity, min_val, max_val):
563
+ """Convert intensity value to RGB using RdBu_r colormap logic"""
564
+ if max_val == min_val:
565
+ # All same value, use neutral color
566
+ return np.array([255, 255, 255], dtype=np.uint8) # White
567
+
568
+ # Normalize to -1 to 1 range (like RdBu_r colormap)
569
+ normalized = 2 * (intensity - min_val) / (max_val - min_val) - 1
570
+ normalized = np.clip(normalized, -1, 1)
571
+
572
+ if normalized > 0:
573
+ # Positive values: white to red
574
+ r = 255
575
+ g = int(255 * (1 - normalized))
576
+ b = int(255 * (1 - normalized))
577
+ else:
578
+ # Negative values: white to blue
579
+ r = int(255 * (1 + normalized))
580
+ g = int(255 * (1 + normalized))
581
+ b = 255
582
+
583
+ return np.array([r, g, b], dtype=np.uint8)
584
+
585
+ # Create lookup table for RGB colors
586
+ max_label = max(max(labeled_array.flat), max(node_to_intensity.keys()) if node_to_intensity else 0)
587
+ color_lut = np.zeros((max_label + 1, 3), dtype=np.uint8) # Default to black (0,0,0)
588
+
589
+ # Fill lookup table with RGB colors based on intensity
590
+ for node_id, intensity in node_to_intensity.items():
591
+ rgb_color = intensity_to_rgb(intensity, min_intensity, max_intensity)
592
+ color_lut[int(node_id)] = rgb_color
593
+
594
+ # Apply lookup table to labeled array - single vectorized operation
595
+ if is_3d:
596
+ # Return full 3D RGB array [Z, Y, X, 3]
597
+ heatmap_array = color_lut[labeled_array]
598
+ else:
599
+ # Return 2D RGB array
600
+ if labeled_array.ndim == 3:
601
+ # Take middle slice for 2D representation
602
+ middle_slice = labeled_array.shape[0] // 2
603
+ heatmap_array = color_lut[labeled_array[middle_slice]]
604
+ else:
605
+ # Already 2D
606
+ heatmap_array = color_lut[labeled_array]
607
+
608
+ return heatmap_array
609
+
610
+ else:
611
+ # Create matplotlib plot
612
+ fig = plt.figure(figsize=figsize)
613
+
614
+ if is_3d:
615
+ # 3D plot
616
+ ax = fig.add_subplot(111, projection='3d')
617
+
618
+ # Extract coordinates (assuming [Z, Y, X] format)
619
+ z_coords = positions[:, 0]
620
+ y_coords = positions[:, 1]
621
+ x_coords = positions[:, 2]
622
+
623
+ # Create scatter plot
624
+ scatter = ax.scatter(x_coords, y_coords, z_coords,
625
+ c=intensities, s=point_size, alpha=alpha,
626
+ cmap='RdBu_r', vmin=min_intensity, vmax=max_intensity)
627
+
628
+ ax.set_xlabel('X')
629
+ ax.set_ylabel('Y')
630
+ ax.set_zlabel('Z')
631
+ ax.set_title(f'{title}')
632
+
633
+ # Set axis limits based on shape
634
+ ax.set_xlim(0, shape[2])
635
+ ax.set_ylim(0, shape[1])
636
+ ax.set_zlim(0, shape[0])
637
+
638
+ else:
639
+ # 2D plot (using Y, X coordinates, ignoring Z/first dimension)
640
+ ax = fig.add_subplot(111)
641
+
642
+ # Extract Y, X coordinates
643
+ y_coords = positions[:, 1]
644
+ x_coords = positions[:, 2]
645
+
646
+ # Create scatter plot
647
+ scatter = ax.scatter(x_coords, y_coords,
648
+ c=intensities, s=point_size, alpha=alpha,
649
+ cmap='RdBu_r', vmin=min_intensity, vmax=max_intensity)
650
+
651
+ ax.set_xlabel('X')
652
+ ax.set_ylabel('Y')
653
+ ax.set_title(f'{title}')
654
+ ax.grid(True, alpha=0.3)
655
+
656
+ # Set axis limits based on shape
657
+ ax.set_xlim(0, shape[2])
658
+ ax.set_ylim(0, shape[1])
659
+
660
+ # Set origin to top-left (invert Y-axis)
661
+ ax.invert_yaxis()
662
+
663
+ # Add colorbar
664
+ cbar = plt.colorbar(scatter, ax=ax, shrink=0.8)
665
+ cbar.set_label(colorbar_label)
666
+
667
+ # Add text annotations for min/max values
668
+ cbar.ax.text(1.05, 0, f'Min: {min_intensity:.3f}\n(Blue)',
669
+ transform=cbar.ax.transAxes, va='bottom')
670
+ cbar.ax.text(1.05, 1, f'Max: {max_intensity:.3f}\n(Red)',
671
+ transform=cbar.ax.transAxes, va='top')
672
+
673
+ plt.tight_layout()
674
+ plt.show()
337
675
 
338
676
  # Example usage:
339
677
  if __name__ == "__main__":
@@ -350,5 +688,4 @@ if __name__ == "__main__":
350
688
  fig, ax = plot_dict_heatmap(sample_dict, sample_id_set,
351
689
  title="Sample Heatmap Visualization")
352
690
 
353
- plt.show()
354
-
691
+ plt.show()
nettracer3d/nettracer.py CHANGED
@@ -4291,7 +4291,7 @@ class Network_3D:
4291
4291
 
4292
4292
 
4293
4293
 
4294
- def get_degrees(self, down_factor = 1, directory = None, called = False, no_img = 0):
4294
+ def get_degrees(self, down_factor = 1, directory = None, called = False, no_img = 0, heatmap = False):
4295
4295
  """
4296
4296
  Method to obtain information on the degrees of nodes in the network, also generating overlays that relate this information to the 3D structure.
4297
4297
  Overlays include a grayscale image where nodes are assigned a grayscale value corresponding to their degree, and a numerical index where numbers are drawn at nodes corresponding to their degree.
@@ -4301,6 +4301,27 @@ class Network_3D:
4301
4301
  :returns: A dictionary of degree values for each node.
4302
4302
  """
4303
4303
 
4304
+ if heatmap:
4305
+ import statistics
4306
+ degrees_dict = {node: val for (node, val) in self.network.degree()}
4307
+ pred = statistics.mean(list(degrees_dict.values()))
4308
+
4309
+ node_intensity = {}
4310
+ import math
4311
+ node_centroids = {}
4312
+
4313
+ for node in list(self.network.nodes()):
4314
+ node_intensity[node] = math.log(self.network.degree(node)/pred)
4315
+ node_centroids[node] = self.node_centroids[node]
4316
+
4317
+ from . import neighborhoods
4318
+
4319
+ overlay = neighborhoods.create_node_heatmap(node_intensity, node_centroids, shape = self.nodes.shape, is_3d=True, labeled_array = self.nodes)
4320
+
4321
+ return degrees_dict, overlay
4322
+
4323
+
4324
+
4304
4325
  if down_factor > 1:
4305
4326
  centroids = self._node_centroids.copy()
4306
4327
  for item in self._node_centroids:
@@ -4657,18 +4678,16 @@ class Network_3D:
4657
4678
  dim = 3
4658
4679
  break
4659
4680
 
4660
-
4661
4681
  if ignore_dims:
4662
4682
 
4663
4683
  factor = 0.25
4664
4684
 
4685
+ big_array = proximity.convert_centroids_to_array(list(self.node_centroids.values()))
4665
4686
 
4666
4687
  if bounds is None:
4667
- if dim == 2:
4668
- min_coords = np.array([0,0])
4669
- else:
4670
- min_coords = np.array([0,0,0])
4671
- max_coords = np.max(points_array, axis=0)
4688
+ min_coords = np.array([0,0,0])
4689
+ max_coords = [np.max(big_array[:, 0]), np.max(big_array[:, 1]), np.max(big_array[:, 2])]
4690
+ del big_array
4672
4691
  max_coords = np.flip(max_coords)
4673
4692
  bounds = (min_coords, max_coords)
4674
4693
  else:
@@ -4683,11 +4702,16 @@ class Network_3D:
4683
4702
 
4684
4703
  new_list = []
4685
4704
 
4705
+
4686
4706
  if dim == 3:
4687
4707
  for centroid in roots:
4688
4708
 
4689
4709
  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):
4690
4710
  new_list.append(centroid)
4711
+
4712
+
4713
+ #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):
4714
+ #new_list.append(centroid)
4691
4715
  #print(f"dim_list: {dim_list}, centroid: {centroid}, min_coords: {min_coords}, max_coords: {max_coords}")
4692
4716
  else:
4693
4717
  for centroid in roots:
@@ -5068,7 +5092,7 @@ class Network_3D:
5068
5092
 
5069
5093
  self.communities = invert_dict(com_dict)
5070
5094
 
5071
- def community_heatmap(self, num_nodes = None, is3d = True):
5095
+ def community_heatmap(self, num_nodes = None, is3d = True, numpy = False):
5072
5096
 
5073
5097
  import math
5074
5098
 
@@ -5106,10 +5130,21 @@ class Network_3D:
5106
5130
  for com, nodes in coms.items():
5107
5131
  heat_dict[com] = math.log(len(nodes)/rand_dens)
5108
5132
 
5133
+ try:
5134
+ shape = self.nodes.shape
5135
+ except:
5136
+ big_array = proximity.convert_centroids_to_array(list(self.node_centroids.values()))
5137
+ shape = [np.max(big_array[0, :]) + 1, np.max(big_array[1, :]) + 1, np.max(big_array[2, :]) + 1]
5138
+
5109
5139
  from . import neighborhoods
5110
- neighborhoods.create_community_heatmap(heat_dict, self.communities, self.node_centroids, is_3d=is3d)
5140
+ if not numpy:
5141
+ neighborhoods.create_community_heatmap(heat_dict, self.communities, self.node_centroids, shape = shape, is_3d=is3d)
5142
+
5143
+ return heat_dict
5144
+ else:
5145
+ overlay = neighborhoods.create_community_heatmap(heat_dict, self.communities, self.node_centroids, shape = shape, is_3d=is3d, labeled_array = self.nodes)
5146
+ return heat_dict, overlay
5111
5147
 
5112
- return heat_dict
5113
5148
 
5114
5149
  def merge_node_ids(self, path, data):
5115
5150
 
@@ -5161,6 +5196,81 @@ class Network_3D:
5161
5196
  pass
5162
5197
 
5163
5198
 
5199
+ def nearest_neighbors_avg(self, root, targ, xy_scale = 1, z_scale = 1, num = 1, heatmap = False, threed = True, numpy = False):
5200
+
5201
+ root_set = []
5202
+
5203
+ compare_set = []
5204
+
5205
+ if root is None:
5206
+
5207
+ root_set = list(self.node_centroids.keys())
5208
+ compare_set = root_set
5209
+ title = "Nearest Neighbors Between Nodes Heatmap"
5210
+
5211
+ else:
5212
+
5213
+ title = f"Nearest Neighbors of ID {targ} from ID {root} Heatmap"
5214
+
5215
+ for node, iden in self.node_identities.items():
5216
+
5217
+ if iden == root:
5218
+
5219
+ root_set.append(node)
5220
+
5221
+ elif (iden == targ) or (targ == 'All Others (Excluding Self)'):
5222
+
5223
+ compare_set.append(node)
5224
+
5225
+ if root == targ:
5226
+
5227
+ compare_set = root_set
5228
+ if len(compare_set) - 1 < num:
5229
+
5230
+ print("Error: Not enough neighbor nodes for requested number of neighbors")
5231
+ return
5232
+
5233
+ if len(compare_set) < num:
5234
+
5235
+ print("Error: Not enough neighbor nodes for requested number of neighbors")
5236
+ return
5237
+
5238
+ avg, output = proximity.average_nearest_neighbor_distances(self.node_centroids, root_set, compare_set, xy_scale=self.xy_scale, z_scale=self.z_scale, num = num)
5239
+
5240
+ if heatmap:
5241
+
5242
+
5243
+ from . import neighborhoods
5244
+ try:
5245
+ shape = self.nodes.shape
5246
+ except:
5247
+ big_array = proximity.convert_centroids_to_array(list(self.node_centroids.values()))
5248
+ shape = [np.max(big_array[0, :]) + 1, np.max(big_array[1, :]) + 1, np.max(big_array[2, :]) + 1]
5249
+
5250
+ pred = avg
5251
+
5252
+ node_intensity = {}
5253
+ import math
5254
+ node_centroids = {}
5255
+
5256
+ for node in root_set:
5257
+ node_intensity[node] = math.log(pred/output[node])
5258
+ node_centroids[node] = self.node_centroids[node]
5259
+
5260
+ if numpy:
5261
+
5262
+ overlay = neighborhoods.create_node_heatmap(node_intensity, node_centroids, shape = shape, is_3d=threed, labeled_array = self.nodes, colorbar_label="Clustering Intensity", title = title)
5263
+
5264
+ return avg, output, overlay
5265
+
5266
+ else:
5267
+ neighborhoods.create_node_heatmap(node_intensity, node_centroids, shape = shape, is_3d=threed, labeled_array = None, colorbar_label="Clustering Intensity", title = title)
5268
+
5269
+ return avg, output
5270
+
5271
+
5272
+
5273
+
5164
5274
 
5165
5275
 
5166
5276
 
@@ -3432,6 +3432,8 @@ class ImageViewerWindow(QMainWindow):
3432
3432
  ripley_action.triggered.connect(self.show_ripley_dialog)
3433
3433
  heatmap_action = stats_menu.addAction("Community Cluster Heatmap")
3434
3434
  heatmap_action.triggered.connect(self.show_heatmap_dialog)
3435
+ nearneigh_action = stats_menu.addAction("Average Nearest Neighbors")
3436
+ nearneigh_action.triggered.connect(self.show_nearneigh_dialog)
3435
3437
  vol_action = stats_menu.addAction("Calculate Volumes")
3436
3438
  vol_action.triggered.connect(self.volumes)
3437
3439
  rad_action = stats_menu.addAction("Calculate Radii")
@@ -5212,6 +5214,10 @@ class ImageViewerWindow(QMainWindow):
5212
5214
  dialog = HeatmapDialog(self)
5213
5215
  dialog.exec()
5214
5216
 
5217
+ def show_nearneigh_dialog(self):
5218
+ dialog = NearNeighDialog(self)
5219
+ dialog.exec()
5220
+
5215
5221
  def show_random_dialog(self):
5216
5222
  dialog = RandomDialog(self)
5217
5223
  dialog.exec()
@@ -7269,6 +7275,174 @@ class DegreeDistDialog(QDialog):
7269
7275
  except Exception as e:
7270
7276
  print(f"An error occurred: {e}")
7271
7277
 
7278
+ class NearNeighDialog(QDialog):
7279
+ def __init__(self, parent=None):
7280
+ super().__init__(parent)
7281
+ self.setWindowTitle(f"Nearest Neighborhood Averages (Using Centroids)")
7282
+ self.setModal(True)
7283
+
7284
+ # Main layout
7285
+ main_layout = QVBoxLayout(self)
7286
+
7287
+ # Identities group box (only if node_identities exists)
7288
+ identities_group = QGroupBox("Identities")
7289
+ identities_layout = QFormLayout(identities_group)
7290
+
7291
+ if my_network.node_identities is not None:
7292
+
7293
+ self.root = QComboBox()
7294
+ self.root.addItems(list(set(my_network.node_identities.values())))
7295
+ self.root.setCurrentIndex(0)
7296
+ identities_layout.addRow("Root Identity to Search for Neighbor's IDs?", self.root)
7297
+
7298
+ self.targ = QComboBox()
7299
+ neighs = list(set(my_network.node_identities.values()))
7300
+ neighs.append("All Others (Excluding Self)")
7301
+ self.targ.addItems(neighs)
7302
+ self.targ.setCurrentIndex(0)
7303
+ identities_layout.addRow("Neighbor Identities to Search For?", self.targ)
7304
+ else:
7305
+ self.root = None
7306
+ self.targ = None
7307
+
7308
+ self.num = QLineEdit("1")
7309
+ identities_layout.addRow("Number of Nearest Neighbors to Evaluate Per Node?:", self.num)
7310
+
7311
+
7312
+ main_layout.addWidget(identities_group)
7313
+
7314
+
7315
+ # Optional Heatmap group box
7316
+ heatmap_group = QGroupBox("Optional Heatmap")
7317
+ heatmap_layout = QFormLayout(heatmap_group)
7318
+
7319
+ self.map = QPushButton("(If getting distribution): Generate Heatmap?")
7320
+ self.map.setCheckable(True)
7321
+ self.map.setChecked(False)
7322
+ heatmap_layout.addRow("Heatmap:", self.map)
7323
+
7324
+ self.threed = QPushButton("(For above): Return 3D map? (uncheck for 2D): ")
7325
+ self.threed.setCheckable(True)
7326
+ self.threed.setChecked(True)
7327
+ heatmap_layout.addRow("3D:", self.threed)
7328
+
7329
+ self.numpy = QPushButton("(For heatmap): Return image overlay instead of graph? (Goes in Overlay 2): ")
7330
+ self.numpy.setCheckable(True)
7331
+ self.numpy.setChecked(False)
7332
+ self.numpy.clicked.connect(self.toggle_map)
7333
+ heatmap_layout.addRow("Overlay:", self.numpy)
7334
+
7335
+ main_layout.addWidget(heatmap_group)
7336
+
7337
+ # Get Distribution group box
7338
+ distribution_group = QGroupBox("Get Distribution")
7339
+ distribution_layout = QVBoxLayout(distribution_group)
7340
+
7341
+ run_button = QPushButton("Get Average Nearest Neighbor (Plus Distribution)")
7342
+ run_button.clicked.connect(self.run)
7343
+ distribution_layout.addWidget(run_button)
7344
+
7345
+ main_layout.addWidget(distribution_group)
7346
+
7347
+ # Get All Averages group box (only if node_identities exists)
7348
+ if my_network.node_identities is not None:
7349
+ averages_group = QGroupBox("Get All Averages")
7350
+ averages_layout = QVBoxLayout(averages_group)
7351
+
7352
+ run_button2 = QPushButton("Get Average Nearest All ID Combinations (No Distribution, No Heatmap)")
7353
+ run_button2.clicked.connect(self.run2)
7354
+ averages_layout.addWidget(run_button2)
7355
+
7356
+ main_layout.addWidget(averages_group)
7357
+
7358
+ def toggle_map(self):
7359
+
7360
+ if self.numpy.isChecked():
7361
+
7362
+ if not self.map.isChecked():
7363
+
7364
+ self.map.click()
7365
+
7366
+ def run(self):
7367
+
7368
+ try:
7369
+
7370
+ try:
7371
+ root = self.root.currentText()
7372
+ except:
7373
+ root = None
7374
+ try:
7375
+ targ = self.targ.currentText()
7376
+ except:
7377
+ targ = None
7378
+
7379
+ heatmap = self.map.isChecked()
7380
+ threed = self.threed.isChecked()
7381
+ numpy = self.numpy.isChecked()
7382
+ num = int(self.num.text()) if self.num.text().strip() else 1
7383
+
7384
+ if root is not None and targ is not None:
7385
+ title = f"Nearest {num} Neighbor(s) Distance of {targ} from {root}"
7386
+ header = f"Shortest Distance to Closest {num} {targ}(s)"
7387
+ header2 = f"{root} Node ID"
7388
+ else:
7389
+ title = f"Nearest {num} Neighbor(s) Distance Between Nodes"
7390
+ header = f"Shortest Distance to Closest {num} Nodes"
7391
+ header2 = "Root Node ID"
7392
+
7393
+ if my_network.node_centroids is None:
7394
+ self.parent().show_centroid_dialog()
7395
+ if my_network.node_centroids is None:
7396
+ return
7397
+
7398
+ if not numpy:
7399
+ avg, output = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num, heatmap = heatmap, threed = threed)
7400
+ else:
7401
+ avg, output, overlay = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num, heatmap = heatmap, threed = threed, numpy = True)
7402
+ self.parent().load_channel(3, overlay, data = True)
7403
+
7404
+ self.parent().format_for_upperright_table([avg], metric = f'Avg {title}', title = f'Avg {title}')
7405
+ self.parent().format_for_upperright_table(output, header2, header, title = title)
7406
+
7407
+ self.accept()
7408
+
7409
+ except Exception as e:
7410
+ import traceback
7411
+ print(traceback.format_exc())
7412
+
7413
+ print(f"Error: {e}")
7414
+
7415
+ def run2(self):
7416
+
7417
+ try:
7418
+
7419
+ available = list(set(my_network.node_identities.values()))
7420
+
7421
+ num = int(self.num.text()) if self.num.text().strip() else 1
7422
+
7423
+ output_dict = {}
7424
+
7425
+ while len(available) > 1:
7426
+
7427
+ root = available[0]
7428
+
7429
+ for targ in available:
7430
+
7431
+ avg, _ = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num)
7432
+
7433
+ output_dict[f"{root} vs {targ}"] = avg
7434
+
7435
+ del available[0]
7436
+
7437
+ self.parent().format_for_upperright_table(output_dict, "ID Combo", "Avg Distance to Nearest", title = "Average Distance to Nearest Neighbors for All ID Combos")
7438
+
7439
+ self.accept()
7440
+
7441
+ except Exception as e:
7442
+
7443
+ print(f"Error: {e}")
7444
+
7445
+
7272
7446
  class NeighborIdentityDialog(QDialog):
7273
7447
 
7274
7448
  def __init__(self, parent=None):
@@ -7455,8 +7629,7 @@ class RipleyDialog(QDialog):
7455
7629
  "Error:",
7456
7630
  f"Failed to preform cluster analysis: {str(e)}"
7457
7631
  )
7458
- import traceback
7459
- print(traceback.format_exc())
7632
+
7460
7633
  print(f"Error: {e}")
7461
7634
 
7462
7635
  class HeatmapDialog(QDialog):
@@ -7479,6 +7652,11 @@ class HeatmapDialog(QDialog):
7479
7652
  self.is3d.setChecked(True)
7480
7653
  layout.addRow("Use 3D Plot (uncheck for 2D)?:", self.is3d)
7481
7654
 
7655
+ self.numpy = QPushButton("(For heatmap): Return image overlay instead of graph? (Goes in Overlay 2): ")
7656
+ self.numpy.setCheckable(True)
7657
+ self.numpy.setChecked(False)
7658
+ layout.addRow("Overlay:", self.numpy)
7659
+
7482
7660
 
7483
7661
  # Add Run button
7484
7662
  run_button = QPushButton("Run")
@@ -7487,25 +7665,40 @@ class HeatmapDialog(QDialog):
7487
7665
 
7488
7666
  def run(self):
7489
7667
 
7490
- nodecount = int(self.nodecount.text()) if self.nodecount.text().strip() else None
7668
+ try:
7491
7669
 
7492
- is3d = self.is3d.isChecked()
7670
+ nodecount = int(self.nodecount.text()) if self.nodecount.text().strip() else None
7671
+
7672
+ is3d = self.is3d.isChecked()
7493
7673
 
7494
7674
 
7495
- if my_network.communities is None:
7496
- if my_network.network is not None:
7497
- self.parent().show_partition_dialog()
7498
- else:
7499
- self.parent().handle_com_cell()
7500
7675
  if my_network.communities is None:
7501
- return
7676
+ if my_network.network is not None:
7677
+ self.parent().show_partition_dialog()
7678
+ else:
7679
+ self.parent().handle_com_cell()
7680
+ if my_network.communities is None:
7681
+ return
7502
7682
 
7503
- heat_dict = my_network.community_heatmap(num_nodes = nodecount, is3d = is3d)
7683
+ numpy = self.numpy.isChecked()
7504
7684
 
7505
- self.parent().format_for_upperright_table(heat_dict, metric='Community', value='ln(Predicted Community Nodecount/Actual)', title="Community Heatmap")
7685
+ if not numpy:
7506
7686
 
7507
- self.accept()
7687
+ heat_dict = my_network.community_heatmap(num_nodes = nodecount, is3d = is3d)
7508
7688
 
7689
+ else:
7690
+
7691
+ heat_dict, overlay = my_network.community_heatmap(num_nodes = nodecount, is3d = is3d, numpy = True)
7692
+ self.parent().load_channel(3, overlay, data = True)
7693
+
7694
+
7695
+ self.parent().format_for_upperright_table(heat_dict, metric='Community', value='ln(Predicted Community Nodecount/Actual)', title="Community Heatmap")
7696
+
7697
+ self.accept()
7698
+
7699
+ except Exception as e:
7700
+
7701
+ print(f"Error: {e}")
7509
7702
 
7510
7703
 
7511
7704
 
@@ -7749,7 +7942,7 @@ class DegreeDialog(QDialog):
7749
7942
 
7750
7943
  # Add mode selection dropdown
7751
7944
  self.mode_selector = QComboBox()
7752
- self.mode_selector.addItems(["Just make table", "Draw degree of node as overlay (literally draws 1, 2, 3, etc... faster)", "Label nodes by degree (nodes will take on the value 1, 2, 3, etc, based on their degree, to export for array based analysis... slower)"])
7945
+ self.mode_selector.addItems(["Just make table", "Draw degree of node as overlay (literally draws 1, 2, 3, etc... faster)", "Label nodes by degree (nodes will take on the value 1, 2, 3, etc, based on their degree, to export for array based analysis)", "Create Heatmap of Degrees"])
7753
7946
  self.mode_selector.setCurrentIndex(0) # Default to Mode 1
7754
7947
  layout.addRow("Execution Mode:", self.mode_selector)
7755
7948
 
@@ -7770,6 +7963,14 @@ class DegreeDialog(QDialog):
7770
7963
 
7771
7964
  accepted_mode = self.mode_selector.currentIndex()
7772
7965
 
7966
+ if accepted_mode == 3:
7967
+ degree_dict, overlay = my_network.get_degrees(heatmap = True)
7968
+ self.parent().format_for_upperright_table(degree_dict, 'Node ID', 'Degree', title = 'Degrees of nodes')
7969
+ self.parent().load_channel(3, channel_data = overlay, data = True)
7970
+ self.accept()
7971
+ return
7972
+
7973
+
7773
7974
  try:
7774
7975
  down_factor = float(self.down_factor.text()) if self.down_factor.text() else 1
7775
7976
  except ValueError:
@@ -10274,7 +10475,7 @@ class CropDialog(QDialog):
10274
10475
  try:
10275
10476
 
10276
10477
  super().__init__(parent)
10277
- self.setWindowTitle("Crop Image?")
10478
+ self.setWindowTitle("Crop Image (Will transpose any centroids)?")
10278
10479
  self.setModal(True)
10279
10480
 
10280
10481
  layout = QFormLayout(self)
@@ -10330,10 +10531,70 @@ class CropDialog(QDialog):
10330
10531
 
10331
10532
  self.parent().load_channel(i, array, data = True)
10332
10533
 
10534
+ print("Transposing centroids...")
10535
+
10536
+ try:
10537
+
10538
+ if my_network.node_centroids is not None:
10539
+ nodes = list(my_network.node_centroids.keys())
10540
+ centroids = np.array(list(my_network.node_centroids.values()))
10541
+
10542
+ # Transform all at once
10543
+ transformed = centroids - np.array([zmin, ymin, xmin])
10544
+ transformed = transformed.astype(int)
10545
+
10546
+ # Boolean mask for valid coordinates
10547
+ valid_mask = ((transformed >= 0) &
10548
+ (transformed <= np.array([zmax, ymax, xmax]))).all(axis=1)
10549
+
10550
+ # Rebuild dictionary with only valid entries
10551
+ my_network.node_centroids = {
10552
+ nodes[int(i)]: [int(transformed[i, 0]), int(transformed[i, 1]), int(transformed[i, 2])]
10553
+ for i in range(len(nodes)) if valid_mask[i]
10554
+ }
10555
+
10556
+ self.parent().format_for_upperright_table(my_network.node_centroids, 'NodeID', ['Z', 'Y', 'X'], 'Node Centroids')
10557
+
10558
+ except Exception as e:
10559
+
10560
+ print(f"Error transposing node centroids: {e}")
10561
+
10562
+ try:
10563
+
10564
+ if my_network.edge_centroids is not None:
10565
+
10566
+ if my_network.edge_centroids is not None:
10567
+ nodes = list(my_network.edge_centroids.keys())
10568
+ centroids = np.array(list(my_network.edge_centroids.values()))
10569
+
10570
+ # Transform all at once
10571
+ transformed = centroids - np.array([zmin, ymin, xmin])
10572
+ transformed = transformed.astype(int)
10573
+
10574
+ # Boolean mask for valid coordinates
10575
+ valid_mask = ((transformed >= 0) &
10576
+ (transformed <= np.array([zmax, ymax, xmax]))).all(axis=1)
10577
+
10578
+ # Rebuild dictionary with only valid entries
10579
+ my_network.edge_centroids = {
10580
+ nodes[int(i)]: [int(transformed[i, 0]), int(transformed[i, 1]), int(transformed[i, 2])]
10581
+ for i in range(len(nodes)) if valid_mask[i]
10582
+ }
10583
+
10584
+ self.parent().format_for_upperright_table(my_network.edge_centroids, 'EdgeID', ['Z', 'Y', 'X'], 'Edge Centroids')
10585
+
10586
+ except Exception as e:
10587
+
10588
+ print(f"Error transposing edge centroids: {e}")
10589
+
10590
+
10333
10591
  self.accept()
10334
10592
 
10335
10593
  except Exception as e:
10336
10594
 
10595
+ import traceback
10596
+ print(traceback.format_exc())
10597
+
10337
10598
  print(f"Error cropping: {e}")
10338
10599
 
10339
10600
 
nettracer3d/node_draw.py CHANGED
@@ -201,18 +201,28 @@ def degree_draw(degree_dict, centroid_dict, nodes):
201
201
  return draw_array
202
202
 
203
203
  def degree_infect(degree_dict, nodes):
204
-
205
- num_nodes = int(np.max(nodes))
206
- return_nodes = nodes.copy()
207
-
208
- for node in range(1, num_nodes + 1):
209
- if node not in degree_dict:
210
- continue
211
- else:
212
- idxs = np.argwhere(nodes == node)
213
- for idx in idxs:
214
- return_nodes[tuple(idx)] = degree_dict[node]
215
-
204
+ return_nodes = np.zeros_like(nodes) # Start with all zeros
205
+
206
+ if not degree_dict: # Handle empty dict
207
+ return return_nodes
208
+
209
+ # Create arrays for old and new values
210
+ old_vals = np.array(list(degree_dict.keys()))
211
+ new_vals = np.array(list(degree_dict.values()))
212
+
213
+ # Sort for searchsorted to work correctly
214
+ sort_idx = np.argsort(old_vals)
215
+ old_vals_sorted = old_vals[sort_idx]
216
+ new_vals_sorted = new_vals[sort_idx]
217
+
218
+ # Find which nodes exist in the dictionary
219
+ mask = np.isin(nodes, old_vals_sorted)
220
+
221
+ # Only process nodes that exist in the dictionary
222
+ if np.any(mask):
223
+ indices = np.searchsorted(old_vals_sorted, nodes[mask])
224
+ return_nodes[mask] = new_vals_sorted[indices]
225
+
216
226
  return return_nodes
217
227
 
218
228
 
nettracer3d/proximity.py CHANGED
@@ -296,6 +296,69 @@ def extract_pairwise_connections(connections):
296
296
  return output
297
297
 
298
298
 
299
+ def average_nearest_neighbor_distances(point_centroids, root_set, compare_set, xy_scale=1.0, z_scale=1.0, num = 1):
300
+ """
301
+ Calculate the average distance between each point in root_set and its nearest neighbor in compare_set.
302
+
303
+ Args:
304
+ point_centroids (dict): Dictionary mapping point IDs to [Z, Y, X] coordinates
305
+ root_set (set): Set of point IDs to find nearest neighbors for
306
+ compare_set (set): Set of point IDs to search for nearest neighbors in
307
+ xy_scale (float): Scaling factor for X and Y coordinates
308
+ z_scale (float): Scaling factor for Z coordinate
309
+
310
+ Returns:
311
+ float: Average distance to nearest neighbors
312
+ """
313
+
314
+ # Extract and scale coordinates for compare_set
315
+ compare_coords = []
316
+ compare_ids = list(compare_set)
317
+
318
+ for point_id in compare_ids:
319
+ z, y, x = point_centroids[point_id]
320
+ compare_coords.append([z * z_scale, y * xy_scale, x * xy_scale])
321
+
322
+ compare_coords = np.array(compare_coords)
323
+
324
+ # Build KDTree for efficient nearest neighbor search
325
+ tree = KDTree(compare_coords)
326
+
327
+ distances = {}
328
+ same_sets = root_set == compare_set
329
+
330
+ for root_id in root_set:
331
+ # Get scaled coordinates for root point
332
+ z, y, x = point_centroids[root_id]
333
+ root_coord = np.array([z * z_scale, y * xy_scale, x * xy_scale])
334
+
335
+ if same_sets:
336
+ # When sets are the same, find 2 nearest neighbors and take the second one
337
+ # (first one would be the point itself)
338
+ distances_to_all, indices = tree.query(root_coord, k= (num + 1))
339
+
340
+ temp_dist = 0
341
+ for i in range(1, len(distances_to_all)):
342
+ temp_dist += distances_to_all[i]
343
+
344
+ distances[root_id] = temp_dist/(len(distances_to_all) - 1)
345
+
346
+ else:
347
+ # Different sets, find nearest neighbors
348
+ distances_to_all, _ = tree.query(root_coord, k=num)
349
+ temp_dist = 0
350
+ for val in distances_to_all:
351
+ temp_dist += val
352
+
353
+ distances[root_id] = temp_dist/(len(distances_to_all))
354
+
355
+ avg = np.mean(list(distances.values())) if list(distances.values()) else 0.0
356
+
357
+
358
+ # Return average distance
359
+ return avg, distances
360
+
361
+
299
362
 
300
363
  #voronois:
301
364
  def create_voronoi_3d_kdtree(centroids: Dict[Union[int, str], Union[Tuple[int, int, int], List[int]]],
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nettracer3d
3
- Version: 0.8.0
3
+ Version: 0.8.1
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,11 +73,8 @@ 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.8.0 Updates --
76
+ -- Version 0.8.1 Updates --
77
77
 
78
- * Added ability to threshold nodes by degree.
79
- * Improved image viewer window performance.
80
- * Bug fixes and a few optimizations.
81
- * Added ability to 'merge node identities' which just uses the nodes image as a reference for collecting 'identity' information from a group of other images - ie can use with cell nuclei (DAPI) to see what markers from the same imaging session overlap.
82
- * Added ability to search for specific nodes directly in the nodes image with 'shift + f' or right click.
83
-
78
+ * Added nearest neighbor evaluation function (Analysis -> Stats -> Avg Nearest Neighbor)
79
+ * Added heatmap outputs for node degrees (Analysis -> Data/Overlays -> Get Degree Information).
80
+ * Bug fixes and misc improvements.
@@ -3,21 +3,21 @@ nettracer3d/community_extractor.py,sha256=ZOz97Au4k7dHK8azarWQtxCPyvwzGmDHNL8kTI
3
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=kkKR8m6Gjw34cDd_mytAIwLxqvuNBtQb2hU4JuBY9pI,12301
7
- nettracer3d/nettracer.py,sha256=494nHDmdrdfecTAShbXc0eFE2tG6WKJtEqKvJyt4sh4,227141
8
- nettracer3d/nettracer_gui.py,sha256=Pc_t5pDk0P4gpyC1axx37JJX6EpMvXdj0UHbqjqENrQ,501757
6
+ nettracer3d/neighborhoods.py,sha256=LnflCJdlNOuILal_fsT-idp1IROQH2PvLG-Ro3m55vo,26757
7
+ nettracer3d/nettracer.py,sha256=F91vCLJ-OB5qGLjOXRjNh2qE3vFkXYh_VPpFud2Cu4o,231437
8
+ nettracer3d/nettracer_gui.py,sha256=rRm3hquNbJJ8wbqPyojK45jFKc--Y72L4Ia9_dMSONE,512171
9
9
  nettracer3d/network_analysis.py,sha256=h-5yzUWdE0hcWYy8wcBA5LV1bRhdqiMnKbQLrRzb1Sw,41443
10
10
  nettracer3d/network_draw.py,sha256=F7fw6Pcf4qWOhdKwLmhwqWdschbDlHzwCVolQC9imeU,14117
11
- nettracer3d/node_draw.py,sha256=k3sCTfUCJs3aH1C1q1gTNxDz9EAQbBd1hsUIJajxRx8,9823
12
- nettracer3d/proximity.py,sha256=hPmTPFGUziPMVwfWRLVV9gUjqSL7nzLD6WAVLekvxbE,28545
11
+ nettracer3d/node_draw.py,sha256=LoeTFeOcrX6kPquZvCqYnMW-jDd9oqKM27r-rTlKEtY,10274
12
+ nettracer3d/proximity.py,sha256=3cgF9hWYlzodO_vFc9TA2QO1tZG6jqYbz4_AG89EKDE,30867
13
13
  nettracer3d/run.py,sha256=xYeaAc8FCx8MuzTGyL3NR3mK7WZzffAYAH23bNRZYO4,127
14
14
  nettracer3d/segmenter.py,sha256=VatOSpc41lxhPuYLTTejCxG1CcwP5hwiQ3ZFK9OBavA,60115
15
15
  nettracer3d/segmenter_GPU.py,sha256=sFVmz_cYIVOQqnfFV3peK9hzb6IoIV5WDQHH9Lws96I,53915
16
16
  nettracer3d/simple_network.py,sha256=dkG4jpc4zzdeuoaQobgGfL3PNo6N8dGKQ5hEEubFIvA,9947
17
17
  nettracer3d/smart_dilate.py,sha256=DOEOQq9ig6-AO4MpqAG0CqrGDFqw5_UBeqfSedqHk28,25933
18
- nettracer3d-0.8.0.dist-info/licenses/LICENSE,sha256=gM207DhJjWrxLuEWXl0Qz5ISbtWDmADfjHp3yC2XISs,888
19
- nettracer3d-0.8.0.dist-info/METADATA,sha256=2bK3auAT6Xr7y_VDBaZE_cB4dgShYtF0qrIzcuTDflU,4605
20
- nettracer3d-0.8.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
21
- nettracer3d-0.8.0.dist-info/entry_points.txt,sha256=Nx1rr_0QhJXDBHAQg2vcqCzLMKBzSHfwy3xwGkueVyc,53
22
- nettracer3d-0.8.0.dist-info/top_level.txt,sha256=zsYy9rZwirfCEOubolhee4TyzqBAL5gSUeFMzhFTX8c,12
23
- nettracer3d-0.8.0.dist-info/RECORD,,
18
+ nettracer3d-0.8.1.dist-info/licenses/LICENSE,sha256=gM207DhJjWrxLuEWXl0Qz5ISbtWDmADfjHp3yC2XISs,888
19
+ nettracer3d-0.8.1.dist-info/METADATA,sha256=kw33GSA0zjO1PbRkCfMDjh-opacoSn1OSx7X5M8UA88,4332
20
+ nettracer3d-0.8.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
21
+ nettracer3d-0.8.1.dist-info/entry_points.txt,sha256=Nx1rr_0QhJXDBHAQg2vcqCzLMKBzSHfwy3xwGkueVyc,53
22
+ nettracer3d-0.8.1.dist-info/top_level.txt,sha256=zsYy9rZwirfCEOubolhee4TyzqBAL5gSUeFMzhFTX8c,12
23
+ nettracer3d-0.8.1.dist-info/RECORD,,