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

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.
@@ -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:
644
669
 
645
- # Step 3: Sort the remaining values
646
- sorted_values = np.sort(non_zero_values)
670
+ threshold_value = custom_rad
647
671
 
648
- # Step 4: Determine the threshold for the top proportion%
672
+ else:
673
+ # Step 1: Flatten the array
674
+ flattened = arr.flatten()
675
+
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
+ """
5016
+
5017
+ for centroid in roots:
4710
5018
 
4711
- if dim == 3:
4712
- for centroid in roots:
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)
4713
5021
 
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)
4716
5022
 
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)
4717
5031
 
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}")
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
 
@@ -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:
@@ -5043,21 +5404,23 @@ class Network_3D:
5043
5404
 
5044
5405
  zero_group = {}
5045
5406
 
5407
+ comus = invert_dict(self.communities)
5046
5408
 
5047
- if limit is not None:
5048
-
5049
- coms = invert_dict(self.communities)
5050
5409
 
5410
+ if limit is not None:
5051
5411
 
5052
- for com, nodes in coms.items():
5412
+ for com, nodes in comus.items():
5053
5413
 
5054
5414
  if len(nodes) < limit:
5055
5415
 
5056
5416
  del identities[com]
5057
5417
 
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)
5418
+ try:
5419
+ if count > len(identities):
5420
+ print(f"Requested neighborhoods too large for available communities. Using {len(identities)} neighborhoods (max for these coms)")
5421
+ count = len(identities)
5422
+ except:
5423
+ pass
5061
5424
 
5062
5425
 
5063
5426
  if mode == 0:
@@ -5068,13 +5431,21 @@ class Network_3D:
5068
5431
  coms = {}
5069
5432
 
5070
5433
  neighbors = {}
5434
+ len_dict = {}
5435
+ inc_count = 0
5071
5436
 
5072
5437
  for i, cluster in enumerate(clusters):
5073
5438
 
5439
+ size = len(cluster)
5440
+ inc_count += size
5441
+
5442
+ len_dict[i + 1] = [size]
5443
+
5074
5444
  for com in cluster: # For community ID per list
5075
5445
 
5076
5446
  coms[com] = i + 1
5077
5447
 
5448
+
5078
5449
  copy_dict = copy.deepcopy(self.communities)
5079
5450
 
5080
5451
  for node, com in copy_dict.items():
@@ -5092,18 +5463,17 @@ class Network_3D:
5092
5463
 
5093
5464
  if len(zero_group) > 0:
5094
5465
  self.communities.update(zero_group)
5466
+ len_dict[0] = [len(comus) - inc_count]
5095
5467
 
5096
5468
 
5097
5469
  identities, id_set = self.community_id_info_per_com()
5098
5470
 
5099
- len_dict = {}
5100
-
5101
5471
  coms = invert_dict(self.communities)
5102
5472
  node_count = len(list(self.communities.keys()))
5103
5473
 
5104
5474
  for com, nodes in coms.items():
5105
5475
 
5106
- len_dict[com] = len(nodes)/node_count
5476
+ len_dict[com].append(len(nodes)/node_count)
5107
5477
 
5108
5478
  matrixes = []
5109
5479
 
@@ -5119,7 +5489,7 @@ class Network_3D:
5119
5489
 
5120
5490
  identities3 = {}
5121
5491
  for iden in identities2:
5122
- identities3[iden] = identities2[iden]/len_dict[iden]
5492
+ identities3[iden] = identities2[iden]/len_dict[iden][1]
5123
5493
 
5124
5494
  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
5495
  matrixes.append(output)
@@ -5291,7 +5661,33 @@ class Network_3D:
5291
5661
  pass
5292
5662
 
5293
5663
 
5294
- def nearest_neighbors_avg(self, root, targ, xy_scale = 1, z_scale = 1, num = 1, heatmap = False, threed = True, numpy = False):
5664
+ def nearest_neighbors_avg(self, root, targ, xy_scale = 1, z_scale = 1, num = 1, heatmap = False, threed = True, numpy = False, quant = False):
5665
+
5666
+ def get_theoretical_nearest_neighbor_distance(compare_set, num_neighbors, volume, is_2d=False):
5667
+ """
5668
+ Calculate theoretical expected distance to k-th nearest neighbor
5669
+ assuming random uniform distribution in 2D or 3D space.
5670
+ """
5671
+ import math
5672
+
5673
+ if len(compare_set) == 0 or volume <= 0:
5674
+ raise ValueError("Invalid input: empty set or non-positive volume")
5675
+
5676
+ density = len(compare_set) / volume
5677
+ k = num_neighbors
5678
+
5679
+ if is_2d:
5680
+ # Expected distance to k-th nearest neighbor in 2D
5681
+ # μ1' = Γ(k + 1/2) / (Γ(k) × √(m × π))
5682
+ expected_distance = math.gamma(k + 0.5) / (math.gamma(k) * math.sqrt(density * math.pi))
5683
+ else:
5684
+ # Expected distance to k-th nearest neighbor in 3D
5685
+ # μ1' = Γ(k + 1/3) / (Γ(k) × (m × Φ)^(1/3))
5686
+ # where Φ = π^(3/2) / Γ(3/2 + 1) = π^(3/2) / Γ(5/2) = 4π/3
5687
+ phi_3d = 4 * math.pi / 3 # Volume of unit sphere in 3D
5688
+ expected_distance = math.gamma(k + 1/3) / (math.gamma(k) * (density * phi_3d)**(1/3))
5689
+
5690
+ return expected_distance
5295
5691
 
5296
5692
  root_set = []
5297
5693
 
@@ -5335,6 +5731,14 @@ class Network_3D:
5335
5731
 
5336
5732
  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
5733
 
5734
+ if quant:
5735
+ try:
5736
+ quant_overlay = node_draw.degree_infect(output, self._nodes, make_floats = True)
5737
+ except:
5738
+ quant_overlay = None
5739
+ else:
5740
+ quant_overlay = None
5741
+
5338
5742
  if heatmap:
5339
5743
 
5340
5744
 
@@ -5345,7 +5749,30 @@ class Network_3D:
5345
5749
  big_array = proximity.convert_centroids_to_array(list(self.node_centroids.values()))
5346
5750
  shape = [np.max(big_array[0, :]) + 1, np.max(big_array[1, :]) + 1, np.max(big_array[2, :]) + 1]
5347
5751
 
5348
- pred = avg
5752
+
5753
+ try:
5754
+ bounds = self.nodes.shape
5755
+ except:
5756
+ try:
5757
+ bounds = self.edges.shape
5758
+ except:
5759
+ try:
5760
+ bounds = self.network_overlay.shape
5761
+ except:
5762
+ try:
5763
+ bounds = self.id_overlay.shape
5764
+ except:
5765
+ big_array = proximity.convert_centroids_to_array(list(self.node_centroids.values()))
5766
+ max_coords = [np.max(big_array[:, 0]), np.max(big_array[:, 1]), np.max(big_array[:, 2])]
5767
+ del big_array
5768
+ volume = bounds[0] * bounds[1] * bounds[2] * self.z_scale * self.xy_scale**2
5769
+ if 1 in bounds or 0 in bounds:
5770
+ is_2d = True
5771
+ else:
5772
+ is_2d = False
5773
+
5774
+ pred = get_theoretical_nearest_neighbor_distance(compare_set, num, volume, is_2d = is_2d)
5775
+ #pred = avg
5349
5776
 
5350
5777
  node_intensity = {}
5351
5778
  import math
@@ -5359,12 +5786,12 @@ class Network_3D:
5359
5786
 
5360
5787
  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
5788
 
5362
- return avg, output, overlay
5789
+ return avg, output, overlay, quant_overlay
5363
5790
 
5364
5791
  else:
5365
5792
  neighborhoods.create_node_heatmap(node_intensity, node_centroids, shape = shape, is_3d=threed, labeled_array = None, colorbar_label="Clustering Intensity", title = title)
5366
5793
 
5367
- return avg, output
5794
+ return avg, output, quant_overlay
5368
5795
 
5369
5796
 
5370
5797