objscale 0.1.2__tar.gz → 0.1.4__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.4
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
  import numpy as np
2
2
  from scipy.ndimage import label
3
- from numba import njit
3
+ from numba import njit, prange
4
4
  from numba.typed import List
5
5
  from warnings import warn
6
6
  from skimage.segmentation import clear_border
@@ -90,24 +90,33 @@ def get_structure_props(array, x_sizes, y_sizes, structure = np.array([[0, 1, 0]
90
90
  return p,a,h,w
91
91
 
92
92
 
93
- @njit()
93
+ @njit(parallel=True)
94
94
  def _get_structure_props_helper(labelled_array, separated_structure_indices, x_sizes, y_sizes):
95
-
96
- p, a, = [],[]
97
- h, w = [],[]
98
95
 
99
- for indices in separated_structure_indices:
96
+
97
+ # Preallocate arrays
98
+ n_structures = len(separated_structure_indices)
99
+ p = np.empty(n_structures, dtype=np.float32)
100
+ a = np.empty(n_structures, dtype=np.float32)
101
+ h = np.empty(n_structures, dtype=np.float32)
102
+ w = np.empty(n_structures, dtype=np.float32)
103
+
104
+
105
+ for iteration in prange(len(separated_structure_indices)):
106
+ iteration = np.int64(iteration)
107
+ structure_coords = separated_structure_indices[iteration]
100
108
  perimeter = 0
101
109
  area = 0
102
110
 
103
- y_coords_structure = np.array([c[0] for c in indices])
104
- x_coords_structure = np.array([c[1] for c in indices])
111
+ y_coords_structure = np.array([c[0] for c in structure_coords])
112
+ x_coords_structure = np.array([c[1] for c in structure_coords])
105
113
  unique_y_coords = []
106
114
  unique_x_coords = []
107
115
  height = 0
108
116
  width = 0
109
117
 
110
- for (i,j) in indices:
118
+ for i,j in structure_coords:
119
+
111
120
  # Height, Width
112
121
  if i not in unique_y_coords:
113
122
  unique_y_coords.append(i)
@@ -143,14 +152,16 @@ def _get_structure_props_helper(labelled_array, separated_structure_indices, x_s
143
152
  area += y_sizes[i,j] * x_sizes[i,j]
144
153
 
145
154
 
146
- if area != 0:
147
- p.append(perimeter)
148
- a.append(area)
149
- h.append(height)
150
- w.append(width)
151
-
152
-
153
- return p, a, h, w
155
+ if area != 0:
156
+ p[iteration] = perimeter
157
+ a[iteration] = area
158
+ h[iteration] = height
159
+ w[iteration] = width
160
+ # valid_count += 1
161
+
162
+ # Return only the valid entries
163
+ valid_mask = (a>0)
164
+ return p[valid_mask], a[valid_mask], h[valid_mask], w[valid_mask]
154
165
 
155
166
 
156
167
  def label_periodic_boundaries(labelled_array, wrap):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: objscale
3
- Version: 0.1.2
3
+ Version: 0.1.4
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.4"
8
8
  description = "Object-based analysis functions for fractal dimensions and size distributions"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -7,6 +7,7 @@ Creates 4 percolation lattices and calculates all parameters
7
7
  import numpy as np
8
8
  import matplotlib.pyplot as plt
9
9
  import sys
10
+ import time
10
11
  sys.path.insert(0, '..')
11
12
  import objscale
12
13
 
@@ -36,6 +37,7 @@ def main():
36
37
  print("\n=== CALCULATING PARAMETERS ===")
37
38
 
38
39
  # Calculate power-law exponents and get distribution data
40
+ s = time.time()
39
41
  print("Calculating area power-law exponent and distribution...")
40
42
  (area_exponent, area_error), (area_sizes, area_counts) = objscale.finite_array_powerlaw_exponent(
41
43
  arrays, 'area', bins=50, min_threshold=10, return_counts=True
@@ -46,13 +48,14 @@ def main():
46
48
  arrays, 'perimeter', bins=50, min_threshold=10, return_counts=True
47
49
  )
48
50
 
49
- print(f"Area exponent: {area_exponent:.3f} ± {area_error:.3f}")
50
- print(f"Perimeter exponent: {perim_exponent:.3f} ± {perim_error:.3f}")
51
+ print(f'Power law exponents took {time.time()-s:.02f} seconds')
52
+ print(f" Area exponent: {area_exponent:.3f} ± {area_error:.3f}")
53
+ print(f" Perimeter exponent: {perim_exponent:.3f} ± {perim_error:.3f}")
51
54
 
52
55
  # Calculate correlation dimension
53
56
  print("Calculating correlation dimension...")
54
57
  corr_dim, corr_error, corr_lengths, corr_integrals = objscale.ensemble_correlation_dimension(
55
- arrays, return_C_l=True, point_reduction_factor=1000
58
+ arrays, return_C_l=True, point_reduction_factor=1000, maxlength='auto', interior_circles_only=False
56
59
  )
57
60
  print(f"Correlation dimension: {corr_dim:.3f} ± {corr_error:.3f}")
58
61
 
File without changes
File without changes
File without changes
File without changes