From 43e75680fd3b3c81244be861e651d0fa4cbeacba Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Wed, 7 Jun 2023 18:23:18 +1000 Subject: [PATCH 01/12] Use regionprops instead of image == label when grain-finding --- defdap/hrdic.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/defdap/hrdic.py b/defdap/hrdic.py index 8a1c852..89454da 100755 --- a/defdap/hrdic.py +++ b/defdap/hrdic.py @@ -18,6 +18,7 @@ import inspect from skimage import transform as tf +from skimage import measure from scipy.stats import mode from scipy.ndimage import binary_dilation @@ -618,6 +619,8 @@ def find_grains(self, algorithm=None, min_grain_size=10): new = np.concatenate((neg_vals, np.arange(1, len(ebsd_grain_ids) + 1))) index = np.digitize(grains.ravel(), old, right=True) grains = new[index].reshape(self.shape) + grainprops = measure.regionprops(grains) + props_dict = {prop.label: prop for prop in grainprops} for dic_grain_id, ebsd_grain_id in enumerate(ebsd_grain_ids): yield dic_grain_id / len(ebsd_grain_ids) @@ -626,8 +629,10 @@ def find_grains(self, algorithm=None, min_grain_size=10): grain = Grain(dic_grain_id, self, group_id) # Find (x,y) coordinates and corresponding max shears of grain - coords = np.argwhere(grains == dic_grain_id + 1) # (y,x) - grain.data.point = [(x, y) for y, x in coords] + # coords = np.argwhere(grains == dic_grain_id + 1) # (y,x) + coords = props_dict[dic_grain_id + 1].coords # (r, c) + # grain.data.point = [(x, y) for y, x in coords] + grain.data.point = np.flip(coords, axis=1) # (x, y) # Assign EBSD grain ID to DIC grain and increment grain list grain.ebsd_grain = self.ebsd_map[ebsd_grain_id - 1] From bc04a5c1c56c7e8d52d904a27e6fd59a4028140e Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Wed, 25 Oct 2023 19:16:51 +1100 Subject: [PATCH 02/12] Use numba for accelerated flood fill --- defdap/_accelerated.py | 64 ++++++++++++++++++++++++++++++++++++++++++ defdap/ebsd.py | 56 ++++++++---------------------------- setup.py | 1 + 3 files changed, 76 insertions(+), 45 deletions(-) create mode 100644 defdap/_accelerated.py diff --git a/defdap/_accelerated.py b/defdap/_accelerated.py new file mode 100644 index 0000000..400002e --- /dev/null +++ b/defdap/_accelerated.py @@ -0,0 +1,64 @@ +from numba import njit + + +@njit +def find_first(arr): + for i in range(len(arr)): + if arr[i]: + return i + + +@njit +def flood_fill(seed, index, points_remaining, grains, boundary_x, boundary_y, + added_coords): + x, y = seed + grains[y, x] = index + points_remaining[y, x] = False + edge = [seed] + added_coords[0] = seed + npoints = 1 + + while edge: + x, y = edge.pop() + moves = [(x+1, y), (x-1, y), (x, y+1), (x, y-1)] + # get rid of any that go out of the map area + if x <= 0: + moves.pop(1) + elif x > grains.shape[1] - 2: + moves.pop(0) + if y <= 0: + moves.pop(-1) + elif y > grains.shape[0] - 2: + moves.pop(-2) + + for (s, t) in moves: + if grains[t, s] > 0: + continue + + add_point = False + + if t == y: + # moving horizontally + if s > x: + # moving right + add_point = not boundary_x[y, x] + else: + # moving left + add_point = not boundary_x[t, s] + else: + # moving vertically + if t > y: + # moving down + add_point = not boundary_y[y, x] + else: + # moving up + add_point = not boundary_y[t, s] + + if add_point: + added_coords[npoints] = s, t + grains[t, s] = index + points_remaining[t, s] = False + npoints += 1 + edge.append((s, t)) + + return added_coords[:npoints] diff --git a/defdap/ebsd.py b/defdap/ebsd.py index ed93945..33a6c6d 100755 --- a/defdap/ebsd.py +++ b/defdap/ebsd.py @@ -25,6 +25,7 @@ from defdap.file_writers import EBSDDataWriter from defdap.quat import Quat from defdap import base +from defdap._accelerated import flood_fill from defdap import defaults from defdap.plotting import MapPlot @@ -881,12 +882,14 @@ def find_grains(self, min_grain_size=10): # Loop until all points (except boundaries) have been assigned # to a grain or ignored i = 0 + added_coords_buffer = np.zeros((boundary_im_y.size, 2), dtype=np.intp) while found_point >= 0: # Flood fill first unknown point and return grain object seed = np.unravel_index(next_point, self.shape) grain = self.flood_fill( (seed[1], seed[0]), grain_index, points_left, grains, - boundary_im_x, boundary_im_y, group_id + boundary_im_x, boundary_im_y, group_id, + added_coords_buffer, ) if len(grain) < min_grain_size: @@ -963,7 +966,7 @@ def plot_grain_map(self, **kwargs): return plot def flood_fill(self, seed, index, points_left, grains, boundary_im_x, - boundary_im_y, group_id): + boundary_im_y, group_id, added_coords_buffer=None): """Flood fill algorithm that uses the x and y boundary arrays to fill a connected area around the seed point. The points are inserted into a grain object and the grain map array is updated. @@ -991,49 +994,12 @@ def flood_fill(self, seed, index, points_left, grains, boundary_im_x, grains[y, x] = index points_left[y, x] = False edge = [seed] - - while edge: - x, y = edge.pop(0) - - moves = [(x+1, y), (x-1, y), (x, y+1), (x, y-1)] - # get rid of any that go out of the map area - if x <= 0: - moves.pop(1) - elif x >= self.shape[1] - 1: - moves.pop(0) - if y <= 0: - moves.pop(-1) - elif y >= self.shape[0] - 1: - moves.pop(-2) - - for (s, t) in moves: - if grains[t, s] > 0: - continue - - add_point = False - - if t == y: - # moving horizontally - if s > x: - # moving right - add_point = not boundary_im_x[y, x] - else: - # moving left - add_point = not boundary_im_x[t, s] - else: - # moving vertically - if t > y: - # moving down - add_point = not boundary_im_y[y, x] - else: - # moving up - add_point = not boundary_im_y[t, s] - - if add_point: - grain.add_point((s, t)) - grains[t, s] = index - points_left[t, s] = False - edge.append((s, t)) + added_coords = flood_fill( + seed, index, + points_left, grains, + boundary_im_x, boundary_im_y, + added_coords_buffer) + grain.data.point = list(added_coords) return grain diff --git a/setup.py b/setup.py index 06d2323..9bc95a2 100644 --- a/setup.py +++ b/setup.py @@ -61,6 +61,7 @@ def get_version(): 'peakutils', 'matplotlib_scalebar', 'networkx', + 'numba', ], extras_require={ 'testing': ['pytest', 'coverage', 'pytest-cov', 'pytest_cases'], From 9a0664c2ea4cf9189dff4a7986da727d3803a58c Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Wed, 21 Jun 2023 16:45:34 +1000 Subject: [PATCH 03/12] Use numba in hrdic flood fill --- defdap/_accelerated.py | 42 ++++++++++++++++++++++++++++++++++++ defdap/hrdic.py | 49 +++++++++++------------------------------- 2 files changed, 54 insertions(+), 37 deletions(-) diff --git a/defdap/_accelerated.py b/defdap/_accelerated.py index 400002e..b24e916 100644 --- a/defdap/_accelerated.py +++ b/defdap/_accelerated.py @@ -62,3 +62,45 @@ def flood_fill(seed, index, points_remaining, grains, boundary_x, boundary_y, edge.append((s, t)) return added_coords[:npoints] + + +@njit +def flood_fill_dic(seed, index, points_remaining, grains, added_coords): + # add first point to the grain + x, y = seed + grains[y, x] = index + points_remaining[y, x] = False + edge = [seed] + npoints = 1 + + while edge: + x, y = edge.pop() + + moves = [(x+1, y), (x-1, y), (x, y+1), (x, y-1)] + # get rid of any that go out of the map area + if x <= 0: + moves.pop(1) + elif x >= grains.shape[1] - 1: + moves.pop(0) + if y <= 0: + moves.pop(-1) + elif y >= grains.shape[0] - 1: + moves.pop(-2) + + for (s, t) in moves: + add_point = False + + if grains[t, s] == 0: + add_point = True + edge.append((s, t)) + + elif grains[t, s] == -1 and (s > x or t > y): + add_point = True + + if add_point: + added_coords[npoints] = (s, t) + grains[t, s] = index + points_remaining[t, s] = False + npoints += 1 + + return added_coords[:npoints] \ No newline at end of file diff --git a/defdap/hrdic.py b/defdap/hrdic.py index 89454da..334f50e 100755 --- a/defdap/hrdic.py +++ b/defdap/hrdic.py @@ -25,6 +25,7 @@ import peakutils +from defdap._accelerated import flood_fill_dic from defdap.utils import Datastore from defdap.file_readers import DICDataLoader from defdap import base @@ -645,6 +646,9 @@ def find_grains(self, algorithm=None, min_grain_size=10): # List of points where no grain has been set yet points_left = grains == 0 + added_coords_buffer = np.zeros( + (points_left.size, 2), dtype=np.intp + ) total_points = points_left.sum() found_point = 0 next_point = points_left.tobytes().find(b'\x01') @@ -658,7 +662,8 @@ def find_grains(self, algorithm=None, min_grain_size=10): # Flood fill first unknown point and return grain object seed = np.unravel_index(next_point, self.shape) grain = self.flood_fill( - (seed[1], seed[0]), grain_index, points_left, grains, group_id + (seed[1], seed[0]), grain_index, points_left, grains, + group_id, added_coords_buffer, ) if len(grain) < min_grain_size: @@ -716,7 +721,8 @@ def find_grains(self, algorithm=None, min_grain_size=10): self._grains = grain_list return grains - def flood_fill(self, seed, index, points_left, grains, group_id): + def flood_fill(self, seed, index, points_left, grains, group_id, + added_coords_buffer=None): """Flood fill algorithm that uses the combined x and y boundary array to fill a connected area around the seed point. The points are inserted into a grain object and the grain map array is updated. @@ -739,41 +745,10 @@ def flood_fill(self, seed, index, points_left, grains, group_id): # create new grain grain = Grain(index - 1, self, group_id) - # add first point to the grain - x, y = seed - grain.add_point(seed) - grains[y, x] = index - points_left[y, x] = False - edge = [seed] - - while edge: - x, y = edge.pop(0) - - moves = [(x+1, y), (x-1, y), (x, y+1), (x, y-1)] - # get rid of any that go out of the map area - if x <= 0: - moves.pop(1) - elif x >= self.shape[1] - 1: - moves.pop(0) - if y <= 0: - moves.pop(-1) - elif y >= self.shape[0] - 1: - moves.pop(-2) - - for (s, t) in moves: - add_point = False - - if grains[t, s] == 0: - add_point = True - edge.append((s, t)) - - elif grains[t, s] == -1 and (s > x or t > y): - add_point = True - - if add_point: - grain.add_point((s, t)) - grains[t, s] = index - points_left[t, s] = False + added_coords = flood_fill_dic(seed, index, points_left, grains, + added_coords_buffer) + + grain.data.point = list(added_coords) return grain From a3e9882e19ae5ee7609715a3e5cb077292c09cce Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Tue, 31 Oct 2023 12:42:09 +1100 Subject: [PATCH 04/12] Copy buffer after flood fill to avoid overwriting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Using a NumPy array as a buffer increases performance, but leaves you open to bugs due to reusing the same buffer and overwriting it, which is what was happening. 😅 There's a better-performing fix still: use a buffer as big as the image, but advance the start index in the buffer as you finish each flood fill. This guarantees optimal space and time requirements (no need for copies, or a huge buffer that is mostly unused). --- defdap/_accelerated.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/defdap/_accelerated.py b/defdap/_accelerated.py index b24e916..b9cae29 100644 --- a/defdap/_accelerated.py +++ b/defdap/_accelerated.py @@ -1,4 +1,5 @@ from numba import njit +import numpy as np @njit @@ -61,7 +62,7 @@ def flood_fill(seed, index, points_remaining, grains, boundary_x, boundary_y, npoints += 1 edge.append((s, t)) - return added_coords[:npoints] + return np.copy(added_coords[:npoints]) @njit @@ -103,4 +104,4 @@ def flood_fill_dic(seed, index, points_remaining, grains, added_coords): points_remaining[t, s] = False npoints += 1 - return added_coords[:npoints] \ No newline at end of file + return np.copy(added_coords[:npoints]) From f8fc4f384ab1f3112d1b23bc3cadb8d5ab25ff40 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Tue, 31 Oct 2023 22:32:22 +1100 Subject: [PATCH 05/12] Add missing initial coordinate from flood-fill --- defdap/_accelerated.py | 1 + 1 file changed, 1 insertion(+) diff --git a/defdap/_accelerated.py b/defdap/_accelerated.py index b9cae29..eafda25 100644 --- a/defdap/_accelerated.py +++ b/defdap/_accelerated.py @@ -72,6 +72,7 @@ def flood_fill_dic(seed, index, points_remaining, grains, added_coords): grains[y, x] = index points_remaining[y, x] = False edge = [seed] + added_coords[0] = seed npoints = 1 while edge: From dfd2998a25c67b9e07b0895711765fe0ddfdc8ef Mon Sep 17 00:00:00 2001 From: Michael Atkinson Date: Mon, 6 Nov 2023 22:55:46 +0000 Subject: [PATCH 06/12] Change `grain.data.point` to an array Update tests to pass --- defdap/base.py | 20 ++------------------ defdap/ebsd.py | 4 +--- defdap/hrdic.py | 10 +++------- tests/test_ebsd.py | 6 +++--- tests/test_hrdic.py | 6 +++--- 5 files changed, 12 insertions(+), 34 deletions(-) diff --git a/defdap/base.py b/defdap/base.py index ca149c7..f419e29 100755 --- a/defdap/base.py +++ b/defdap/base.py @@ -793,17 +793,6 @@ def __len__(self): def __str__(self): return f"Grain(ID={self.grain_id})" - def add_point(self, point): - """Append a coordinate and a quat to a grain. - - Parameters - ---------- - point : tuple - (x,y) coordinate to append - - """ - self.data.point.append(point) - @property def extreme_coords(self): """Coordinates of the bounding box for a grain. @@ -814,12 +803,7 @@ def extreme_coords(self): minimum x, minimum y, maximum x, maximum y. """ - points = np.array(self.data.point, dtype=int) - - x0, y0 = points.min(axis=0) - xmax, ymax = points.max(axis=0) - - return x0, y0, xmax, ymax + return *self.data.point.min(axis=0), *self.data.point.max(axis=0) def centre_coords(self, centre_type="box", grain_coords=True): """ @@ -846,7 +830,7 @@ def centre_coords(self, centre_type="box", grain_coords=True): x_centre = round((xmax + x0) / 2) y_centre = round((ymax + y0) / 2) elif centre_type == "com": - x_centre, y_centre = np.array(self.data.point).mean(axis=0).round() + x_centre, y_centre = self.data.point.mean(axis=0).round() else: raise ValueError("centreType must be box or com") diff --git a/defdap/ebsd.py b/defdap/ebsd.py index 33a6c6d..399f8b5 100755 --- a/defdap/ebsd.py +++ b/defdap/ebsd.py @@ -990,16 +990,14 @@ def flood_fill(self, seed, index, points_left, grains, boundary_im_x, # add first point to the grain x, y = seed - grain.add_point(seed) grains[y, x] = index points_left[y, x] = False - edge = [seed] added_coords = flood_fill( seed, index, points_left, grains, boundary_im_x, boundary_im_y, added_coords_buffer) - grain.data.point = list(added_coords) + grain.data.point = added_coords return grain diff --git a/defdap/hrdic.py b/defdap/hrdic.py index 334f50e..3a1fd4d 100755 --- a/defdap/hrdic.py +++ b/defdap/hrdic.py @@ -630,9 +630,7 @@ def find_grains(self, algorithm=None, min_grain_size=10): grain = Grain(dic_grain_id, self, group_id) # Find (x,y) coordinates and corresponding max shears of grain - # coords = np.argwhere(grains == dic_grain_id + 1) # (y,x) - coords = props_dict[dic_grain_id + 1].coords # (r, c) - # grain.data.point = [(x, y) for y, x in coords] + coords = props_dict[dic_grain_id + 1].coords # (y, x) grain.data.point = np.flip(coords, axis=1) # (x, y) # Assign EBSD grain ID to DIC grain and increment grain list @@ -646,9 +644,7 @@ def find_grains(self, algorithm=None, min_grain_size=10): # List of points where no grain has been set yet points_left = grains == 0 - added_coords_buffer = np.zeros( - (points_left.size, 2), dtype=np.intp - ) + added_coords_buffer = np.zeros((points_left.size, 2), dtype=np.intp) total_points = points_left.sum() found_point = 0 next_point = points_left.tobytes().find(b'\x01') @@ -748,7 +744,7 @@ def flood_fill(self, seed, index, points_left, grains, group_id, added_coords = flood_fill_dic(seed, index, points_left, grains, added_coords_buffer) - grain.data.point = list(added_coords) + grain.data.point = added_coords return grain diff --git a/tests/test_ebsd.py b/tests/test_ebsd.py index 2772361..f612f18 100644 --- a/tests/test_ebsd.py +++ b/tests/test_ebsd.py @@ -223,11 +223,11 @@ def test_grain_points(mock_map, min_grain_size): f'{EXPECTED_RESULTS_DIR}/ebsd_grains_5deg_{min_grain_size}.npz' )['grains'] + # transform both to set of tuples so order of points is ignored for i in range(expected_grains.max()): + expected_point = set(zip(*np.nonzero(expected_grains == i+1)[::-1])) - expected_point = zip(*np.nonzero(expected_grains == i+1)[::-1]) - - assert set(result[i].data.point) == set(expected_point) + assert set([(*r, ) for r in result[i].data.point]) == expected_point ''' Functions left to test diff --git a/tests/test_hrdic.py b/tests/test_hrdic.py index 1fcec0d..0bd61ca 100644 --- a/tests/test_hrdic.py +++ b/tests/test_hrdic.py @@ -99,7 +99,6 @@ def test_grain_list_size(mock_map): assert len(result) == 111 @staticmethod - @pytest.mark.parametrize('min_grain_size', [0, 10, 100]) def test_grain_points(mock_map, min_grain_size): algorithm = 'warp' hrdic.Map.find_grains(mock_map, algorithm=algorithm) @@ -110,10 +109,11 @@ def test_grain_points(mock_map, min_grain_size): f'{EXPECTED_RESULTS_DIR}/hrdic_grains_{algorithm}.npz' )['grains'] + # transform both to set of tuples so order of points is ignored for i in range(expected_grains.max()): - expected_point = zip(*np.nonzero(expected_grains == i+1)[::-1]) + expected_point = set(zip(*np.nonzero(expected_grains == i+1)[::-1])) - assert set(result[i].data.point) == set(expected_point) + assert set([(*r, ) for r in result[i].data.point]) == expected_point # methods to test From a93cc744beb131af0a43d2c1e4f6d98af1a44526 Mon Sep 17 00:00:00 2001 From: Michael Atkinson Date: Mon, 6 Nov 2023 22:58:22 +0000 Subject: [PATCH 07/12] Fix test --- tests/test_hrdic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_hrdic.py b/tests/test_hrdic.py index 0bd61ca..a4cb9e5 100644 --- a/tests/test_hrdic.py +++ b/tests/test_hrdic.py @@ -99,7 +99,7 @@ def test_grain_list_size(mock_map): assert len(result) == 111 @staticmethod - def test_grain_points(mock_map, min_grain_size): + def test_grain_points(mock_map): algorithm = 'warp' hrdic.Map.find_grains(mock_map, algorithm=algorithm) result = mock_map._grains From 8e916754d2d871ebf2b6818259001c2a732557f4 Mon Sep 17 00:00:00 2001 From: Michael Atkinson Date: Tue, 7 Nov 2023 22:52:55 +0000 Subject: [PATCH 08/12] Remove flood_fill methods from maps --- defdap/_accelerated.py | 40 +++++++++++++++++++++++++++++++++++ defdap/ebsd.py | 47 +++++++----------------------------------- defdap/hrdic.py | 39 +++++------------------------------ 3 files changed, 52 insertions(+), 74 deletions(-) diff --git a/defdap/_accelerated.py b/defdap/_accelerated.py index eafda25..d8e6eae 100644 --- a/defdap/_accelerated.py +++ b/defdap/_accelerated.py @@ -12,6 +12,24 @@ def find_first(arr): @njit def flood_fill(seed, index, points_remaining, grains, boundary_x, boundary_y, added_coords): + """Flood fill algorithm that uses the x and y boundary arrays to + fill a connected area around the seed point. The points are inserted + into a grain object and the grain map array is updated. + + Parameters + ---------- + seed : tuple of 2 int + Seed point x for flood fill + index : int + Value to fill in grain map + points_remaining : numpy.ndarray + Boolean map of the points that have not been assigned a grain yet + + Returns + ------- + grain : defdap.ebsd.Grain + New grain object with points added + """ x, y = seed grains[y, x] = index points_remaining[y, x] = False @@ -67,6 +85,28 @@ def flood_fill(seed, index, points_remaining, grains, boundary_x, boundary_y, @njit def flood_fill_dic(seed, index, points_remaining, grains, added_coords): + """Flood fill algorithm that uses the combined x and y boundary array + to fill a connected area around the seed point. The points are returned and + the grain map array is updated. + + Parameters + ---------- + seed : tuple of 2 int + Seed point x for flood fill + index : int + Value to fill in grain map + points_remaining : numpy.ndarray + Boolean map of the points remaining to assign a grain yet + grains : numpy.ndarray + added_coords : numpy.ndarray + Buffer for points in the grain + + Returns + ------- + numpy.ndarray + Flooded points (n, 2) + + """ # add first point to the grain x, y = seed grains[y, x] = index diff --git a/defdap/ebsd.py b/defdap/ebsd.py index 399f8b5..88b02f8 100755 --- a/defdap/ebsd.py +++ b/defdap/ebsd.py @@ -886,10 +886,13 @@ def find_grains(self, min_grain_size=10): while found_point >= 0: # Flood fill first unknown point and return grain object seed = np.unravel_index(next_point, self.shape) - grain = self.flood_fill( - (seed[1], seed[0]), grain_index, points_left, grains, - boundary_im_x, boundary_im_y, group_id, - added_coords_buffer, + + grain = Grain(grain_index - 1, self, group_id) + grain.data.point = flood_fill( + (seed[1], seed[0]), grain_index, + points_left, grains, + boundary_im_x, boundary_im_y, + added_coords_buffer ) if len(grain) < min_grain_size: @@ -965,42 +968,6 @@ def plot_grain_map(self, **kwargs): return plot - def flood_fill(self, seed, index, points_left, grains, boundary_im_x, - boundary_im_y, group_id, added_coords_buffer=None): - """Flood fill algorithm that uses the x and y boundary arrays to - fill a connected area around the seed point. The points are inserted - into a grain object and the grain map array is updated. - - Parameters - ---------- - seed : tuple of 2 int - Seed point x for flood fill - index : int - Value to fill in grain map - points_left : numpy.ndarray - Boolean map of the points that have not been assigned a grain yet - - Returns - ------- - grain : defdap.ebsd.Grain - New grain object with points added - """ - # create new grain - grain = Grain(index - 1, self, group_id) - - # add first point to the grain - x, y = seed - grains[y, x] = index - points_left[y, x] = False - added_coords = flood_fill( - seed, index, - points_left, grains, - boundary_im_x, boundary_im_y, - added_coords_buffer) - grain.data.point = added_coords - - return grain - @report_progress("calculating grain mean orientations") def calc_grain_av_oris(self): """Calculate the average orientation of grains. diff --git a/defdap/hrdic.py b/defdap/hrdic.py index 3a1fd4d..1eb7358 100755 --- a/defdap/hrdic.py +++ b/defdap/hrdic.py @@ -657,9 +657,11 @@ def find_grains(self, algorithm=None, min_grain_size=10): while found_point >= 0: # Flood fill first unknown point and return grain object seed = np.unravel_index(next_point, self.shape) - grain = self.flood_fill( - (seed[1], seed[0]), grain_index, points_left, grains, - group_id, added_coords_buffer, + + grain = Grain(grain_index - 1, self, group_id) + grain.data.point = flood_fill_dic( + (seed[1], seed[0]), grain_index, points_left, + grains, added_coords_buffer ) if len(grain) < min_grain_size: @@ -717,37 +719,6 @@ def find_grains(self, algorithm=None, min_grain_size=10): self._grains = grain_list return grains - def flood_fill(self, seed, index, points_left, grains, group_id, - added_coords_buffer=None): - """Flood fill algorithm that uses the combined x and y boundary array - to fill a connected area around the seed point. The points are inserted - into a grain object and the grain map array is updated. - - Parameters - ---------- - seed : tuple of 2 int - Seed point x for flood fill - index : int - Value to fill in grain map - points_left : numpy.ndarray - Boolean map of the points that have not been assigned a grain yet - - Returns - ------- - grain : defdap.hrdic.Grain - New grain object with points added - - """ - # create new grain - grain = Grain(index - 1, self, group_id) - - added_coords = flood_fill_dic(seed, index, points_left, grains, - added_coords_buffer) - - grain.data.point = added_coords - - return grain - def grain_inspector(self, vmax=0.1, correction_angle=0, rdr_line_length=3): """Run the grain inspector interactive tool. From 86634978e09e803b3f1f602efeb598acd712eb25 Mon Sep 17 00:00:00 2001 From: Michael Atkinson Date: Tue, 7 Nov 2023 22:54:47 +0000 Subject: [PATCH 09/12] More testing of `hrdic.Map.find_grains` --- .../hrdic_grain_boundaries_5deg.npz | Bin 0 -> 11774 bytes .../hrdic_grains_floodfill_0.npz | Bin 0 -> 6302 bytes .../hrdic_grains_floodfill_10.npz | Bin 0 -> 6399 bytes .../hrdic_grains_floodfill_100.npz | Bin 0 -> 6297 bytes .../hrdic_grains_warped-old.npz | Bin 0 -> 7162 bytes .../expected_output/hrdic_grains_warped.npz | Bin 0 -> 7180 bytes tests/test_ebsd.py | 8 - tests/test_hrdic.py | 156 +++++++++++++++--- 8 files changed, 132 insertions(+), 32 deletions(-) create mode 100644 tests/data/expected_output/hrdic_grain_boundaries_5deg.npz create mode 100644 tests/data/expected_output/hrdic_grains_floodfill_0.npz create mode 100644 tests/data/expected_output/hrdic_grains_floodfill_10.npz create mode 100644 tests/data/expected_output/hrdic_grains_floodfill_100.npz create mode 100644 tests/data/expected_output/hrdic_grains_warped-old.npz create mode 100644 tests/data/expected_output/hrdic_grains_warped.npz diff --git a/tests/data/expected_output/hrdic_grain_boundaries_5deg.npz b/tests/data/expected_output/hrdic_grain_boundaries_5deg.npz new file mode 100644 index 0000000000000000000000000000000000000000..4535de31b5beb1d44f71710c2ad3321ce5aec2e9 GIT binary patch literal 11774 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 WABtMJ>VG|MOMA#t`_KP*xc?ttTP`>N literal 0 HcmV?d00001 diff --git a/tests/data/expected_output/hrdic_grains_floodfill_0.npz b/tests/data/expected_output/hrdic_grains_floodfill_0.npz new file mode 100644 index 0000000000000000000000000000000000000000..2a23636485e1b2a614341708659532eb0d8c02ec GIT binary patch literal 6302 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 literal 0 HcmV?d00001 diff --git a/tests/data/expected_output/hrdic_grains_floodfill_10.npz b/tests/data/expected_output/hrdic_grains_floodfill_10.npz new file mode 100644 index 0000000000000000000000000000000000000000..1b5492e3837e9ca391c2d5c88b12328569dced1f GIT binary patch literal 6399 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 literal 0 HcmV?d00001 diff --git a/tests/test_ebsd.py b/tests/test_ebsd.py index f612f18..c07a153 100644 --- a/tests/test_ebsd.py +++ b/tests/test_ebsd.py @@ -1,7 +1,5 @@ import pytest -from pytest import approx from unittest.mock import Mock, MagicMock -from functools import partial import numpy as np import defdap.ebsd as ebsd @@ -102,10 +100,6 @@ def mock_map(good_quat_array, good_phase_array, good_symmetries): mock_crystal_structure.symmetries = good_symmetries mock_phase = Mock(spec=crystal.Phase) mock_phase.crystal_structure = mock_crystal_structure - - # mock_phase = Mock(spec=crystal.Phase) - # mock_phase.crystal_structure = crystal.crystalStructures['cubic'] - mock_map.primary_phase = mock_phase return mock_map @@ -153,10 +147,8 @@ def mock_map(good_grain_boundaries, good_phase_array): mock_datastore.phase = good_phase_array mock_datastore.grain_boundaries = good_grain_boundaries mock_datastore.generate_id = Mock(return_value=1) - # mock_datastore.__iter__.return_value = [] mock_map.data = mock_datastore mock_map.shape = good_phase_array.shape - mock_map.flood_fill = partial(ebsd.Map.flood_fill, mock_map) mock_map.num_phases = 1 mock_map.phases = [Mock(crystal.Phase)] diff --git a/tests/test_hrdic.py b/tests/test_hrdic.py index a4cb9e5..29a0464 100644 --- a/tests/test_hrdic.py +++ b/tests/test_hrdic.py @@ -1,9 +1,8 @@ import pytest -from pytest import approx from unittest.mock import Mock, MagicMock -from functools import partial import numpy as np + import defdap.ebsd as ebsd import defdap.hrdic as hrdic from defdap.utils import Datastore @@ -15,12 +14,40 @@ @pytest.fixture(scope="module") -def good_warped_grains(): +def good_grain_boundaries(): + expected = np.load( + f'{EXPECTED_RESULTS_DIR}/hrdic_grain_boundaries_5deg.npz' + ) + mock_map = Mock(spec=hrdic.Map) + mock_map.shape = (200, 300) + return hrdic.BoundarySet( + mock_map, + [tuple(row) for row in expected['points']], + None + ) + + +@pytest.fixture(scope="module") +def good_warped_ebsd_grains(): return np.load( f'{EXPECTED_RESULTS_DIR}/ebsd_grains_warped_5deg_0.npz' )['grains'] +@pytest.fixture(scope="module") +def good_warped_dic_grains(): + return np.load( + f'{EXPECTED_RESULTS_DIR}/hrdic_grains_warped.npz' + )['grains'] + + +@pytest.fixture(scope="module") +def good_ebsd_grains(): + return np.load( + f'{EXPECTED_RESULTS_DIR}/ebsd_grains_5deg_0.npz' + )['grains'] + + class TestMapFindGrains: # for warp depends on # check_ebsd_linked, warp_to_dic_frame, shape, ebsd_map @@ -30,26 +57,36 @@ class TestMapFindGrains: @staticmethod @pytest.fixture - def mock_map(good_warped_grains): + def mock_map(good_warped_ebsd_grains, good_grain_boundaries, + good_warped_dic_grains, good_ebsd_grains): # create stub object mock_map = Mock(spec=hrdic.Map) mock_map.check_ebsd_linked = Mock(return_value=True) - mock_map.warp_to_dic_frame = Mock(return_value=good_warped_grains) - mock_map.shape = good_warped_grains.shape + mock_map.warp_to_dic_frame = Mock(return_value=good_warped_ebsd_grains) + mock_map.shape = good_warped_ebsd_grains.shape mock_map.data = MagicMock(spec=Datastore) mock_map.data.generate_id = Mock(return_value=1) - mock_map.ebsd_map = MagicMock(spec=ebsd.Map) + mock_map.ebsd_map.__getitem__ = lambda self, k: k mock_map.ebsd_map.data = Mock(spec=Datastore) - mock_map.ebsd_map.data.grains = 'ebsd_grains' - # mock_map.flood_fill = partial(hrdic.Map.flood_fill, mock_map) + mock_map.data.grain_boundaries = good_grain_boundaries + + mock_map.experiment = Mock() + mock_map.experiment.warp_image = Mock( + return_value=good_warped_dic_grains + ) + mock_map.frame = Mock() + mock_map.ebsd_map.frame = Mock() + mock_map.ebsd_map.shape = good_warped_dic_grains.shape + mock_map.ebsd_map.data.grains = good_ebsd_grains return mock_map @staticmethod - def test_return_type(mock_map): - algorithm = 'warp' + @pytest.mark.parametrize('algorithm', ['warp', 'floodfill']) + def test_return_type(mock_map, algorithm): + # algorithm = 'warp' # run test and collect result result = hrdic.Map.find_grains(mock_map, algorithm=algorithm) @@ -58,14 +95,23 @@ def test_return_type(mock_map): assert result.dtype == np.int64 @staticmethod - def test_calc_warp(mock_map): - algorithm = 'warp' + @pytest.mark.parametrize('algorithm, min_grain_size', [ + ('warp', None), + ('floodfill', 0), + ('floodfill', 10), + ('floodfill', 100), + ]) + def test_calc_warp(mock_map, algorithm, min_grain_size): + # algorithm = 'warp' # run test and collect result - result = hrdic.Map.find_grains(mock_map, algorithm=algorithm) + result = hrdic.Map.find_grains( + mock_map, algorithm=algorithm, min_grain_size=min_grain_size + ) # load expected + min_grain_size = '' if min_grain_size is None else f'_{min_grain_size}' expected = np.load( - f'{EXPECTED_RESULTS_DIR}/hrdic_grains_{algorithm}.npz' + f'{EXPECTED_RESULTS_DIR}/hrdic_grains_{algorithm}{min_grain_size}.npz' )['grains'] assert np.alltrue(result == expected) @@ -81,7 +127,8 @@ def test_add_derivative(mock_map): mock_add_derivative.assert_called_once() @staticmethod - def test_grain_list_type(mock_map): + @pytest.mark.parametrize('algorithm', ['warp', 'floodfill']) + def test_grain_list_type(mock_map, algorithm): algorithm = 'warp' hrdic.Map.find_grains(mock_map, algorithm=algorithm) result = mock_map._grains @@ -91,22 +138,32 @@ def test_grain_list_type(mock_map): assert isinstance(g, hrdic.Grain) @staticmethod - def test_grain_list_size(mock_map): - algorithm = 'warp' - hrdic.Map.find_grains(mock_map, algorithm=algorithm) + @pytest.mark.parametrize('algorithm, expected', [ + ('warp', 111), ('floodfill', 80) + ]) + def test_grain_list_size(mock_map, algorithm, expected): + hrdic.Map.find_grains(mock_map, algorithm=algorithm, min_grain_size=10) result = mock_map._grains - assert len(result) == 111 + assert len(result) == expected @staticmethod - def test_grain_points(mock_map): - algorithm = 'warp' - hrdic.Map.find_grains(mock_map, algorithm=algorithm) + @pytest.mark.parametrize('algorithm, min_grain_size', [ + ('warp', None), + ('floodfill', 0), + ('floodfill', 10), + ('floodfill', 100), + ]) + def test_grain_points(mock_map, algorithm, min_grain_size): + hrdic.Map.find_grains( + mock_map, algorithm=algorithm, min_grain_size=min_grain_size + ) result = mock_map._grains # load expected + min_grain_size = '' if min_grain_size is None else f'_{min_grain_size}' expected_grains = np.load( - f'{EXPECTED_RESULTS_DIR}/hrdic_grains_{algorithm}.npz' + f'{EXPECTED_RESULTS_DIR}/hrdic_grains_{algorithm}{min_grain_size}.npz' )['grains'] # transform both to set of tuples so order of points is ignored @@ -115,6 +172,57 @@ def test_grain_points(mock_map): assert set([(*r, ) for r in result[i].data.point]) == expected_point + @staticmethod + def test_call_warp_to_dic_frame(mock_map, good_ebsd_grains): + hrdic.Map.find_grains(mock_map, algorithm='warp') + + mock_map.warp_to_dic_frame.assert_called_once() + mock_map.warp_to_dic_frame.assert_called_with( + good_ebsd_grains, order=0, preserve_range=True + ) + + @staticmethod + def test_call_experiment_warp_image(mock_map, good_ebsd_grains): + hrdic.Map.find_grains(mock_map, algorithm='floodfill', min_grain_size=10) + + good_grains = np.load( + f'{EXPECTED_RESULTS_DIR}/hrdic_grains_floodfill_10.npz' + )['grains'] + + mock_map.experiment.warp_image.assert_called_once() + call_args = mock_map.experiment.warp_image.call_args + np.testing.assert_array_equal( + good_grains.astype(float), call_args[0][0] + ) + assert call_args[0][1] == mock_map.frame + assert call_args[0][2] == mock_map.ebsd_map.frame + assert call_args[1]['output_shape'] == mock_map.ebsd_map.shape + assert call_args[1]['order'] == 0 + + @staticmethod + @pytest.mark.parametrize('algorithm, expected', [ + ('warp', [ + 1, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, + 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, + 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, + 57, 58, 59, 60, 61, 62, 63, 64, 65, 67, 68, 69, 71, 72, 75, 77, 78, + 79, 80, 81, 82, 83, 84, 85, 86, 87, 89, 90, 91, 92, 93, 95, 96, 97, + 98, 99, 100, 101, 102, 103, 105, 106, 107, 108, 109, 111, 112, 113, + 114, 115, 116, 117, 119, 120, 121, 122, 123, 125, 126 + ]), + ('floodfill', [ + 1, 13, 5, 6, 7, 8, 9, 11, 15, 14, 16, 17, 20, 21, 22, 18, 23, 25, + 19, 24, 28, 29, 32, 31, 30, 34, 35, 36, 33, 38, 39, 41, 40, 50, 44, + 39, 52, 47, 51, 48, 37, 57, 58, 65, 61, 62, 64, 72, 77, 79, 81, 80, + 75, 86, 90, 85, 87, 57, 91, 93, 92, 99, 99, 95, 97, 96, 100, 106, + 102, 97, 107, 108, 111, 112, 115, 117, 114, 120, 122, 123 + ]) + ]) + def test_grain_assigned_ebsd_grains(mock_map, algorithm, expected): + hrdic.Map.find_grains(mock_map, algorithm=algorithm, min_grain_size=10) + result = [g.ebsd_grain for g in mock_map._grains] + + assert result == expected # methods to test # '_grad', From 24ffb2830f35a038e9563aa94a583645c0d98e47 Mon Sep 17 00:00:00 2001 From: Michael Atkinson Date: Tue, 7 Nov 2023 23:01:13 +0000 Subject: [PATCH 10/12] Require python >= 3.8 required for scipy >=1.9 --- .github/workflows/test.yml | 2 +- setup.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c773b3c..e59c208 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v2 diff --git a/setup.py b/setup.py index 9bc95a2..e1d03d7 100644 --- a/setup.py +++ b/setup.py @@ -41,17 +41,18 @@ def get_version(): 'Natural Language :: English', 'Operating System :: OS Independent', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'Framework :: IPython', 'Framework :: Jupyter', 'Framework :: Matplotlib' ], packages=find_packages(exclude=['tests']), package_data={'defdap': ['slip_systems/*.txt']}, - python_requires='>=3.6', + python_requires='>=3.8', install_requires=[ 'scipy>=1.9', 'numpy', From d656cc85ed2f58e88e50cc19727e22deade0d421 Mon Sep 17 00:00:00 2001 From: Michael Atkinson Date: Tue, 7 Nov 2023 23:10:22 +0000 Subject: [PATCH 11/12] Remove py 3.12 --- .github/workflows/test.yml | 2 +- setup.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e59c208..88c425f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v2 diff --git a/setup.py b/setup.py index e1d03d7..af67775 100644 --- a/setup.py +++ b/setup.py @@ -45,7 +45,6 @@ def get_version(): 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', 'Framework :: IPython', 'Framework :: Jupyter', 'Framework :: Matplotlib' @@ -66,8 +65,10 @@ def get_version(): ], extras_require={ 'testing': ['pytest', 'coverage', 'pytest-cov', 'pytest_cases'], - 'docs': ['sphinx==5.0.2', 'sphinx_rtd_theme==0.5.0', 'sphinx_autodoc_typehints==1.11.1', - 'nbsphinx==0.9.3', 'ipykernel', 'pandoc', 'ipympl'] + 'docs': [ + 'sphinx==5.0.2', 'sphinx_rtd_theme==0.5.0', + 'sphinx_autodoc_typehints==1.11.1', 'nbsphinx==0.9.3', + 'ipykernel', 'pandoc', 'ipympl' + ] } - ) From 1e586b1a0c17b9104154bdf069a42fe8908bcee6 Mon Sep 17 00:00:00 2001 From: Michael Atkinson Date: Tue, 7 Nov 2023 23:51:10 +0000 Subject: [PATCH 12/12] Update use of buffer in floodfill --- defdap/_accelerated.py | 4 ++-- defdap/ebsd.py | 9 ++++----- defdap/hrdic.py | 5 +++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/defdap/_accelerated.py b/defdap/_accelerated.py index d8e6eae..5adf189 100644 --- a/defdap/_accelerated.py +++ b/defdap/_accelerated.py @@ -80,7 +80,7 @@ def flood_fill(seed, index, points_remaining, grains, boundary_x, boundary_y, npoints += 1 edge.append((s, t)) - return np.copy(added_coords[:npoints]) + return added_coords[:npoints] @njit @@ -145,4 +145,4 @@ def flood_fill_dic(seed, index, points_remaining, grains, added_coords): points_remaining[t, s] = False npoints += 1 - return np.copy(added_coords[:npoints]) + return added_coords[:npoints] diff --git a/defdap/ebsd.py b/defdap/ebsd.py index 88b02f8..5d6919e 100755 --- a/defdap/ebsd.py +++ b/defdap/ebsd.py @@ -882,18 +882,17 @@ def find_grains(self, min_grain_size=10): # Loop until all points (except boundaries) have been assigned # to a grain or ignored i = 0 - added_coords_buffer = np.zeros((boundary_im_y.size, 2), dtype=np.intp) + coords_buffer = np.zeros((boundary_im_y.size, 2), dtype=np.intp) while found_point >= 0: # Flood fill first unknown point and return grain object seed = np.unravel_index(next_point, self.shape) grain = Grain(grain_index - 1, self, group_id) grain.data.point = flood_fill( - (seed[1], seed[0]), grain_index, - points_left, grains, - boundary_im_x, boundary_im_y, - added_coords_buffer + (seed[1], seed[0]), grain_index, points_left, grains, + boundary_im_x, boundary_im_y, coords_buffer ) + coords_buffer = coords_buffer[len(grain.data.point):] if len(grain) < min_grain_size: # if grain size less than minimum, ignore grain and set diff --git a/defdap/hrdic.py b/defdap/hrdic.py index 1eb7358..d13d6fc 100755 --- a/defdap/hrdic.py +++ b/defdap/hrdic.py @@ -644,7 +644,7 @@ def find_grains(self, algorithm=None, min_grain_size=10): # List of points where no grain has been set yet points_left = grains == 0 - added_coords_buffer = np.zeros((points_left.size, 2), dtype=np.intp) + coords_buffer = np.zeros((points_left.size, 2), dtype=np.intp) total_points = points_left.sum() found_point = 0 next_point = points_left.tobytes().find(b'\x01') @@ -661,8 +661,9 @@ def find_grains(self, algorithm=None, min_grain_size=10): grain = Grain(grain_index - 1, self, group_id) grain.data.point = flood_fill_dic( (seed[1], seed[0]), grain_index, points_left, - grains, added_coords_buffer + grains, coords_buffer ) + coords_buffer = coords_buffer[len(grain.data.point):] if len(grain) < min_grain_size: # if grain size less than minimum, ignore grain and set