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.
- {objscale-0.1.2 → objscale-0.1.4}/PKG-INFO +1 -1
- {objscale-0.1.2 → objscale-0.1.4}/objscale/__init__.py +1 -1
- {objscale-0.1.2 → objscale-0.1.4}/objscale/_fractal_dimensions.py +63 -20
- {objscale-0.1.2 → objscale-0.1.4}/objscale/_object_analysis.py +28 -17
- {objscale-0.1.2 → objscale-0.1.4}/objscale.egg-info/PKG-INFO +1 -1
- {objscale-0.1.2 → objscale-0.1.4}/pyproject.toml +1 -1
- {objscale-0.1.2 → objscale-0.1.4}/tests/test_objscale.py +6 -3
- {objscale-0.1.2 → objscale-0.1.4}/LICENSE +0 -0
- {objscale-0.1.2 → objscale-0.1.4}/README.md +0 -0
- {objscale-0.1.2 → objscale-0.1.4}/objscale/_size_distributions.py +0 -0
- {objscale-0.1.2 → objscale-0.1.4}/objscale/_utils.py +0 -0
- {objscale-0.1.2 → objscale-0.1.4}/objscale.egg-info/SOURCES.txt +0 -0
- {objscale-0.1.2 → objscale-0.1.4}/objscale.egg-info/dependency_links.txt +0 -0
- {objscale-0.1.2 → objscale-0.1.4}/objscale.egg-info/requires.txt +0 -0
- {objscale-0.1.2 → objscale-0.1.4}/objscale.egg-info/top_level.txt +0 -0
- {objscale-0.1.2 → objscale-0.1.4}/setup.cfg +0 -0
|
@@ -50,7 +50,7 @@ __all__ = [
|
|
|
50
50
|
'encase_in_value',
|
|
51
51
|
]
|
|
52
52
|
|
|
53
|
-
__version__ = "0.1.
|
|
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,
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
the
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
104
|
-
x_coords_structure = np.array([c[1] for c in
|
|
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
|
|
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
|
|
148
|
-
a
|
|
149
|
-
h
|
|
150
|
-
w
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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):
|
|
@@ -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
|
|
50
|
-
print(f"
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|