nettracer3d 0.8.3__py3-none-any.whl → 0.8.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.
nettracer3d/nettracer.py CHANGED
@@ -31,6 +31,7 @@ from . import community_extractor
31
31
  from . import network_analysis
32
32
  from . import morphology
33
33
  from . import proximity
34
+ from skimage.segmentation import watershed as water
34
35
 
35
36
 
36
37
  #These next several methods relate to searching with 3D objects by dilating each one in a subarray around their neighborhood although I don't explicitly use this anywhere... can call them deprecated although I may want to use them later again so I have them still written out here.
@@ -104,7 +105,7 @@ def process_label(args):
104
105
  print(f"Processing node {label}")
105
106
 
106
107
  # Get the pre-computed bounding box for this label
107
- slice_obj = bounding_boxes[label-1] # -1 because label numbers start at 1
108
+ slice_obj = bounding_boxes[int(label)-1] # -1 because label numbers start at 1
108
109
  if slice_obj is None:
109
110
  return None, None, None
110
111
 
@@ -129,7 +130,7 @@ def create_node_dictionary(nodes, edges, num_nodes, dilate_xy, dilate_z):
129
130
  with ThreadPoolExecutor(max_workers=mp.cpu_count()) as executor:
130
131
  # Create args list with bounding_boxes included
131
132
  args_list = [(nodes, edges, i, dilate_xy, dilate_z, array_shape, bounding_boxes)
132
- for i in range(1, num_nodes + 1)]
133
+ for i in range(1, int(num_nodes) + 1)]
133
134
 
134
135
  # Execute parallel tasks to process labels
135
136
  results = executor.map(process_label, args_list)
@@ -214,7 +215,6 @@ def establish_connections_parallel(edge_labels, num_edge, node_labels):
214
215
 
215
216
  edge_connections.append(node_labels[index])
216
217
 
217
- #the set() wrapper removes duplicates from the same sublist
218
218
  my_connections = list(set(edge_connections))
219
219
 
220
220
 
@@ -282,8 +282,36 @@ def extract_pairwise_connections(connections):
282
282
 
283
283
 
284
284
  #Saving outputs
285
+ def create_and_save_dataframe(pairwise_connections, excel_filename=None):
286
+ """Internal method used to convert lists of discrete connections into an excel output"""
287
+
288
+ # Create DataFrame directly from the connections with 3 columns
289
+ df = pd.DataFrame(pairwise_connections, columns=['Node A', 'Node B', 'Edge C'])
290
+
291
+ if excel_filename is not None:
292
+ # Remove file extension if present to use as base path
293
+ base_path = excel_filename.rsplit('.', 1)[0]
294
+
295
+ # First try to save as CSV
296
+ try:
297
+ csv_path = f"{base_path}.csv"
298
+ df.to_csv(csv_path, index=False)
299
+ print(f"Network file saved to {csv_path}")
300
+ return
301
+ except Exception as e:
302
+ print(f"Could not save as CSV: {str(e)}")
303
+
304
+ # If CSV fails, try to save as Excel
305
+ try:
306
+ xlsx_path = f"{base_path}.xlsx"
307
+ df.to_excel(xlsx_path, index=False)
308
+ print(f"Network file saved to {xlsx_path}")
309
+ except Exception as e:
310
+ print(f"Unable to write network file to disk... please make sure that {base_path}.xlsx is being saved to a valid directory and try again")
311
+ else:
312
+ return df
285
313
 
286
- def create_and_save_dataframe(pairwise_connections, excel_filename = None):
314
+ def create_and_save_dataframe_old(pairwise_connections, excel_filename = None):
287
315
  """Internal method used to convert lists of discrete connections into an excel output"""
288
316
  # Determine the length of the input list
289
317
  length = len(pairwise_connections)
@@ -636,28 +664,25 @@ def threshold(arr, proportion, custom_rad = None):
636
664
  def find_closest_index(target: float, num_list: list[float]) -> int:
637
665
  return min(range(len(num_list)), key=lambda i: abs(num_list[i] - target))
638
666
 
639
- # Step 1: Flatten the array
640
- flattened = arr.flatten()
641
667
 
642
- # Step 2: Filter out the zero values
643
- non_zero_values = list(set(flattened[flattened > 0]))
668
+ if custom_rad is not None:
669
+
670
+ threshold_value = custom_rad
644
671
 
645
- # Step 3: Sort the remaining values
646
- sorted_values = np.sort(non_zero_values)
672
+ else:
673
+ # Step 1: Flatten the array
674
+ flattened = arr.flatten()
647
675
 
648
- # Step 4: Determine the threshold for the top proportion%
676
+ # Step 2: Filter out the zero values
677
+ non_zero_values = list(set(flattened[flattened > 0]))
649
678
 
650
- if custom_rad is None:
679
+ # Step 3: Sort the remaining values
680
+ sorted_values = np.sort(non_zero_values)
651
681
 
652
682
  threshold_index = int(len(sorted_values) * proportion)
653
683
  threshold_value = sorted_values[threshold_index]
684
+ print(f"Thresholding as if smallest_radius as assigned {threshold_value}")
654
685
 
655
- else:
656
-
657
- targ = int(find_closest_index(custom_rad, sorted_values) - (0.02 * len(sorted_values)))
658
-
659
- threshold_value = sorted_values[targ]
660
- print(f"Suggested proportion for rad {custom_rad} -> {targ/len(sorted_values)}")
661
686
 
