objscale 0.1.2__tar.gz → 0.1.3__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: objscale
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Object-based analysis functions for fractal dimensions and size distributions
5
5
  Author-email: Thomas DeWitt <thomas.dewitt@utah.edu>
6
6
  License: MIT
@@ -50,7 +50,7 @@ __all__ = [
50
50
  'encase_in_value',
51
51
  ]
52
52
 
53
- __version__ = "0.1.2"
53
+ __version__ = "0.1.3"
54
54
  __author__ = "Thomas DeWitt"
55
55
  __email__ = "thomas.dewitt@utah.edu"
56
56
  __description__ = "Object-based analysis functions for fractal dimensions and size distributions"
@@ -8,7 +8,7 @@ from ._object_analysis import remove_structures_touching_border_nan, remove_stru
8
8
  from ._utils import linear_regression, encase_in_value
9
9
 
10
10
 
11
- def ensemble_correlation_dimension(arrays, x_sizes=None, y_sizes=None, middle_ninth=True, return_C_l=False, bins=None, point_reduction_factor=1, nbins=50):
11
+ def ensemble_correlation_dimension(arrays, x_sizes=None, y_sizes=None, minlength='auto', maxlength='auto', interior_circles_only=True, return_C_l=False, bins=None, point_reduction_factor=1, nbins=50):
12
12
  """
13
13
  Calculate the correlation dimension D where C_l ∝ l^D for binary arrays.
14
14
 
@@ -24,9 +24,16 @@ def ensemble_correlation_dimension(arrays, x_sizes=None, y_sizes=None, middle_ni
24
24
  Pixel sizes in the y direction. If None, assume all pixel dimensions are 1.
25
25
  If np.ndarray, use these for each array in 'arrays'. If list, assume
26
26
  y_sizes[i] corresponds to arrays[i].
27
- middle_ninth : bool, default=True
28
- For each pixel, only use distances between that pixel and pixels within
29
- the central 9th section of the array. This reduces boundary effects.
27
+ minlength : str or float, default='auto'
28
+ Minimum length scale for correlation calculation. If 'auto', uses 3 times
29
+ the minimum pixel size.
30
+ maxlength : str or float, default='auto'
31
+ Maximum length scale for correlation calculation. If 'auto', uses 0.1 times
32
+ the minimum array dimension.
33
+ interior_circles_only : bool, default=True
34
+ If True, only use circle centers that are at least maxlength distance from
35
+ all array edges to avoid boundary effects. In other words, only use circles
36
+ that are fully contained within the array. Recommended!
30
37
  return_C_l : bool, default=False
31
38
  If True, return dimension, error, bins, C_l. Otherwise, return dimension, error.
32
39
  bins : None, int, or array-like, optional
@@ -63,22 +70,34 @@ def ensemble_correlation_dimension(arrays, x_sizes=None, y_sizes=None, middle_ni
63
70
  h = x_sizes.shape[0]
64
71
  w = x_sizes.shape[1]
65
72
 
66
- if middle_ninth:
67
- # For the most conservative estimate, the maximum radius should be
68
- # the minimum distance from the middle ninth boundary to the array edge
69
- # This should be 1/3 of the array shape
70
- # This ensures no circle extends outside the domain
71
- # Note this assumes rectangular domain
72
- maxlength = min(h/3, w/3)
73
- else: # min(width, height) of entire array, where width, height are calculated in the center
74
- maxlength = np.sqrt((locations_x[int(h/2), 0]-locations_x[int(h/2), w-1])**2 + (locations_y[0, int(w/2)]-locations_y[h-1, int(w/2)])**2)
75
-
76
- minlength = 3*min(np.nanmin(x_sizes), np.nanmin(y_sizes))
73
+ if maxlength == 'auto':
74
+ # One tenth of min(width, height) of entire array, where width, height are calculated in the center
75
+ maxlength = 0.1 * min((locations_x[int(h/2), w-1]-locations_x[int(h/2), 0]), (locations_y[h-1, int(w/2)]-locations_y[0, int(w/2)]))
76
+
77
+ if minlength == 'auto': minlength = 3*min(np.nanmin(x_sizes), np.nanmin(y_sizes))
78
+
79
+ # Basic validation checks
80
+ if np.any(np.isnan(x_sizes)) or np.any(np.isnan(y_sizes)):
81
+ raise ValueError("x_sizes and y_sizes cannot contain NaN values")
82
+
83
+ if np.any(x_sizes <= 0) or np.any(y_sizes <= 0):
84
+ raise ValueError("x_sizes and y_sizes must be positive")
85
+
77
86
  if bins is None:
78
87
  bins = np.geomspace(minlength, maxlength, nbins)
79
88
  elif isinstance(bins, int):
80
89
  bins = np.geomspace(minlength, maxlength, bins)
81
90
 
91
+ # range of scale checks
92
+ # check these with the actual calculated bins for greatest relevance
93
+ if bins[-1] <= bins[0]:
94
+ raise ValueError(f"bin maximum length ({bins[-1]:.3f}) must be greater than bin minimum length ({bins[0]:.3f}); or if bins are passed, they must be increasing. Did you pass invalid values for minlength/maxlength?")
95
+
96
+ if bins[-1] / bins[0] < 10:
97
+ raise ValueError(f"Available scale ratio ({maxlength/minlength:.2f}) is less than 10. "
98
+ f"Need at least one order of magnitude separation for reliable dimension estimation.")
99
+
100
+
82
101
  C_l = np.zeros(bins.shape)
83
102
 
84
103
  for array in arrays:
@@ -88,14 +107,38 @@ def ensemble_correlation_dimension(arrays, x_sizes=None, y_sizes=None, middle_ni
88
107
 
89
108
  all_boundary_coordinates = get_coords_of_boundaries(array)
90
109
 
91
- if middle_ninth:
92
- middle_coordinates = all_boundary_coordinates[np.argwhere((all_boundary_coordinates[:,0]<int(2*h/3)) & (all_boundary_coordinates[:,0]>int(h/3))).flatten()]
93
- middle_coordinates = middle_coordinates[np.argwhere((middle_coordinates[:,1]<int(2*w/3)) & (middle_coordinates[:,1]>int(w/3))).flatten()]
110
+ if interior_circles_only:
111
+ # Calculate distance from each boundary coordinate to all array edges
112
+ coord_locations_x = locations_x[all_boundary_coordinates[:,0], all_boundary_coordinates[:,1]]
113
+ coord_locations_y = locations_y[all_boundary_coordinates[:,0], all_boundary_coordinates[:,1]]
114
+
115
+ # Distance to each edge for all coordinates
116
+ dist_to_left = coord_locations_x - locations_x[int(h/2), 0]
117
+ dist_to_right = locations_x[int(h/2), w-1] - coord_locations_x
118
+ dist_to_top = coord_locations_y - locations_y[0, int(w/2)]
119
+ dist_to_bottom = locations_y[h-1, int(w/2)] - coord_locations_y
120
+
121
+ # Find coordinates that are at least maxlength from ALL edges
122
+ min_dist_to_any_edge = np.minimum.reduce([dist_to_left, dist_to_right, dist_to_top, dist_to_bottom])
123
+ interior_mask = min_dist_to_any_edge >= maxlength
124
+
125
+ circle_centers = all_boundary_coordinates[interior_mask]
94
126
 
95
- circle_centers = middle_coordinates
127
+ if len(circle_centers) == 0:
128
+ raise ValueError(f"No circle centers remain after interior filtering. "
129
+ f"Decrease maxlength (currently {maxlength:.3f}) or consider whether interior_circles_only=True is appropriate.")
130
+
131
+ # Check if sufficient circle centers remain
132
+ elif len(circle_centers)//point_reduction_factor < 10:
133
+ warn(f"Only {len(circle_centers)} circle centers remain after interior filtering. "
134
+ f"Consider decreasing maxlength or reducing point_reduction_factor for better statistics.")
135
+
96
136
  else:
97
137
  circle_centers = all_boundary_coordinates
98
138
 
139
+ if len(circle_centers)//point_reduction_factor<1:
140
+ raise ValueError(f'uh oh no circle center locations! is point_reduction_factor={point_reduction_factor} too high?')
141
+
99
142
  if point_reduction_factor>1:
100
143
  circle_centers = circle_centers[np.random.choice(np.arange(len(circle_centers)), int(len(circle_centers)/point_reduction_factor), replace=False)]
101
144
  elif point_reduction_factor<1: raise ValueError('point_reduction_factor must be >= 1')
@@ -692,7 +735,7 @@ def _label_size_helper(labelled_array, separated_structure_indices, labelled_wit
692
735
  elif j == labelled_array.shape[1]-1 and labelled_array[i, 0] == 0: perimeter += y_sizes[i,j]
693
736
 
694
737
  if j != 0 and labelled_array[i, j-1] == 0: perimeter += y_sizes[i,j]
695
- elif j == 0 and labelled_array[i, 0] == 0: perimeter += y_sizes[i,j]
738
+ elif j == 0 and labelled_array[i, labelled_array.shape[1]-1] == 0: perimeter += y_sizes[i,j]
696
739
 
697
740
  # Area:
698
741
  area += y_sizes[i,j] * x_sizes[i,j]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: objscale
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Object-based analysis functions for fractal dimensions and size distributions
5
5
  Author-email: Thomas DeWitt <thomas.dewitt@utah.edu>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "objscale"
7
- version = "0.1.2"
7
+ version = "0.1.3"
8
8
  description = "Object-based analysis functions for fractal dimensions and size distributions"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -27,7 +27,7 @@ def main():
27
27
 
28
28
  # Create 4 percolation lattices
29
29
  arrays = []
30
- for i in range(4):
30
+ for i in range(2):
31
31
  print(f"Creating array {i+1}/4...")
32
32
  array = create_percolation_lattice(size, P_C)
33
33
  arrays.append(array)
@@ -52,7 +52,7 @@ def main():
52
52
  # Calculate correlation dimension
53
53
  print("Calculating correlation dimension...")
54
54
  corr_dim, corr_error, corr_lengths, corr_integrals = objscale.ensemble_correlation_dimension(
55
- arrays, return_C_l=True, point_reduction_factor=1000
55
+ arrays, return_C_l=True, point_reduction_factor=1000, maxlength='auto', interior_circles_only=False
56
56
  )
57
57
  print(f"Correlation dimension: {corr_dim:.3f} ± {corr_error:.3f}")
58
58
 
File without changes
File without changes
File without changes
File without changes