662
687
  mask = arr > threshold_value
663
688
 
@@ -1574,6 +1599,187 @@ def directory_info(directory = None):
1574
1599
  return items
1575
1600
 
1576
1601
 
1602
+ # Ripley's K Helpers:
1603
+
1604
+ def mirror_points_for_edge_correction(points_array, bounds, max_r, dim=3):
1605
+ """
1606
+ Mirror points near boundaries to handle edge effects in Ripley's K analysis.
1607
+ Works with actual coordinate positions, not spatial grid placement.
1608
+
1609
+ Parameters:
1610
+ points_array: numpy array of shape (n, 3) with [z, y, x] coordinates (already scaled)
1611
+ bounds: tuple of (min_coords, max_coords) where each is array - can be 2D or 3D
1612
+ max_r: maximum search radius (determines mirroring distance)
1613
+ dim: dimension (2 or 3) - affects which coordinates are used
1614
+
1615
+ Returns:
1616
+ numpy array with original points plus mirrored points
1617
+ """
1618
+ min_coords, max_coords = bounds
1619
+
1620
+ # Ensure bounds are numpy arrays and handle dimension mismatch
1621
+ min_coords = np.array(min_coords)
1622
+ max_coords = np.array(max_coords)
1623
+
1624
+ # Handle case where bounds might be 2D but points are 3D
1625
+ if len(min_coords) == 2 and points_array.shape[1] == 3:
1626
+ # Extend 2D bounds to 3D by adding z=0 dimension at the front
1627
+ min_coords = np.array([0, min_coords[0], min_coords[1]]) # [0, min_x, min_y] -> [min_z, min_y, min_x]
1628
+ max_coords = np.array([0, max_coords[0], max_coords[1]]) # [0, max_x, max_y] -> [max_z, max_y, max_x]
1629
+ elif len(min_coords) == 3 and points_array.shape[1] == 3:
1630
+ # Already 3D, but ensure it's in [z,y,x] format (your bounds are [x,y,z] and get flipped)
1631
+ pass # Should already be handled by the flip in your bounds calculation
1632
+
1633
+ # Start with original points
1634
+ all_points = points_array.copy()
1635
+
1636
+ if dim == 2:
1637
+ # For 2D: work with y, x coordinates (indices 1, 2), z should be 0
1638
+ active_dims = [1, 2] # y, x
1639
+ # 8 potential mirror regions for 2D (excluding center)
1640
+ mirror_combinations = [
1641
+ [0, -1], [0, 1], # left, right (y direction)
1642
+ [-1, 0], [1, 0], # bottom, top (x direction)
1643
+ [-1, -1], [-1, 1], # corners
1644
+ [1, -1], [1, 1]
1645
+ ]
1646
+ else:
1647
+ # For 3D: work with z, y, x coordinates (indices 0, 1, 2)
1648
+ active_dims = [0, 1, 2] # z, y, x
1649
+ # 26 potential mirror regions for 3D (3^3 - 1, excluding center)
1650
+ mirror_combinations = []
1651
+ for dz in [-1, 0, 1]:
1652
+ for dy in [-1, 0, 1]:
1653
+ for dx in [-1, 0, 1]:
1654
+ if not (dz == 0 and dy == 0 and dx == 0): # exclude center
1655
+ mirror_combinations.append([dz, dy, dx])
1656
+
1657
+ # Process each potential mirror region
1658
+ for mirror_dir in mirror_combinations:
1659
+ # Find points that need this specific mirroring
1660
+ needs_mirror = np.ones(len(points_array), dtype=bool)
1661
+
1662
+ # Check each active dimension
1663
+ for i, dim_idx in enumerate(active_dims):
1664
+ direction = mirror_dir[i] if dim == 3 else mirror_dir[i]
1665
+
1666
+ # Safety check: make sure we have bounds for this dimension
1667
+ if dim_idx >= len(min_coords) or dim_idx >= len(max_coords):
1668
+ needs_mirror = np.zeros(len(points_array), dtype=bool) # Skip this mirror if bounds insufficient
1669
+ break
1670
+
1671
+ if direction == -1: # Points near minimum boundary
1672
+ # Distance from point to min boundary < max_r
1673
+ needs_mirror &= (points_array[:, dim_idx] - min_coords[dim_idx]) < max_r
1674
+ elif direction == 1: # Points near maximum boundary
1675
+ # Distance from point to max boundary < max_r
1676
+ needs_mirror &= (max_coords[dim_idx] - points_array[:, dim_idx]) < max_r
1677
+ # direction == 0 means no constraint for this dimension
1678
+
1679
+ # Create mirrored points if any qualify
1680
+ if np.any(needs_mirror):
1681
+ mirrored_points = points_array[needs_mirror].copy()
1682
+
1683
+ # Apply mirroring transformation for each active dimension
1684
+ for i, dim_idx in enumerate(active_dims):
1685
+ direction = mirror_dir[i] if dim == 3 else mirror_dir[i]
1686
+
1687
+ # Safety check again
1688
+ if dim_idx >= len(min_coords) or dim_idx >= len(max_coords):
1689
+ continue
1690
+
1691
+ if direction == -1: # Mirror across minimum boundary
1692
+ # Reflection formula: new_coord = 2 * boundary - old_coord
1693
+ mirrored_points[:, dim_idx] = 2 * min_coords[dim_idx] - mirrored_points[:, dim_idx]
1694
+ elif direction == 1: # Mirror across maximum boundary
1695
+ # Reflection formula: new_coord = 2 * boundary - old_coord
1696
+ mirrored_points[:, dim_idx] = 2 * max_coords[dim_idx] - mirrored_points[:, dim_idx]
1697
+
1698
+ # Add mirrored points to collection
1699
+ all_points = np.vstack([all_points, mirrored_points])
1700
+
1701
+ return all_points
1702
+ def get_max_r_from_proportion(bounds, proportion):
1703
+ """
1704
+ Calculate max_r based on bounds and proportion, matching your generate_r_values logic.
1705
+
1706
+ Parameters:
1707
+ bounds: tuple of (min_coords, max_coords)
1708
+ proportion: maximum proportion of study area extent
1709
+
1710
+ Returns:
1711
+ max_r value
1712
+ """
1713
+ min_coords, max_coords = bounds
1714
+ min_coords = np.array(min_coords)
1715
+ max_coords = np.array(max_coords)
1716
+
1717
+ # Calculate dimensions
1718
+ dimensions = max_coords - min_coords
1719
+
1720
+ # Remove placeholder dimensions (where dimension = 1, typically for 2D z-dimension)
1721
+ # But ensure we don't end up with an empty array
1722
+ filtered_dimensions = dimensions[dimensions != 1]
1723
+ if len(filtered_dimensions) == 0:
1724
+ # If all dimensions were 1 (shouldn't happen), use original dimensions
1725
+ filtered_dimensions = dimensions
1726
+
1727
+ # Use minimum dimension for safety (matches your existing logic)
1728
+ min_dimension = np.min(filtered_dimensions)
1729
+ max_r = min_dimension * proportion
1730
+
1731
+ return max_r
1732
+
1733
+ def apply_edge_correction_to_ripley(roots, targs, proportion, bounds, dim, node_centroids=None):
1734
+ """
1735
+ Apply edge correction through mirroring to target points.
1736
+
1737
+ This should be called AFTER convert_centroids_to_array but BEFORE
1738
+ convert_augmented_array_to_points (for 2D case).
1739
+
1740
+ Parameters:
1741
+ roots: array of root points (search centers) - already scaled
1742
+ targs: array of target points (points being searched for) - already scaled
1743
+ proportion: the proportion parameter from your workflow
1744
+ bounds: boundary tuple (min_coords, max_coords) or None
1745
+ dim: dimension (2 or 3)
1746
+ node_centroids: dict of node centroids (needed if bounds is None)
1747
+
1748
+ Returns:
1749
+ tuple: (roots, mirrored_targs) where mirrored_targs includes edge corrections
1750
+ """
1751
+ # Handle bounds calculation if not provided (matching your existing logic)
1752
+ if bounds is None:
1753
+ if node_centroids is None:
1754
+ # Fallback: calculate from the points we have
1755
+ all_points = np.vstack([roots, targs])
1756
+ else:
1757
+ # Use your existing method
1758
+ import proximity # Assuming this is available
1759
+ big_array = proximity.convert_centroids_to_array(list(node_centroids.values()))
1760
+ all_points = big_array
1761
+
1762
+ min_coords = np.array([0, 0, 0])
1763
+ max_coords = [np.max(all_points[:, 0]), np.max(all_points[:, 1]), np.max(all_points[:, 2])]
1764
+ max_coords = np.flip(max_coords) # Convert [x,y,z] to [z,y,x] format
1765
+ bounds = (min_coords, max_coords)
1766
+
1767
+ if 'big_array' in locals():
1768
+ del big_array
1769
+
1770
+ # Calculate max_r using your existing logic
1771
+ max_r = get_max_r_from_proportion(bounds, proportion)
1772
+
1773
+ # Mirror target points for edge correction
1774
+ mirrored_targs = mirror_points_for_edge_correction(targs, bounds, max_r, dim)
1775
+
1776
+ print(f"Original target points: {len(targs)}, After mirroring: {len(mirrored_targs)}")
1777
+ print(f"Added {len(mirrored_targs) - len(targs)} mirrored points for edge correction")
1778
+ print(f"Using max_r = {max_r} for mirroring threshold")
1779
+ print(f"Bounds used: min={bounds[0]}, max={bounds[1]}")
1780
+
1781
+ return roots, mirrored_targs
1782
+
1577
1783
 
1578
1784
  #CLASSLESS FUNCTIONS THAT MAY BE USEFUL TO USERS TO RUN DIRECTLY THAT SUPPORT ANALYSIS IN SOME WAY. NOTE THESE METHODS SOMETIMES ARE USED INTERNALLY AS WELL:
1579
1785
 
@@ -1803,13 +2009,21 @@ def label_branches(array, peaks = 0, branch_removal = 0, comp_dil = 0, max_vol =
1803
2009
  if nodes is None:
1804
2010
 
1805
2011
  array = smart_dilate.smart_label(array, other_array, GPU = GPU, remove_template = True)
2012
+ #distance = smart_dilate.compute_distance_transform_distance(array)
2013
+ print("Watershedding result...")
2014
+ #array = water(-distance, other_array, mask=array) #Tried out skimage watershed as shown and found it did not label branches as well as smart_label (esp combined combined with post-processing label splitting if needed)
1806
2015
 
1807
2016
  else:
1808
2017
  if down_factor is not None:
1809
2018
  array = smart_dilate.smart_label(bonus_array, array, GPU = GPU, predownsample = down_factor, remove_template = True)
2019
+ #distance = smart_dilate.compute_distance_transform_distance(bonus_array)
2020
+ #array = water(-distance, array, mask=bonus_array)
1810
2021
  else:
1811
2022
 
1812
2023
  array = smart_dilate.smart_label(bonus_array, array, GPU = GPU, remove_template = True)
2024
+ #distance = smart_dilate.compute_distance_transform_distance(bonus_array)
2025
+ #array = water(-distance, array, mask=bonus_array)
2026
+
1813
2027
 
1814
2028
  if down_factor is not None and nodes is None:
1815
2029
  array = upsample_with_padding(array, down_factor, arrayshape)
@@ -2089,6 +2303,56 @@ def filter_size_by_vol(binary_array, volume_threshold):
2089
2303
 
2090
2304
  return result
2091
2305
 
2306
+ def gray_watershed(image, min_distance = 1, threshold_abs = None):
2307
+
2308
+
2309
+ from skimage.feature import peak_local_max
2310
+
2311
+ if len(np.unique(image)) == 2:
2312
+ image = smart_dilate.compute_distance_transform_distance(image)
2313
+
2314
+
2315
+ is_pseudo_3d = image.shape[0] == 1
2316
+ if is_pseudo_3d:
2317
+ image = np.squeeze(image) # Convert to 2D for processing
2318
+
2319
+ #smoothed = ndimage.gaussian_filter(image.astype(float), sigma=2)
2320
+
2321
+ peaks = peak_local_max(image, min_distance = min_distance, threshold_abs = threshold_abs)
2322
+ if len(peaks) < 256:
2323
+ dtype = np.uint8
2324
+ elif len(peaks) < 65535:
2325
+ dtype = np.uint16
2326
+ else:
2327
+ dytpe = np.uint32
2328
+
2329
+ clone = np.zeros_like(image).astype(dtype)
2330
+
2331
+ if not is_pseudo_3d:
2332
+ for i, peak in enumerate(peaks):
2333
+ z, y, x = peak
2334
+ clone[z,y,x] = i + 1
2335
+ else:
2336
+ for i, peak in enumerate(peaks):
2337
+ y, x = peak
2338
+ clone[y,x] = i + 1
2339
+
2340
+
2341
+ if is_pseudo_3d:
2342
+ image = np.expand_dims(image, axis = 0)
2343
+ clone = np.expand_dims(clone, axis = 0)
2344
+
2345
+
2346
+ binary_image = binarize(image)
2347
+ #image = smart_dilate.smart_label(image, clone, GPU = False)
2348
+
2349
+ image = water(-image, clone, mask=binary_image)
2350
+
2351
+
2352
+
2353
+ return image
2354
+
2355
+
2092
2356
  def watershed(image, directory = None, proportion = 0.1, GPU = True, smallest_rad = None, predownsample = None, predownsample2 = None):
2093
2357
  """
2094
2358
  Can be used to 3D watershed a binary image. Watershedding attempts to use an algorithm to split touching objects into seperate labelled components. Labelled output will be saved to the active directory if none is specified.
@@ -2162,15 +2426,17 @@ def watershed(image, directory = None, proportion = 0.1, GPU = True, smallest_ra
2162
2426
  if len(labels.shape) ==2:
2163
2427
  labels = np.expand_dims(labels, axis = 0)
2164
2428
 
2165
- del distance
2429
+ #del distance
2166
2430
 
2167
2431
 
2168
2432
  if labels.shape[1] < original_shape[1]: #If downsample was used, upsample output
2169
2433
  labels = upsample_with_padding(labels, downsample_needed, original_shape)
2170
2434
  labels = labels * old_mask
2171
- labels = smart_dilate.smart_label(old_mask, labels, GPU = GPU, predownsample = predownsample2)
2435
+ labels = water(-distance, labels, mask=old_mask) # Here i like skimage watershed over smart_label, mainly because skimage just kicks out too-small nodes from the image, while smart label just labels them sort of wrongly.
2436
+ #labels = smart_dilate.smart_label(old_mask, labels, GPU = GPU, predownsample = predownsample2)
2172
2437
  else:
2173
- labels = smart_dilate.smart_label(image, labels, GPU = GPU, predownsample = predownsample2)
2438
+ labels = water(-distance, labels, mask=image)
2439
+ #labels = smart_dilate.smart_label(image, labels, GPU = GPU, predownsample = predownsample2)
2174
2440
 
2175
2441
  if directory is None:
2176
2442
  pass
@@ -3389,8 +3655,6 @@ class Network_3D:
3389
3655
  if skeletonized:
3390
3656
  binary_edges = skeletonize(binary_edges)
3391
3657
 
3392
-
3393
-
3394
3658
  if search is not None and hasattr(self, '_nodes') and self._nodes is not None and self._search_region is None:
3395
3659
  search_region = binarize(self._nodes)
3396
3660
  dilate_xy, dilate_z = dilation_length_to_pixels(self._xy_scale, self._z_scale, search, search)
@@ -3550,11 +3814,12 @@ class Network_3D:
3550
3814
  self._network_lists = network_analysis.read_excel_to_lists(df)
3551
3815
  self._network, net_weights = network_analysis.weighted_network(df)
3552
3816
 
3553
- if ignore_search_region and hasattr(self, '_edges') and self._edges is not None and hasattr(self, '_nodes') and self._nodes is not None and search is not None:
3554
- dilate_xy, dilate_z = dilation_length_to_pixels(self._xy_scale, self._z_scale, search, search)
3555
- print(f"{dilate_xy}, {dilate_z}")
3817
+ if ignore_search_region and hasattr(self, '_edges') and self._edges is not None and hasattr(self, '_nodes') and self._nodes is not None:
3818
+ #dilate_xy, dilate_z = dilation_length_to_pixels(self._xy_scale, self._z_scale, search, search)
3819
+ #print(f"{dilate_xy}, {dilate_z}")
3556
3820
  num_nodes = np.max(self._nodes)
3557
- connections_parallel = create_node_dictionary(self._nodes, self._edges, num_nodes, dilate_xy, dilate_z) #Find which edges connect which nodes and put them in a dictionary.
3821
+ #connections_parallel = create_node_dictionary(self._nodes, self._edges, num_nodes, dilate_xy, dilate_z) #Find which edges connect which nodes and put them in a dictionary.
3822
+ connections_parallel = create_node_dictionary(self._nodes, self._edges, num_nodes, 3, 3) #For now I only implement this for immediate neighbor search so we'll just use 3 and 3 here.
3558
3823
  connections_parallel = find_shared_value_pairs(connections_parallel) #Sort through the dictionary to find connected node pairs.
3559
3824
  df = create_and_save_dataframe(connections_parallel)
3560
3825
  self._network_lists = network_analysis.read_excel_to_lists(df)
@@ -3635,7 +3900,10 @@ class Network_3D:
3635
3900
  except:
3636
3901
  pass
3637
3902
 
3638
- 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)
3903
+ 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
3904
+ else:
3905
+ self._edges, _ = label_objects(edges)
3906
+
3639
3907
  del edges
3640
3908
  if directory is not None:
3641
3909
  try:
@@ -4650,27 +4918,50 @@ class Network_3D:
4650
4918
  return neighborhood_dict, proportion_dict, title1, title2, densities
4651
4919
 
4652
4920
 
4653
- def get_ripley(self, root = None, targ = None, distance = 1, edgecorrect = True, bounds = None, ignore_dims = False, proportion = 0.5):
4921
+
4922
+ def get_ripley(self, root = None, targ = None, distance = 1, edgecorrect = True, bounds = None, ignore_dims = False, proportion = 0.5, mode = 0, safe = False, factor = 0.25):
4923
+
4924
+ is_subset = False
4925
+
4926
+ if bounds is None:
4927
+ big_array = proximity.convert_centroids_to_array(list(self.node_centroids.values()))
4928
+ min_coords = np.array([0,0,0])
4929
+ max_coords = [np.max(big_array[:, 0]), np.max(big_array[:, 1]), np.max(big_array[:, 2])]
4930
+ del big_array
4931
+ max_coords = np.flip(max_coords)
4932
+ bounds = (min_coords, max_coords)
4933
+ else:
4934
+ min_coords, max_coords = bounds
4935
+
4936
+ min_bounds, max_bounds = bounds
4937
+ sides = max_bounds - min_bounds
4938
+ # Set max_r to None since we've handled edge effects through mirroring
4654
4939
 
4655
4940
 
4656
4941
  if root is None or targ is None: #Self clustering in this case
4657
4942
  roots = self._node_centroids.values()
4943
+ root_ids = self.node_centroids.keys()
4658
4944
  targs = self._node_centroids.values()
4945
+ is_subset = True
4659
4946
  else:
4660
4947
  roots = []
4661
4948
  targs = []
4949
+ root_ids = []
4662
4950
 
4663
4951
  for node, nodeid in self.node_identities.items(): #Otherwise we need to pull out this info
4664
4952
  if nodeid == root:
4665
4953
  roots.append(self._node_centroids[node])
4954
+ root_ids.append(node)
4666
4955
  if nodeid == targ:
4667
4956
  targs.append(self._node_centroids[node])
4668
4957
 
4958
+ if not is_subset:
4959
+ if np.array_equal(roots, targs):
4960
+ is_subset = True
4961
+
4669
4962
  rooties = proximity.convert_centroids_to_array(roots, xy_scale = self.xy_scale, z_scale = self.z_scale)
4670
4963
  targs = proximity.convert_centroids_to_array(targs, xy_scale = self.xy_scale, z_scale = self.z_scale)
4671
- points_array = np.vstack((rooties, targs))
4672
- del rooties
4673
-
4964
+
4674
4965
  try:
4675
4966
  if self.nodes.shape[0] == 1:
4676
4967
  dim = 2
@@ -4683,66 +4974,135 @@ class Network_3D:
4683
4974
  dim = 3
4684
4975
  break
4685
4976
 
4977
+ if dim == 2:
4978
+ volume = sides[0] * sides[1] * self.xy_scale**2
4979
+ else:
4980
+ volume = np.prod(sides) * self.z_scale * self.xy_scale**2
4981
+
4982
+ points_array = np.vstack((rooties, targs))
4983
+ del rooties
4984
+ max_r = None
4985
+ if safe:
4986
+ proportion = factor
4987
+
4988
+
4686
4989
  if ignore_dims:
4687
4990
 
4688
- factor = 0.25
4991
+ new_list = []
4689
4992
 
4690
- big_array = proximity.convert_centroids_to_array(list(self.node_centroids.values()))
4993
+ if mode == 0:
4691
4994
 
4692
- if bounds is None:
4693
- min_coords = np.array([0,0,0])
4694
- max_coords = [np.max(big_array[:, 0]), np.max(big_array[:, 1]), np.max(big_array[:, 2])]
4695
- del big_array
4696
- max_coords = np.flip(max_coords)
4697
- bounds = (min_coords, max_coords)
4698
- else:
4699
- min_coords, max_coords = bounds
4995
+ try:
4996
+ dim_list = max_coords - min_coords
4997
+ except:
4998
+ min_coords = np.array([0,0,0])
4999
+ bounds = (min_coords, max_coords)
5000
+ dim_list = max_coords - min_coords
4700
5001
 
4701
- try:
4702
- dim_list = max_coords - min_coords
4703
- except:
4704
- min_coords = np.array([0,0,0])
4705
- bounds = (min_coords, max_coords)
4706
- dim_list = max_coords - min_coords
4707
5002
 
4708
- new_list = []
5003
+ if dim == 3:
4709
5004
 
5005
+ """
5006
+ 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
5007
+ factor_xy = (self.z_scale/self.xy_scale) * factor # So 'factor' in the xy dim has to get smaller
5008
+ factor_z = factor
5009
+ elif self.z_scale > self.xy_scale: # Same idea
5010
+ factor_z = (self.xy_scale/self.z_scale) * factor
5011
+ factor_xy = factor
5012
+ else:
5013
+ factor_z = factor
5014
+ factor_xy = factor
5015
+ """
4710
5016
 
4711
- if dim == 3:
4712
- for centroid in roots:
5017
+ for centroid in roots:
4713
5018
 
4714
- 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):
4715
- new_list.append(centroid)
5019
+ 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):
5020
+ new_list.append(centroid)
4716
5021
 
4717
5022
 
4718
- #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):
4719
- #new_list.append(centroid)
4720
- #print(f"dim_list: {dim_list}, centroid: {centroid}, min_coords: {min_coords}, max_coords: {max_coords}")
5023
+ #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):
5024
+ #new_list.append(centroid)
5025
+ #print(f"dim_list: {dim_list}, centroid: {centroid}, min_coords: {min_coords}, max_coords: {max_coords}")
5026
+ else:
5027
+ for centroid in roots:
5028
+
5029
+ 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):
5030
+ new_list.append(centroid)
5031
+
4721
5032
  else:
4722
- for centroid in roots:
4723
5033
 
4724
- 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):
4725
- new_list.append(centroid)
5034
+ if mode == 1:
5035
+
5036
+ legal = self.edges != 0
5037
+
5038
+ elif mode == 2:
5039
+
5040
+ legal = self.network_overlay != 0
5041
+
5042
+ elif mode == 3:
5043
+
5044
+ legal = self.id_overlay != 0
5045
+
5046
+ if self.nodes is None:
5047
+
5048
+ temp_array = proximity.populate_array(self.node_centroids, shape = legal.shape)
5049
+ else:
5050
+ temp_array = self.nodes
5051
+
5052
+ if dim == 2:
5053
+ volume = np.count_nonzero(legal) * self.xy_scale**2
5054
+ else:
5055
+ volume = np.count_nonzero(legal) * self.z_scale * self.xy_scale**2
5056
+ print(f"Using {volume} for the volume measurement (Volume of provided mask as scaled by xy and z scaling)")
5057
+
5058
+ legal = smart_dilate.compute_distance_transform_distance(legal, sampling = [self.z_scale, self.xy_scale, self.xy_scale]) # Get true distances
5059
+
5060
+ max_avail = np.max(legal) # Most internal point
5061
+ min_legal = factor * max_avail # Values of stuff 25% within the tissue
5062
+
5063
+ legal = legal > min_legal
5064
+
5065
+ if safe:
5066
+ max_r = min_legal
5067
+
5068
+
5069
+ legal = temp_array * legal
5070
+
5071
+ legal = np.unique(legal)
5072
+ if 0 in legal:
5073
+ legal = np.delete(legal, 0)
5074
+ for node in legal:
5075
+ if node in root_ids:
5076
+ new_list.append(self.node_centroids[node])
4726
5077
 
4727
5078
  roots = new_list
4728
5079
  print(f"Utilizing {len(roots)} root points. Note that low n values are unstable.")
4729
5080
  is_subset = True
4730
- else:
4731
- is_subset = False
4732
-
4733
5081
 
4734
5082
 
4735
5083
 
4736
5084
  roots = proximity.convert_centroids_to_array(roots, xy_scale = self.xy_scale, z_scale = self.z_scale)
4737
5085
 
5086
+ n_subset = len(targs)
5087
+
5088
+ # Apply edge correction through mirroring
5089
+ if edgecorrect:
5090
+
5091
+
5092
+ roots, targs = apply_edge_correction_to_ripley(
5093
+ roots, targs, proportion, bounds, dim,
5094
+ node_centroids=self.node_centroids # Pass this for bounds calculation if needed
5095
+ )
5096
+
4738
5097
 
4739
5098
  if dim == 2:
4740
5099
  roots = proximity.convert_augmented_array_to_points(roots)
4741
5100
  targs = proximity.convert_augmented_array_to_points(targs)
4742
5101
 
4743
- r_vals = proximity.generate_r_values(points_array, distance, bounds = bounds, dim = dim, max_proportion=proportion)
5102
+ print(f"Using {len(roots)} root points")
5103
+ r_vals = proximity.generate_r_values(points_array, distance, bounds = bounds, dim = dim, max_proportion=proportion, max_r = max_r)
4744
5104
 
4745
- k_vals = proximity.optimized_ripleys_k(roots, targs, r_vals, bounds=bounds, edge_correction=edgecorrect, dim = dim, is_subset = is_subset)
5105
+ k_vals = proximity.optimized_ripleys_k(roots, targs, r_vals, bounds=bounds, dim = dim, is_subset = is_subset, volume = volume, n_subset = n_subset)
4746
5106
 
4747
5107
  h_vals = proximity.compute_ripleys_h(k_vals, r_vals, dim)
4748
5108
 
@@ -4792,7 +5152,7 @@ class Network_3D:
4792
5152
  search_x, search_z = dilation_length_to_pixels(self._xy_scale, self._z_scale, search, search)
4793
5153
 
4794
5154
 
4795
- num_nodes = np.max(self._nodes)
5155
+ num_nodes = int(np.max(self._nodes))
4796
5156
 
4797
5157
  my_dict = proximity.create_node_dictionary(self._nodes, num_nodes, search_x, search_z, targets = targets, fastdil = fastdil, xy_scale = self._xy_scale, z_scale = self._z_scale, search = search)
4798
5158
 
@@ -4804,7 +5164,7 @@ class Network_3D:
4804
5164
 
4805
5165
  self.remove_edge_weights()
4806
5166
 
4807
- def centroid_array(self, clip = False):
5167
+ def centroid_array(self, clip = False, shape = None):
4808
5168
  """Use the centroids to populate a node array"""
4809
5169
 
4810
5170
  if clip:
@@ -4814,12 +5174,13 @@ class Network_3D:
4814
5174
 
4815
5175
  else:
4816
5176
 
4817
- array = proximity.populate_array(self.node_centroids)
5177
+ array = proximity.populate_array(self.node_centroids, shape = shape)
4818
5178
 
4819
5179
  return array
4820
5180
 
4821
5181
 
4822
5182
 
5183
+
4823
5184
  def random_nodes(self, bounds = None, mask = None):
4824
5185
 
4825
5186
  if self.nodes is not None:
@@ -4938,6 +5299,13 @@ class Network_3D:
4938
5299
 
4939
5300
  return output
4940
5301
 
5302
+ def centroid_umap(self):
5303
+
5304
+ from . import neighborhoods
5305
+
5306
+ neighborhoods.visualize_cluster_composition_umap(self.node_centroids, None, id_dictionary = self.node_identities, graph_label = "Node ID", title = 'UMAP Visualization of Node Centroids')
5307
+
5308
+
4941
5309
  def community_id_info_per_com(self, umap = False, label = False, limit = 0, proportional = False):
4942
5310
 
4943
5311
  community_dict = invert_dict(self.communities)
@@ -5043,21 +5411,23 @@ class Network_3D:
5043
5411
 
5044
5412
  zero_group = {}
5045
5413
 
5414
+ comus = invert_dict(self.communities)
5046
5415
 
5047
- if limit is not None:
5048
-
5049
- coms = invert_dict(self.communities)
5050
5416
 
5417
+ if limit is not None:
5051
5418
 
5052
- for com, nodes in coms.items():
5419
+ for com, nodes in comus.items():
5053
5420
 
5054
5421
  if len(nodes) < limit:
5055
5422
 
5056
5423
  del identities[com]
5057
5424
 
5058
- if count > len(identities):
5059
- print(f"Requested neighborhoods too large for available communities. Using {len(identities)} neighborhoods (max for these coms)")
5060
- count = len(identities)
5425
+ try:
5426
+ if count > len(identities):
5427
+ print(f"Requested neighborhoods too large for available communities. Using {len(identities)} neighborhoods (max for these coms)")
5428
+ count = len(identities)
5429
+ except:
5430
+ pass
5061
5431
 
5062
5432
 
5063
5433
  if mode == 0:
@@ -5068,13 +5438,21 @@ class Network_3D:
5068
5438
  coms = {}
5069
5439
 
5070
5440
  neighbors = {}
5441
+ len_dict = {}
5442
+ inc_count = 0
5071
5443
 
5072
5444
  for i, cluster in enumerate(clusters):
5073
5445
 
5446
+ size = len(cluster)
5447
+ inc_count += size
5448
+
5449
+ len_dict[i + 1] = [size]
5450
+
5074
5451
  for com in cluster: # For community ID per list
5075
5452
 
5076
5453
  coms[com] = i + 1
5077
5454
 
5455
+
5078
5456
  copy_dict = copy.deepcopy(self.communities)
5079
5457
 
5080
5458
  for node, com in copy_dict.items():
@@ -5092,18 +5470,17 @@ class Network_3D:
5092
5470
 
5093
5471
  if len(zero_group) > 0:
5094
5472
  self.communities.update(zero_group)
5473
+ len_dict[0] = [len(comus) - inc_count]
5095
5474
 
5096
5475
 
5097
5476
  identities, id_set = self.community_id_info_per_com()
5098
5477
 
5099
- len_dict = {}
5100
-
5101
5478
  coms = invert_dict(self.communities)
5102
5479
  node_count = len(list(self.communities.keys()))
5103
5480
 
5104
5481
  for com, nodes in coms.items():
5105
5482
 
5106
- len_dict[com] = len(nodes)/node_count
5483
+ len_dict[com].append(len(nodes)/node_count)
5107
5484
 
5108
5485
  matrixes = []
5109
5486
 
@@ -5119,7 +5496,7 @@ class Network_3D:
5119
5496
 
5120
5497
  identities3 = {}
5121
5498
  for iden in identities2:
5122
- identities3[iden] = identities2[iden]/len_dict[iden]
5499
+ identities3[iden] = identities2[iden]/len_dict[iden][1]
5123
5500
 
5124
5501
  output = neighborhoods.plot_dict_heatmap(identities3, id_set2, title = "Neighborhood Heatmap by Proportional Composition of Nodes in Neighborhood vs All Nodes Divided by Neighborhood Total Proportion of All Nodes (val < 1 = underrepresented, val > 1 = overrepresented)", center_at_one = True)
5125
5502
  matrixes.append(output)
@@ -5291,7 +5668,33 @@ class Network_3D:
5291
5668
  pass
5292
5669
 
5293
5670
 
5294
- def nearest_neighbors_avg(self, root, targ, xy_scale = 1, z_scale = 1, num = 1, heatmap = False, threed = True, numpy = False):
5671
+ def nearest_neighbors_avg(self, root, targ, xy_scale = 1, z_scale = 1, num = 1, heatmap = False, threed = True, numpy = False, quant = False):
5672
+
5673
+ def get_theoretical_nearest_neighbor_distance(compare_set, num_neighbors, volume, is_2d=False):
5674
+ """
5675
+ Calculate theoretical expected distance to k-th nearest neighbor
5676
+ assuming random uniform distribution in 2D or 3D space.
5677
+ """
5678
+ import math
5679
+
5680
+ if len(compare_set) == 0 or volume <= 0:
5681
+ raise ValueError("Invalid input: empty set or non-positive volume")
5682
+
5683
+ density = len(compare_set) / volume
5684
+ k = num_neighbors
5685
+
5686
+ if is_2d:
5687
+ # Expected distance to k-th nearest neighbor in 2D
5688
+ # μ1' = Γ(k + 1/2) / (Γ(k) × √(m × π))
5689
+ expected_distance = math.gamma(k + 0.5) / (math.gamma(k) * math.sqrt(density * math.pi))
5690
+ else:
5691
+ # Expected distance to k-th nearest neighbor in 3D
5692
+ # μ1' = Γ(k + 1/3) / (Γ(k) × (m × Φ)^(1/3))
5693
+ # where Φ = π^(3/2) / Γ(3/2 + 1) = π^(3/2) / Γ(5/2) = 4π/3
5694
+ phi_3d = 4 * math.pi / 3 # Volume of unit sphere in 3D
5695
+ expected_distance = math.gamma(k + 1/3) / (math.gamma(k) * (density * phi_3d)**(1/3))
5696
+
5697
+ return expected_distance
5295
5698
 
5296
5699
  root_set = []
5297
5700
 
@@ -5335,6 +5738,14 @@ class Network_3D:
5335
5738
 
5336
5739
  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)
5337
5740
 
5741
+ if quant:
5742
+ try:
5743
+ quant_overlay = node_draw.degree_infect(output, self._nodes, make_floats = True)
5744
+ except:
5745
+ quant_overlay = None
5746
+ else:
5747
+ quant_overlay = None
5748
+
5338
5749
  if heatmap:
5339
5750
 
5340
5751
 
@@ -5345,7 +5756,30 @@ class Network_3D:
5345
5756
  big_array = proximity.convert_centroids_to_array(list(self.node_centroids.values()))
5346
5757
  shape = [np.max(big_array[0, :]) + 1, np.max(big_array[1, :]) + 1, np.max(big_array[2, :]) + 1]
5347
5758
 
5348
- pred = avg
5759
+
5760
+ try:
5761
+ bounds = self.nodes.shape
5762
+ except:
5763
+ try:
5764
+ bounds = self.edges.shape
5765
+ except:
5766
+ try:
5767
+ bounds = self.network_overlay.shape
5768
+ except:
5769
+ try:
5770
+ bounds = self.id_overlay.shape
5771
+ except:
5772
+ big_array = proximity.convert_centroids_to_array(list(self.node_centroids.values()))
5773
+ max_coords = [np.max(big_array[:, 0]), np.max(big_array[:, 1]), np.max(big_array[:, 2])]
5774
+ del big_array
5775
+ volume = bounds[0] * bounds[1] * bounds[2] * self.z_scale * self.xy_scale**2
5776
+ if 1 in bounds or 0 in bounds:
5777
+ is_2d = True
5778
+ else:
5779
+ is_2d = False
5780
+
5781
+ pred = get_theoretical_nearest_neighbor_distance(compare_set, num, volume, is_2d = is_2d)
5782
+ #pred = avg
5349
5783
 
5350
5784
  node_intensity = {}
5351
5785
  import math
@@ -5359,12 +5793,12 @@ class Network_3D:
5359
5793
 
5360
5794
  overlay = neighborhoods.create_node_heatmap(node_intensity, node_centroids, shape = shape, is_3d=threed, labeled_array = self.nodes, colorbar_label="Clustering Intensity", title = title)
5361
5795
 
5362
- return avg, output, overlay
5796
+ return avg, output, overlay, quant_overlay
5363
5797
 
5364
5798
  else:
5365
5799
  neighborhoods.create_node_heatmap(node_intensity, node_centroids, shape = shape, is_3d=threed, labeled_array = None, colorbar_label="Clustering Intensity", title = title)
5366
5800
 
5367
- return avg, output
5801
+ return avg, output, quant_overlay
5368
5802
 
5369
5803
 
5370
5804