From b5452d43be0ef45f2803f1529c27b6299aec67d1 Mon Sep 17 00:00:00 2001 From: Patrick Curran Date: Wed, 2 Oct 2024 19:59:14 +0100 Subject: [PATCH] Worked until add_homog_points --- build/lib/defdap/__init__.py | 23 + build/lib/defdap/_accelerated.py | 148 + build/lib/defdap/_version.py | 1 + build/lib/defdap/base.py | 1053 ++++++ build/lib/defdap/crystal.py | 728 ++++ build/lib/defdap/ebsd.py | 1916 +++++++++++ build/lib/defdap/experiment.py | 338 ++ build/lib/defdap/file_readers.py | 839 +++++ build/lib/defdap/file_writers.py | 125 + build/lib/defdap/hrdic.py | 928 ++++++ build/lib/defdap/inspector.py | 663 ++++ build/lib/defdap/plotting.py | 1527 +++++++++ build/lib/defdap/quat.py | 1098 ++++++ build/lib/defdap/slip_systems/cubic_bcc.txt | 50 + .../defdap/slip_systems/cubic_bcc_110only.txt | 14 + build/lib/defdap/slip_systems/cubic_fcc.txt | 14 + .../defdap/slip_systems/cubic_fcc_damask.txt | 14 + .../defdap/slip_systems/hexagonal_noca.txt | 14 + .../defdap/slip_systems/hexagonal_withca.txt | 26 + build/lib/defdap/utils.py | 449 +++ defdap/dev-Optical.ipynb | 169 +- defdap/main-Optical.ipynb | 2937 +++++++++++++++++ defdap/optical.py | 179 +- defdap/test_data/test-meta-data.xlsx | Bin 0 -> 9412 bytes 24 files changed, 13203 insertions(+), 50 deletions(-) create mode 100644 build/lib/defdap/__init__.py create mode 100644 build/lib/defdap/_accelerated.py create mode 100644 build/lib/defdap/_version.py create mode 100644 build/lib/defdap/base.py create mode 100644 build/lib/defdap/crystal.py create mode 100644 build/lib/defdap/ebsd.py create mode 100644 build/lib/defdap/experiment.py create mode 100644 build/lib/defdap/file_readers.py create mode 100644 build/lib/defdap/file_writers.py create mode 100644 build/lib/defdap/hrdic.py create mode 100644 build/lib/defdap/inspector.py create mode 100644 build/lib/defdap/plotting.py create mode 100644 build/lib/defdap/quat.py create mode 100644 build/lib/defdap/slip_systems/cubic_bcc.txt create mode 100644 build/lib/defdap/slip_systems/cubic_bcc_110only.txt create mode 100644 build/lib/defdap/slip_systems/cubic_fcc.txt create mode 100644 build/lib/defdap/slip_systems/cubic_fcc_damask.txt create mode 100644 build/lib/defdap/slip_systems/hexagonal_noca.txt create mode 100644 build/lib/defdap/slip_systems/hexagonal_withca.txt create mode 100644 build/lib/defdap/utils.py create mode 100644 defdap/main-Optical.ipynb create mode 100644 defdap/test_data/test-meta-data.xlsx diff --git a/build/lib/defdap/__init__.py b/build/lib/defdap/__init__.py new file mode 100644 index 0000000..1c8dba2 --- /dev/null +++ b/build/lib/defdap/__init__.py @@ -0,0 +1,23 @@ +from defdap.experiment import Experiment + +defaults = { + # Convention to use when attaching an orthonormal frame to a crystal + # structure. 'hkl' or 'tsl' + # OI/HKL convention - x // [10-10], y // a2 [-12-10] + # TSL convention - x // a1 [2-1-10], y // [01-10] + 'crystal_ortho_conv': 'hkl', + # Projection to use when plotting pole figures. 'stereographic' (equal + # angle), 'lambert' (equal area) or arbitrary projection function + 'pole_projection': 'stereographic', + # Frequency of find grains algorithm reporting progress + 'find_grain_report_freq': 100, + # How to find grain in a HRDIC map, either 'floodfill' or 'warp' + 'hrdic_grain_finding_method': 'floodfill', + 'slip_system_file': { + 'FCC': 'cubic_fcc', + 'BCC': 'cubic_bcc', + 'HCP': 'hexagonal_withca', + }, +} + +anonymous_experiment = Experiment() diff --git a/build/lib/defdap/_accelerated.py b/build/lib/defdap/_accelerated.py new file mode 100644 index 0000000..5adf189 --- /dev/null +++ b/build/lib/defdap/_accelerated.py @@ -0,0 +1,148 @@ +from numba import njit +import numpy as np + + +@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): + """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 + 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] + + +@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 + 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] - 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] diff --git a/build/lib/defdap/_version.py b/build/lib/defdap/_version.py new file mode 100644 index 0000000..723b73e --- /dev/null +++ b/build/lib/defdap/_version.py @@ -0,0 +1 @@ +__version__ = '0.93.5dev' diff --git a/build/lib/defdap/base.py b/build/lib/defdap/base.py new file mode 100644 index 0000000..5d54710 --- /dev/null +++ b/build/lib/defdap/base.py @@ -0,0 +1,1053 @@ +# Copyright 2023 Mechanics of Microstructures Group +# at The University of Manchester +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import ABC, abstractmethod +from pathlib import Path + +import numpy as np +import networkx as nx + +import defdap +from defdap.quat import Quat +from defdap import plotting +from defdap.plotting import Plot, MapPlot, GrainPlot + +from skimage.measure import profile_line + +from defdap.utils import report_progress, Datastore +from defdap.experiment import Frame + + +class Map(ABC): + """ + Base class for a map. Contains common functionality for all maps. + + Attributes + ---------- + + _grains : list of defdap.base.Grain + List of grains. + sel_grain : defdap.base.grain + The last selected grain + + """ + def __init__(self, file_name, data_type=None, experiment=None, + increment=None, frame=None, map_name=None): + """ + + Parameters + ---------- + file_name : str + Path to EBSD file, including name, excluding extension. + data_type : str, {'OxfordBinary', 'OxfordText'} + Format of EBSD data file. + + """ + + self.data = Datastore(crop_func=self.crop) + self.frame = frame if frame is not None else Frame() + if increment is not None: + self.increment = increment + self.experiment = self.increment.experiment + if experiment is not None: + assert self.experiment is experiment + else: + self.experiment = experiment + if experiment is None: + self.experiment = defdap.anonymous_experiment + self.increment = self.experiment.add_increment() + map_name = self.MAPNAME if map_name is None else map_name + self.increment.add_map(map_name, self) + + self.shape = (0, 0) + + self._grains = None + + self.sel_grain = None + + self.proxigram_arr = None + self.neighbour_network = None + + self.grain_plot = None + self.profile_plot = None + + self.file_name = Path(file_name) + self.load_data(self.file_name, data_type=data_type) + + self.data.add_generator( + 'proxigram', self.calc_proxigram, unit='', type='map', order=0, + cropped=True + ) + + @abstractmethod + def load_data(self, file_name, data_type=None): + pass + + def __len__(self): + return len(self.grains) + + # allow array like getting of grains + def __getitem__(self, key): + return self.grains[key] + + @property + def grains(self): + # try to access grains image to generate grains if necessary + self.data.grains + return self._grains + + @property + def x_dim(self): + return self.shape[1] + + @property + def y_dim(self): + return self.shape[0] + + def crop(self, map_data, **kwargs): + return map_data + + def set_homog_point(self, **kwargs): + self.frame.set_homog_point(self, **kwargs) + + def plot_grain_numbers(self, dilate_boundaries=False, ax=None, **kwargs): + """Plot a map with grains numbered. + + Parameters + ---------- + dilate_boundaries : bool, optional + Set to true to dilate boundaries. + ax : matplotlib.axes.Axes, optional + axis to plot on, if not provided the current active axis is used. + kwargs : dict, optional + Keyword arguments passed to :func:`defdap.plotting.MapPlot.add_grain_numbers` + + Returns + ------- + defdap.plotting.MapPlot + + """ + + plot = plotting.MapPlot(self, ax=ax) + plot.add_grain_boundaries(colour='black', dilate=dilate_boundaries) + plot.add_grain_numbers(**kwargs) + + return plot + + def locate_grain(self, click_event=None, display_grain=False, **kwargs): + """Interactive plot for identifying grains. + + Parameters + ---------- + click_event : optional + Click handler to use. + display_grain : bool, optional + If true, plot slip traces for grain selected by click. + kwargs : dict, optional + Keyword arguments passed to :func:`defdap.base.Map.plot_default` + + """ + # reset current selected grain and plot euler map with click handler + plot = self.plot_default(make_interactive=True, **kwargs) + if click_event is None: + # default click handler which highlights grain and prints id + plot.add_event_handler( + 'button_press_event', + lambda e, p: self.click_grain_id(e, p, display_grain) + ) + else: + # click handler loaded in as parameter. Pass current map + # object to it. + plot.add_event_handler('button_press_event', click_event) + + return plot + + def click_grain_id(self, event, plot, display_grain): + """Event handler to capture clicking on a map. + + Parameters + ---------- + event : + Click event. + plot : defdap.plotting.MapPlot + Plot to capture clicks from. + display_grain : bool + If true, plot the selected grain alone in pop-out window. + + """ + # check if click was on the map + if event.inaxes is not plot.ax: + return + + # grain id of selected grain + grain_id = self.data.grains[int(event.ydata), int(event.xdata)] - 1 + if grain_id < 0: + return + grain = self[grain_id] + self.sel_grain = grain + print("Grain ID: {}".format(grain_id)) + + # update the grain highlights layer in the plot + plot.add_grain_highlights([grain_id], alpha=self.highlight_alpha) + + if display_grain: + if self.grain_plot is None or not self.grain_plot.exists: + self.grain_plot = grain.plot_default(make_interactive=True) + else: + self.grain_plot.clear() + self.grain_plot.calling_grain = grain + grain.plot_default(plot=self.grain_plot) + self.grain_plot.draw() + + def draw_line_profile(self, **kwargs): + """Interactive plot for drawing a line profile of data. + + Parameters + ---------- + kwargs : dict, optional + Keyword arguments passed to :func:`defdap.base.Map.plot_default` + + """ + plot = self.plot_default(make_interactive=True, **kwargs) + + plot.add_event_handler('button_press_event', plot.line_slice) + plot.add_event_handler( + 'button_release_event', + lambda e, p: plot.line_slice(e, p, action=self.calc_line_profile) + ) + + return plot + + def calc_line_profile(self, plot, start_end, **kwargs): + """Calculate and plot the line profile. + + Parameters + ---------- + plot : defdap.plotting.MapPlot + Plot to calculate the line profile for. + start_end : array_like + Selected points (x0, y0, x1, y1). + kwargs : dict, optional + Keyword arguments passed to :func:`matplotlib.pyplot.plot` + + """ + x0, y0 = start_end[0:2] + x1, y1 = start_end[2:4] + profile_length = np.sqrt((y1 - y0) ** 2 + (x1 - x0) ** 2) + + # Extract the values along the line + zi = profile_line( + plot.img_layers[0].get_array(), + (start_end[1], start_end[0]), + (start_end[3], start_end[2]), + mode='nearest' + ) + xi = np.linspace(0, profile_length, len(zi)) + + if self.profile_plot is None or not self.profile_plot.exists: + self.profile_plot = Plot(make_interactive=True) + else: + self.profile_plot.clear() + + self.profile_plot.ax.plot(xi, zi, **kwargs) + self.profile_plot.ax.set_xlabel('Distance (pixels)') + self.profile_plot.ax.set_ylabel('Intensity') + self.profile_plot.draw() + + @report_progress("constructing neighbour network") + def build_neighbour_network(self): + """Construct a list of neighbours + + """ + ## TODO: fix HRDIC NN + # create network + nn = nx.Graph() + nn.add_nodes_from(self.grains) + + y_locs, x_locs = np.nonzero(self.boundaries) + total_points = len(x_locs) + + for i_point, (x, y) in enumerate(zip(x_locs, y_locs)): + # report progress + yield i_point / total_points + + if (x == 0 or y == 0 or x == self.data.grains.shape[1] - 1 or + y == self.data.grains.shape[0] - 1): + # exclude boundary pixels of map + continue + + # use 4 nearest neighbour points as potential neighbour grains + # (this maybe needs changing considering the position of + # boundary pixels relative to the actual edges) + # use sets as they do not allow duplicate elements + # minus 1 on all as the grain image starts labeling at 1 + neighbours = { + self.data.grains[y + 1, x] - 1, + self.data.grains[y - 1, x] - 1, + self.data.grains[y, x + 1] - 1, + self.data.grains[y, x - 1] - 1 + } + # neighbours = set(neighbours) + # remove boundary points (-2) and points in small + # grains (-3) (Normally -1 and -2) + neighbours.discard(-2) + neighbours.discard(-3) + + neighbours = tuple(neighbours) + num_neigh = len(neighbours) + if num_neigh <= 1: + continue + for i in range(num_neigh): + for j in range(i + 1, num_neigh): + # Add to network + grain = self[neighbours[i]] + neigh_grain = self[neighbours[j]] + try: + # look up boundary + nn[grain][neigh_grain] + except KeyError: + # neighbour relation doesn't exist so add it + nn.add_edge(grain, neigh_grain) + + self.neighbour_network = nn + + def display_neighbours(self, **kwargs): + return self.locate_grain( + click_event=self.click_grain_neighbours, **kwargs + ) + + def click_grain_neighbours(self, event, plot): + """Event handler to capture clicking and show neighbours of selected grain. + + Parameters + ---------- + event : + Click event. + plot : defdap.plotting.MapPlot + Plot to monitor. + + """ + # check if click was on the map + if event.inaxes is not plot.ax: + return + + # grain id of selected grain + grain_id = self.data.grains[int(event.ydata), int(event.xdata)] - 1 + if grain_id < 0: + return + grain = self[grain_id] + self.sel_grain = grain + + # find first and second nearest neighbours + first_neighbours = list(self.neighbour_network.neighbors(grain)) + highlight_grains = [grain] + first_neighbours + + second_neighbours = [] + for firstNeighbour in first_neighbours: + trial_second_neighbours = list( + self.neighbour_network.neighbors(firstNeighbour) + ) + for second_neighbour in trial_second_neighbours: + if (second_neighbour not in highlight_grains and + second_neighbour not in second_neighbours): + second_neighbours.append(second_neighbour) + highlight_grains.extend(second_neighbours) + + highlight_grains = [grain.grain_id for grain in highlight_grains] + highlight_colours = ['white'] + highlight_colours.extend(['yellow'] * len(first_neighbours)) + highlight_colours.append('green') + + # update the grain highlights layer in the plot + plot.add_grain_highlights(highlight_grains, + grain_colours=highlight_colours) + + @property + def proxigram(self): + """Proxigram for a map. + + Returns + ------- + numpy.ndarray + Distance from a grain boundary at each point in map. + + """ + self.calc_proxigram(force_calc=False) + + return self.proxigram_arr + + @report_progress("calculating proxigram") + def calc_proxigram(self, num_trials=500): + """Calculate distance from a grain boundary at each point in map. + + Parameters + ---------- + num_trials : int, optional + number of trials. + + """ + # add 0.5 to boundary coordinates as they are placed on the + # bottom right edge pixels of grains + index_boundaries = [t[::-1] for t in self.data.grain_boundaries.points] + index_boundaries = np.array(index_boundaries) + 0.5 + + # array of x and y coordinate of each pixel in the map + coords = np.zeros((2,) + self.shape, dtype=float) + coords[0], coords[1] = np.meshgrid( + range(self.shape[0]), range(self.shape[1]), indexing='ij' + ) + + # array to store trial distance from each boundary point + trial_distances = np.full((num_trials + 1,) + self.shape, + 1000, dtype=float) + + # loop over each boundary point (p) and calculate distance from + # p to all points in the map store minimum once numTrails have + # been made and start a new batch of trials + num_boundary_points = len(index_boundaries) + j = 1 + for i, index_boundary in enumerate(index_boundaries): + trial_distances[j] = np.sqrt((coords[0] - index_boundary[0])**2 + + (coords[1] - index_boundary[1])**2) + + if j == num_trials: + # find current minimum distances and store + trial_distances[0] = trial_distances.min(axis=0) + j = 0 + # report progress + yield i / num_boundary_points + j += 1 + + # find final minimum distances to a boundary + return trial_distances.min(axis=0) + + def _validate_map(self, map_name): + """Check the name exists and is a map data. + + Parameters + ---------- + map_name : str + + """ + if map_name not in self.data: + raise ValueError(f'`{map_name}` does not exist.') + if (self.data.get_metadata(map_name, 'type') != 'map' or + self.data.get_metadata(map_name, 'order') is None): + raise ValueError(f'`{map_name}` is not a valid map.') + + def _validate_component(self, map_name, comp): + """ + + Parameters + ---------- + map_name : str + comp : int or tuple of int or str + Component of the map data. This is either the + tensor component (tuple of ints) or the name of a calculation + to be applied e.g. 'norm', 'all_euler' or 'IPF_x'. + + Returns + ------- + tuple of int or str + + """ + order = self.data[map_name, 'order'] + if comp is None: + comp = self.data.get_metadata(map_name, 'default_component') + if comp is not None: + print(f'Using default component: `{comp}`') + + if comp is None: + if order != 0: + raise ValueError('`comp` must be specified.') + else: + return comp + + if isinstance(comp, int): + comp = (comp,) + if isinstance(comp, tuple) and len(comp) != order: + raise ValueError(f'Component length does not match data, expected ' + f'{self.data[map_name, "order"]} values but got ' + f'{len(comp)}.') + + return comp + + def _extract_component(self, map_data, comp): + """Extract a component from the data. + + Parameters + ---------- + map_data : numpy.ndarray + Map data to extract from. + comp : tuple of int or str + Component of the map data to extract. This is either the + tensor component (tuple of ints) or the name of a calculation + to be applied e.g. 'norm', 'all_euler' or 'IPF_x'. + + Returns + ------- + numpy.ndarray + + """ + if comp is None: + return map_data + if isinstance(comp, tuple): + return map_data[comp] + if isinstance(comp, str): + comp = comp.lower() + if comp == 'norm': + if len(map_data.shape) == 3: + axis = 0 + elif len(map_data.shape) == 4: + axis = (0, 1) + else: + raise ValueError('Unsupported data for norm.') + + return np.linalg.norm(map_data, axis=axis) + + if comp == 'all_euler': + return self.calc_euler_colour(map_data) + + if comp.startswith('ipf'): + direction = comp.split('_')[1] + direction = { + 'x': np.array([1, 0, 0]), + 'y': np.array([0, 1, 0]), + 'z': np.array([0, 0, 1]), + }[direction] + return self.calc_ipf_colour(map_data, direction) + + raise ValueError(f'Invalid component `{comp}`') + + def plot_map(self, map_name, component=None, **kwargs): + """Plot a map from the DIC data. + + Parameters + ---------- + map_name : str + Map data name to plot i.e. e, max_shear, euler_angle, orientation. + component : int or tuple of int or str + Component of the map data to plot. This is either the tensor + component (int or tuple of ints) or the name of a calculation + to be applied e.g. 'norm', 'all_euler' or 'IPF_x'. + kwargs + All arguments are passed to :func:`defdap.plotting.MapPlot.create`. + + Returns + ------- + defdap.plotting.MapPlot + Plot containing map. + + """ + self._validate_map(map_name) + comp = self._validate_component(map_name, component) + + # Set default plot parameters then update with any input + plot_params = {} # should load default plotting params + plot_params.update(self.data.get_metadata(map_name, 'plot_params', {})) + + # Add extra info to label + clabel = plot_params.get('clabel') + if clabel is not None: + # tensor component + if isinstance(comp, tuple): + comp_fmt = ' (' + '{}' * len(comp) + ')' + clabel += comp_fmt.format(*(i+1 for i in comp)) + elif isinstance(comp, str): + clabel += f' ({comp.replace("_", " ")})' + # unit + unit = self.data.get_metadata(map_name, 'unit') + if unit is not None and unit != '': + clabel += f' ({unit})' + + plot_params['clabel'] = clabel + + if self.scale is not None: + binning = self.data.get_metadata(map_name, 'binning', 1) + plot_params['scale'] = self.scale / binning + + plot_params.update(kwargs) + + map_data = self._extract_component(self.data[map_name], comp) + + return MapPlot.create(self, map_data, **plot_params) + + def calc_grain_average(self, map_data, grain_ids=-1): + """Calculate grain average of any DIC map data. + + Parameters + ---------- + map_data : numpy.ndarray + Array of map data to grain average. This must be cropped! + grain_ids : list, optional + grain_ids to perform operation on, set to -1 for all grains. + + Returns + ------- + numpy.ndarray + Array containing the grain average values. + + """ + if type(grain_ids) is int and grain_ids == -1: + grain_ids = range(len(self)) + + grain_average_data = np.zeros(len(grain_ids)) + + for i, grainId in enumerate(grain_ids): + grain = self[grainId] + grainData = grain.grain_data(map_data) + grain_average_data[i] = grainData.mean() + + return grain_average_data + + def grain_data_to_map(self, name): + map_data = np.zeros(self[0].data[name].shape[:-1] + self.shape) + for grain in self: + for i, point in enumerate(grain.data.point): + map_data[..., point[1], point[0]] = grain.data[name][..., i] + + return map_data + + def grain_data_to_map_data(self, grain_data, grain_ids=-1, bg=0): + """Create a map array with each grain filled with the given + values. + + Parameters + ---------- + grain_data : list or numpy.ndarray + Grain values. This can be a single value per grain or RGB + values. + grain_ids : list of int or int, optional + IDs of grains to plot for. Use -1 for all grains in the map. + bg : int or real, optional + Value to fill the background with. + + Returns + ------- + grain_map: numpy.ndarray + Array filled with grain data values + + """ + if type(grain_ids) is int: + if grain_ids == -1: + grain_ids = range(len(self)) + else: + grain_ids = [grain_ids] + + grain_data = np.array(grain_data) + if grain_data.shape[0] != len(grain_ids): + raise ValueError("The length of supplied grain data does not" + "match the number of grains.") + if len(grain_data.shape) == 1: + mapShape = [self.y_dim, self.x_dim] + elif len(grain_data.shape) == 2 and grain_data.shape[1] == 3: + mapShape = [self.y_dim, self.x_dim, 3] + else: + raise ValueError("The grain data supplied must be either a" + "single value or RGB values per grain.") + + grain_map = np.full(mapShape, bg, dtype=grain_data.dtype) + for grainId, grain_value in zip(grain_ids, grain_data): + for point in self[grainId].data.point: + grain_map[point[1], point[0]] = grain_value + + return grain_map + + def plot_grain_data_map( + self, map_data=None, grain_data=None, grain_ids=-1, bg=0, **kwargs + ): + """Plot a grain map with grains coloured by given data. The data + can be provided as a list of values per grain or as a map which + a grain average will be applied. + + Parameters + ---------- + map_data : numpy.ndarray, optional + Array of map data. This must be cropped! Either mapData or + grain_data must be supplied. + grain_data : list or np.array, optional + Grain values. This an be a single value per grain or RGB + values. You must supply either mapData or grain_data. + grain_ids: list of int or int, optional + IDs of grains to plot for. Use -1 for all grains in the map. + bg: int or real, optional + Value to fill the background with. + kwargs : dict, optional + Keyword arguments passed to :func:`defdap.plotting.MapPlot.create` + + Returns + ------- + plot: defdap.plotting.MapPlot + Plot object created + + """ + # Set default plot parameters then update with any input + plot_params = {} + plot_params.update(kwargs) + + if grain_data is None: + if map_data is None: + raise ValueError("Either 'mapData' or 'grain_data' must " + "be supplied.") + else: + grain_data = self.calc_grain_average(map_data, grain_ids=grain_ids) + + grain_map = self.grain_data_to_map_data(grain_data, grain_ids=grain_ids, + bg=bg) + + plot = MapPlot.create(self, grain_map, **plot_params) + + return plot + + def plot_grain_data_ipf( + self, direction, map_data=None, grain_data=None, grain_ids=-1, + **kwargs + ): + """ + Plot IPF of grain reference (average) orientations with + points coloured by grain average values from map data. + + Parameters + ---------- + direction : numpy.ndarray + Vector of reference direction for the IPF. + map_data : numpy.ndarray + Array of map data. This must be cropped! Either mapData or + grain_data must be supplied. + grain_data : list or np.array, optional + Grain values. This an be a single value per grain or RGB + values. You must supply either mapData or grain_data. + grain_ids: list of int or int, optional + IDs of grains to plot for. Use -1 for all grains in the map. + kwargs : dict, optional + Keyword arguments passed to :func:`defdap.quat.Quat.plot_ipf` + + """ + # Set default plot parameters then update with any input + plot_params = {} + plot_params.update(kwargs) + + if grain_data is None: + if map_data is None: + raise ValueError("Either 'mapData' or 'grain_data' must " + "be supplied.") + else: + grain_data = self.calc_grain_average(map_data, grain_ids=grain_ids) + + if type(grain_ids) is int and grain_ids == -1: + grain_ids = range(len(self)) + + if len(grain_data) != len(grain_ids): + raise Exception("Must be 1 value for each grain in grain_data.") + + grain_ori = np.empty(len(grain_ids), dtype=Quat) + + for i, grainId in enumerate(grain_ids): + grain = self[grainId] + grain_ori[i] = grain.ref_ori + + plot = Quat.plot_ipf(grain_ori, direction, self.crystal_sym, + c=grain_data, **plot_params) + + return plot + + +class Grain(ABC): + """ + Base class for a grain. + + Attributes + ---------- + grain_id : int + + owner_map : defdap.base.Map + + """ + def __init__(self, grain_id, owner_map, group_id): + self.data = Datastore(group_id=group_id) + self.data.add_derivative( + owner_map.data, self.grain_data, + in_props={ + 'type': 'map' + }, + out_props={ + 'type': 'list' + } + ) + self.data.add( + 'point', [], + unit='', type='list', order=1 + ) + + # list of coords stored as tuples (x, y). These are coords in a + # cropped image if crop exists. + self.grain_id = grain_id + self.owner_map = owner_map + + def __len__(self): + return len(self.data.point) + + def __str__(self): + return f"Grain(ID={self.grain_id})" + + @property + def extreme_coords(self): + """Coordinates of the bounding box for a grain. + + Returns + ------- + int, int, int, int + minimum x, minimum y, maximum x, maximum y. + + """ + return *self.data.point.min(axis=0), *self.data.point.max(axis=0) + + def centre_coords(self, centre_type="box", grain_coords=True): + """ + Calculates the centre of the grain, either as the centre of the + bounding box or the grains centre of mass. + + Parameters + ---------- + centre_type : str, optional, {'box', 'com'} + Set how to calculate the centre. Either 'box' for centre of + bounding box or 'com' for centre of mass. Default is 'box'. + grain_coords : bool, optional + If set True the centre is returned in the grain coordinates + otherwise in the map coordinates. Defaults is grain. + + Returns + ------- + int, int + Coordinates of centre of grain. + + """ + x0, y0, xmax, ymax = self.extreme_coords + if centre_type == "box": + x_centre = round((xmax + x0) / 2) + y_centre = round((ymax + y0) / 2) + elif centre_type == "com": + x_centre, y_centre = self.data.point.mean(axis=0).round() + else: + raise ValueError("centreType must be box or com") + + if grain_coords: + x_centre -= x0 + y_centre -= y0 + + return int(x_centre), int(y_centre) + + def grain_outline(self, bg=np.nan, fg=0): + """Generate an array of the grain outline. + + Parameters + ---------- + bg : int + Value for points not within grain. + fg : int + Value for points within grain. + + Returns + ------- + numpy.ndarray + Bounding box for grain with :obj:`~numpy.nan` outside the grain and given number within. + + """ + x0, y0, xmax, ymax = self.extreme_coords + + # initialise array with nans so area not in grain displays white + outline = np.full((ymax - y0 + 1, xmax - x0 + 1), bg, dtype=int) + + for coord in self.data.point: + outline[coord[1] - y0, coord[0] - x0] = fg + + return outline + + def plot_outline(self, ax=None, plot_scale_bar=False, **kwargs): + """Plot the outline of the grain. + + Parameters + ---------- + ax : matplotlib.axes.Axes + axis to plot on, if not provided the current active axis is used. + plot_scale_bar : bool + plots the scale bar on the grain if true. + kwargs : dict + keyword arguments passed to :func:`defdap.plotting.GrainPlot.add_map` + + Returns + ------- + defdap.plotting.GrainPlot + + """ + plot = plotting.GrainPlot(self, ax=ax) + plot.addMap(self.grain_outline(), **kwargs) + + if plot_scale_bar: + plot.add_scale_bar() + + return plot + + def grain_data(self, map_data): + """Extract this grains data from the given map data. + + Parameters + ---------- + map_data : numpy.ndarray + Array of map data. This must be cropped! + + Returns + ------- + numpy.ndarray + Array containing this grains values from the given map data. + + """ + grain_data = np.zeros(len(self), dtype=map_data.dtype) + + for i, coord in enumerate(self.data.point): + grain_data[i] = map_data[coord[1], coord[0]] + + return grain_data + + def grain_map_data(self, map_data=None, grain_data=None, bg=np.nan): + """Extract a single grain map from the given map data. + + Parameters + ---------- + map_data : numpy.ndarray + Array of map data. This must be cropped! Either this or + 'grain_data' must be supplied and 'grain_data' takes precedence. + grain_data : numpy.ndarray + Array of data at each point in the grain. Either this or + 'mapData' must be supplied and 'grain_data' takes precedence. + bg : various, optional + Value to fill the background with. Must be same dtype as + input array. + + Returns + ------- + numpy.ndarray + Grain map extracted from given data. + + """ + if grain_data is None: + if map_data is None: + raise ValueError("Either 'mapData' or 'grain_data' must " + "be supplied.") + else: + grain_data = self.grain_data(map_data) + x0, y0, xmax, ymax = self.extreme_coords + + grain_map_data = np.full((ymax - y0 + 1, xmax - x0 + 1), bg, + dtype=type(grain_data[0])) + + for coord, data in zip(self.data.point, grain_data): + grain_map_data[coord[1] - y0, coord[0] - x0] = data + + return grain_map_data + + def grain_map_data_coarse(self, map_data=None, grain_data=None, + kernel_size=2, bg=np.nan): + """ + Create a coarsened data map of this grain only from the given map + data. Data is coarsened using a kernel at each pixel in the + grain using only data in this grain. + + Parameters + ---------- + map_data : numpy.ndarray + Array of map data. This must be cropped! Either this or + 'grain_data' must be supplied and 'grain_data' takes precedence. + grain_data : numpy.ndarray + List of data at each point in the grain. Either this or + 'mapData' must be supplied and 'grain_data' takes precedence. + kernel_size : int, optional + Size of kernel as the number of pixels to dilate by i.e 1 + gives a 3x3 kernel. + bg : various, optional + Value to fill the background with. Must be same dtype as + input array. + + Returns + ------- + numpy.ndarray + Map of this grains coarsened data. + + """ + grain_map_data = self.grain_map_data(map_data=map_data, grain_data=grain_data) + grain_map_data_coarse = np.full_like(grain_map_data, np.nan) + + for i, j in np.ndindex(grain_map_data.shape): + if np.isnan(grain_map_data[i, j]): + grain_map_data_coarse[i, j] = bg + else: + coarse_value = 0 + + if i - kernel_size >= 0: + yLow = i - kernel_size + else: + yLow = 0 + if i + kernel_size + 1 <= grain_map_data.shape[0]: + yHigh = i + kernel_size + 1 + else: + yHigh = grain_map_data.shape[0] + if j - kernel_size >= 0: + x_low = j - kernel_size + else: + x_low = 0 + if j + kernel_size + 1 <= grain_map_data.shape[1]: + x_high = j + kernel_size + 1 + else: + x_high = grain_map_data.shape[1] + + num_points = 0 + for k in range(yLow, yHigh): + for l in range(x_low, x_high): + if not np.isnan(grain_map_data[k, l]): + coarse_value += grain_map_data[k, l] + num_points += 1 + + if num_points > 0: + grain_map_data_coarse[i, j] = coarse_value / num_points + else: + grain_map_data_coarse[i, j] = np.nan + + return grain_map_data_coarse + + def plot_grain_data(self, map_data=None, grain_data=None, **kwargs): + """ + Plot a map of this grain from the given map data. + + Parameters + ---------- + map_data : numpy.ndarray + Array of map data. This must be cropped! Either this or + 'grain_data' must be supplied and 'grain_data' takes precedence. + grain_data : numpy.ndarray + List of data at each point in the grain. Either this or + 'mapData' must be supplied and 'grain_data' takes precedence. + kwargs : dict, optional + Keyword arguments passed to :func:`defdap.plotting.GrainPlot.create` + + """ + # Set default plot parameters then update with any input + plot_params = {} + plot_params.update(kwargs) + + grain_map_data = self.grain_map_data(map_data=map_data, grain_data=grain_data) + + plot = GrainPlot.create(self, grain_map_data, **plot_params) + + return plot diff --git a/build/lib/defdap/crystal.py b/build/lib/defdap/crystal.py new file mode 100644 index 0000000..e6e4fa7 --- /dev/null +++ b/build/lib/defdap/crystal.py @@ -0,0 +1,728 @@ +# Copyright 2023 Mechanics of Microstructures Group +# at The University of Manchester +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import numpy as np +from numpy.linalg import norm + +from defdap import defaults +from defdap.quat import Quat + + +class Phase(object): + def __init__(self, name, laue_group, space_group, lattice_params): + """ + Parameters + ---------- + name : str + Name of the phase + laue_group : int + Laue group + space_group : int + Space group + lattice_params : tuple + Lattice parameters in order (a,b,c,alpha,beta,gamma) + + """ + self.name = name + self.laue_group = laue_group + self.spaceGroup = space_group + self.lattice_params = lattice_params + + try: + self.crystal_structure = { + 9: crystalStructures['hexagonal'], + 11: crystalStructures['cubic'], + }[laue_group] + except KeyError: + raise ValueError(f"Unknown Laue group key: {laue_group}") + + if self.crystal_structure is crystalStructures['hexagonal']: + self.ss_file = defaults['slip_system_file']['HCP'] + else: + try: + self.ss_file = defaults['slip_system_file'][ + {225: 'FCC', 229: 'BCC'}[space_group] + # See http://pd.chem.ucl.ac.uk/pdnn/symm3/allsgp.htm + ] + except KeyError: + self.ss_file = None + + if self.ss_file is None: + self.slip_systems = None + self.slip_trace_colours = None + else: + self.slip_systems, self.slip_trace_colours = SlipSystem.load( + self.ss_file, self.crystal_structure, c_over_a=self.c_over_a + ) + + def __str__(self): + text = ("Phase: {:}\n Crystal structure: {:}\n Lattice params: " + "({:.2f}, {:.2f}, {:.2f}, {:.0f}, {:.0f}, {:.0f})\n" + " Slip systems: {:}") + return text.format(self.name, self.crystal_structure.name, + *self.lattice_params[:3], + *np.array(self.lattice_params[3:]) * 180 / np.pi, + self.ss_file) + + @property + def c_over_a(self): + if self.crystal_structure is crystalStructures['hexagonal']: + return self.lattice_params[2] / self.lattice_params[0] + return None + + def print_slip_systems(self): + """Print a list of slip planes (with colours) and slip directions. + + """ + # TODO: this should be moved to static method of the SlipSystem class + for i, (ss_group, colour) in enumerate(zip(self.slip_systems, + self.slip_trace_colours)): + print('Plane {0}: {1}\tColour: {2}'.format( + i, ss_group[0].slip_plane_label, colour + )) + for j, ss in enumerate(ss_group): + print(' Direction {0}: {1}'.format(j, ss.slip_dir_label)) + + +class CrystalStructure(object): + def __init__(self, name, symmetries, vertices, faces): + self.name = name + self.symmetries = symmetries + self.vertices = vertices + self.faces = faces + + # TODO: Move these to the phase class where the lattice parameters + # can be accessed + @staticmethod + def l_matrix(a, b, c, alpha, beta, gamma, convention=None): + """ Construct L matrix based on Page 22 of + Randle and Engle - Introduction to texture analysis""" + l_matrix = np.zeros((3, 3)) + + cos_alpha = np.cos(alpha) + cos_beta = np.cos(beta) + cos_gamma = np.cos(gamma) + + sin_gamma = np.sin(gamma) + + l_matrix[0, 0] = a + l_matrix[0, 1] = b * cos_gamma + l_matrix[0, 2] = c * cos_beta + + l_matrix[1, 1] = b * sin_gamma + l_matrix[1, 2] = c * (cos_alpha - cos_beta * cos_gamma) / sin_gamma + + l_matrix[2, 2] = c * np.sqrt( + 1 + 2 * cos_alpha * cos_beta * cos_gamma - + cos_alpha**2 - cos_beta**2 - cos_gamma**2 + ) / sin_gamma + + # OI/HKL convention - x // [10-10], y // a2 [-12-10] + # TSL convention - x // a1 [2-1-10], y // [01-10] + if convention is None: + convention = defaults['crystal_ortho_conv'] + + if convention.lower() in ['hkl', 'oi']: + # Swap 00 with 11 and 01 with 10 due to how OI orthonormalises + # From Brad Wynne + t1 = l_matrix[0, 0] + t2 = l_matrix[1, 0] + + l_matrix[0, 0] = l_matrix[1, 1] + l_matrix[1, 0] = l_matrix[0, 1] + + l_matrix[1, 1] = t1 + l_matrix[0, 1] = t2 + + elif convention.lower() != 'tsl': + raise ValueError( + f"Unknown convention '{convention}' for orthonormalisation of " + f"crystal structure, can be 'hkl' or 'tsl'" + ) + + # Set small components to 0 + l_matrix[np.abs(l_matrix) < 1e-10] = 0 + + return l_matrix + + @staticmethod + def q_matrix(l_matrix): + """ Construct matrix of reciprocal lattice vectors to transform + plane normals See C. T. Young and J. L. Lytton, J. Appl. Phys., + vol. 43, no. 4, pp. 1408–1417, 1972.""" + a = l_matrix[:, 0] + b = l_matrix[:, 1] + c = l_matrix[:, 2] + + volume = abs(np.dot(a, np.cross(b, c))) + a_star = np.cross(b, c) / volume + b_star = np.cross(c, a) / volume + c_star = np.cross(a, b) / volume + + q_matrix = np.stack((a_star, b_star, c_star), axis=1) + + return q_matrix + + +over_root2 = np.sqrt(2) / 2 +sqrt3over2 = np.sqrt(3) / 2 +# Use ideal ratio as only used for plotting unit cell +c_over_a = 1.633 / 2 + +crystalStructures = { + "cubic": CrystalStructure( + "cubic", + [ + # identity + Quat(1.0, 0.0, 0.0, 0.0), + + # cubic tetrads(100) + Quat(over_root2, over_root2, 0.0, 0.0), + Quat(0.0, 1.0, 0.0, 0.0), + Quat(over_root2, -over_root2, 0.0, 0.0), + Quat(over_root2, 0.0, over_root2, 0.0), + Quat(0.0, 0.0, 1.0, 0.0), + Quat(over_root2, 0.0, -over_root2, 0.0), + Quat(over_root2, 0.0, 0.0, over_root2), + Quat(0.0, 0.0, 0.0, 1.0), + Quat(over_root2, 0.0, 0.0, -over_root2), + + # cubic dyads (110) + Quat(0.0, over_root2, over_root2, 0.0), + Quat(0.0, -over_root2, over_root2, 0.0), + Quat(0.0, over_root2, 0.0, over_root2), + Quat(0.0, -over_root2, 0.0, over_root2), + Quat(0.0, 0.0, over_root2, over_root2), + Quat(0.0, 0.0, -over_root2, over_root2), + + # cubic triads (111) + Quat(0.5, 0.5, 0.5, 0.5), + Quat(0.5, -0.5, -0.5, -0.5), + Quat(0.5, -0.5, 0.5, 0.5), + Quat(0.5, 0.5, -0.5, -0.5), + Quat(0.5, 0.5, -0.5, 0.5), + Quat(0.5, -0.5, 0.5, -0.5), + Quat(0.5, 0.5, 0.5, -0.5), + Quat(0.5, -0.5, -0.5, 0.5) + ], + np.array([ + [-0.5, -0.5, -0.5], + [0.5, -0.5, -0.5], + [0.5, 0.5, -0.5], + [-0.5, 0.5, -0.5], + [-0.5, -0.5, 0.5], + [0.5, -0.5, 0.5], + [0.5, 0.5, 0.5], + [-0.5, 0.5, 0.5] + ]), + [ + [0, 1, 2, 3], + [4, 5, 6, 7], + [0, 1, 5, 4], + [1, 2, 6, 5], + [2, 3, 7, 6], + [3, 0, 4, 7] + ] + ), + "hexagonal": CrystalStructure( + "hexagonal", + [ + # identity + Quat(1.0, 0.0, 0.0, 0.0), + + Quat(0.0, 1.0, 0.0, 0.0), + Quat(0.0, 0.0, 1.0, 0.0), + Quat(0.0, 0.0, 0.0, 1.0), + + # hexagonal hexads + Quat(sqrt3over2, 0.0, 0.0, 0.5), + Quat(0.5, 0.0, 0.0, sqrt3over2), + Quat(0.5, 0.0, 0.0, -sqrt3over2), + Quat(sqrt3over2, 0.0, 0.0, -0.5), + + # hexagonal diads + Quat(0.0, -0.5, -sqrt3over2, 0.0), + Quat(0.0, 0.5, -sqrt3over2, 0.0), + Quat(0.0, sqrt3over2, -0.5, 0.0), + Quat(0.0, -sqrt3over2, -0.5, 0.0) + ], + np.array([ + [1, 0, -c_over_a], + [0.5, sqrt3over2, -c_over_a], + [-0.5, sqrt3over2, -c_over_a], + [-1, 0, -c_over_a], + [-0.5, -sqrt3over2, -c_over_a], + [0.5, -sqrt3over2, -c_over_a], + [1, 0, c_over_a], + [0.5, sqrt3over2, c_over_a], + [-0.5, sqrt3over2, c_over_a], + [-1, 0, c_over_a], + [-0.5, -sqrt3over2, c_over_a], + [0.5, -sqrt3over2, c_over_a] + ]), + [ + [0, 1, 2, 3, 4, 5], + [6, 7, 8, 9, 10, 11], + [0, 6, 7, 1], + [1, 7, 8, 2], + [2, 8, 9, 3], + [3, 9, 10, 4], + [4, 10, 11, 5], + [5, 11, 6, 0] + ] + ) +} + + +class SlipSystem(object): + """Class used for defining and performing operations on a slip system. + + """ + def __init__(self, slip_plane, slip_dir, crystal_structure, c_over_a=None): + """Initialise a slip system object. + + Parameters + ---------- + slip_plane: numpy.ndarray + Slip plane. + slip_dir: numpy.ndarray + Slip direction. + crystal_structure : defdap.crystal.CrystalStructure + Crystal structure of the slip system. + c_over_a : float, optional + C over a ratio for hexagonal crystals. + + """ + self.crystal_structure = crystal_structure + + # Stored as Miller indices (Miller-Bravais for hexagonal) + self.plane_idc = tuple(slip_plane) + self.dir_idc = tuple(slip_dir) + + # Stored as vectors in a cartesian basis + if self.crystal_structure.name == "cubic": + self.slip_plane = slip_plane / norm(slip_plane) + self.slip_dir = slip_dir / norm(slip_dir) + self.c_over_a = None + elif self.crystal_structure.name == "hexagonal": + if c_over_a is None: + raise Exception("No c over a ratio given") + self.c_over_a = c_over_a + + # Convert plane and dir from Miller-Bravais to Miller + slip_plane_m = convert_idc('mb', plane=slip_plane) + slip_dir_m = convert_idc('mb', dir=slip_dir) + + # Transformation from crystal to orthonormal coords + l_matrix = CrystalStructure.l_matrix( + 1, 1, c_over_a, np.pi / 2, np.pi / 2, np.pi * 2 / 3 + ) + # Q matrix for transforming planes + qMatrix = CrystalStructure.q_matrix(l_matrix) + + # Transform into orthonormal basis and then normalise + self.slip_plane = np.matmul(qMatrix, slip_plane_m) + self.slip_plane /= norm(self.slip_plane) + self.slip_dir = np.matmul(l_matrix, slip_dir_m) + self.slip_dir /= norm(self.slip_dir) + else: + raise Exception("Only cubic and hexagonal currently supported.") + + def __eq__(self, right): + # or one divide the other should be a constant for each place. + return (pos_idc(self.plane_idc) == pos_idc(right.plane_idc) and + pos_idc(self.dir_idc) == pos_idc(right.dir_idc)) + + def __hash__(self): + return hash(pos_idc(self.plane_idc) + pos_idc(self.dir_idc)) + + def __str__(self): + return self.slip_plane_label + self.slip_dir_label + + def __repr__(self): + return (f"SlipSystem(slipPlane={self.slip_plane_label}, " + f"slipDir={self.slip_dir_label}, " + f"symmetry={self.crystal_structure.name})") + + @property + def slip_plane_label(self): + """Return the slip plane label. For example '(111)'. + + Returns + ------- + str + Slip plane label. + + """ + return '(' + ''.join(map(str_idx, self.plane_idc)) + ')' + + @property + def slip_dir_label(self): + """Returns the slip direction label. For example '[110]'. + + Returns + ------- + str + Slip direction label. + + """ + return '[' + ''.join(map(str_idx, self.dir_idc)) + ']' + + def generate_family(self): + """Generate the family of slip systems which this system belongs to. + + Returns + ------- + list of SlipSystem + The family of slip systems. + + """ + # + symms = self.crystal_structure.symmetries + + ss_family = set() # will not preserve order + + plane = self.plane_idc + dir = self.dir_idc + + if self.crystal_structure.name == 'hexagonal': + # Transformation from crystal to orthonormal coords + l_matrix = CrystalStructure.l_matrix( + 1, 1, self.c_over_a, np.pi / 2, np.pi / 2, np.pi * 2 / 3 + ) + # Q matrix for transforming planes + q_matrix = CrystalStructure.q_matrix(l_matrix) + + # Transform into orthonormal basis + plane = np.matmul(q_matrix, convert_idc('mb', plane=plane)) + dir = np.matmul(l_matrix, convert_idc('mb', dir=dir)) + + for i, symm in enumerate(symms): + symm = symm.conjugate + + plane_symm = symm.transform_vector(plane) + dir_symm = symm.transform_vector(dir) + + if self.crystal_structure.name == 'hexagonal': + # q_matrix inverse is equal to l_matrix transposed and vice-versa + plane_symm = reduce_idc(convert_idc( + 'm', plane=safe_int_cast(np.matmul(l_matrix.T, plane_symm)) + )) + dir_symm = reduce_idc(convert_idc( + 'm', dir=safe_int_cast(np.matmul(q_matrix.T, dir_symm)) + )) + + ss_family.add(SlipSystem( + pos_idc(safe_int_cast(plane_symm)), + pos_idc(safe_int_cast(dir_symm)), + self.crystal_structure, c_over_a=self.c_over_a + )) + + return ss_family + + @staticmethod + def load(name, crystal_structure, c_over_a=None, group_by='plane'): + """ + Load in slip systems from file. 3 integers for slip plane + normal and 3 for slip direction. Returns a list of list of slip + systems grouped by slip plane. + + Parameters + ---------- + name : str + Name of the slip system file (without file extension) + stored in the defdap install dir or path to a file. + crystal_structure : defdap.crystal.CrystalStructure + Crystal structure of the slip systems. + c_over_a : float, optional + C over a ratio for hexagonal crystals. + group_by : str, optional + How to group the slip systems, either by slip plane ('plane') + or slip system family ('family') or don't group (None). + + Returns + ------- + list of list of SlipSystem + A list of list of slip systems grouped slip plane. + + Raises + ------ + IOError + Raised if not 6/8 integers per line. + + """ + # try and load from package dir first + try: + file_ext = ".txt" + package_dir, _ = os.path.split(__file__) + filepath = f"{package_dir}/slip_systems/{name}{file_ext}" + + slip_system_file = open(filepath) + + except FileNotFoundError: + # if it doesn't exist in the package dir, try and load the path + try: + filepath = name + + slip_system_file = open(filepath) + + except FileNotFoundError: + raise(FileNotFoundError("Couldn't find the slip systems file")) + + slip_system_file.readline() + slip_trace_colours = slip_system_file.readline().strip().split(',') + slip_system_file.close() + + if crystal_structure.name == "hexagonal": + vect_size = 4 + else: + vect_size = 3 + + ss_data = np.loadtxt(filepath, delimiter='\t', skiprows=2, + dtype=np.int8) + if ss_data.shape[1] != 2 * vect_size: + raise IOError("Slip system file not valid") + + # Create list of slip system objects + slip_systems = [] + for row in ss_data: + slip_systems.append(SlipSystem( + row[0:vect_size], row[vect_size:2 * vect_size], + crystal_structure, c_over_a=c_over_a + )) + + # Group slip systems is required + if group_by is not None: + slip_systems = SlipSystem.group(slip_systems, group_by) + + return slip_systems, slip_trace_colours + + @staticmethod + def group(slip_systems, group_by): + """ + Groups slip systems by their slip plane. + + Parameters + ---------- + slip_systems : list of SlipSystem + A list of slip systems. + group_by : str + How to group the slip systems, either by slip plane ('plane') + or slip system family ('family'). + + Returns + ------- + list of list of SlipSystem + A list of list of grouped slip systems. + + """ + if group_by.lower() == 'plane': + # Group by slip plane and keep slip plane order from file + grouped_slip_systems = [[slip_systems[0]]] + for ss in slip_systems[1:]: + for i, ssGroup in enumerate(grouped_slip_systems): + if pos_idc(ss.plane_idc) == pos_idc(ssGroup[0].plane_idc): + grouped_slip_systems[i].append(ss) + break + else: + grouped_slip_systems.append([ss]) + + elif group_by.lower() == 'family': + grouped_slip_systems = [] + ssFamilies = [] + for ss in slip_systems: + for i, ssFamily in enumerate(ssFamilies): + if ss in ssFamily: + grouped_slip_systems[i].append(ss) + break + else: + grouped_slip_systems.append([ss]) + ssFamilies.append(ss.generate_family()) + + else: + raise ValueError("Slip systems can be grouped by plane or family") + + return grouped_slip_systems + + @staticmethod + def print_slip_system_directory(): + """ + Prints the location where slip system definition files are stored. + + """ + package_dir, _ = os.path.split(__file__) + print("Slip system definition files are stored in directory:") + print(f"{package_dir}/slip_systems/") + + +def convert_idc(in_type, *, dir=None, plane=None): + """ + Convert between Miller and Miller-Bravais indices. + + Parameters + ---------- + in_type : str {'m', 'mb'} + Type of indices provided. If 'm' converts from Miller to + Miller-Bravais, opposite for 'mb'. + dir : tuple of int or equivalent, optional + Direction to convert. This OR `plane` must me provided. + plane : tuple of int or equivalent, optional + Plane to convert. This OR `direction` must me provided. + + Returns + ------- + tuple of int + The converted plane or direction. + + """ + if dir is None and plane is None: + raise ValueError("One of either `direction` or `plane` must be " + "provided.") + if dir is not None and plane is not None: + raise ValueError("One of either `direction` or `plane` must be " + "provided, not both.") + + def check_len(val, length): + if len(val) != length: + raise ValueError(f"Vector must have {length} values.") + + if in_type.lower() == 'm': + if dir is None: + # plane M->MB + check_len(plane, 3) + out = np.array(plane)[[0, 1, 0, 2]] + out[2] += plane[1] + out[2] *= -1 + + else: + # direction M->MB + check_len(dir, 3) + u, v, w = dir + out = np.array([2*u-v, 2*v-u, -u-v, 3*w]) / 3 + try: + # Attempt to cast to integers + out = safe_int_cast(out) + except ValueError: + pass + + elif in_type.lower() == 'mb': + if dir is None: + # plane MB->M + check_len(plane, 4) + out = np.array(plane)[[0, 1, 3]] + + else: + # direction MB->M + check_len(dir, 4) + out = np.array(dir)[[0, 1, 3]] + out[[0, 1]] -= dir[2] + + else: + raise ValueError("`inType` must be either 'm' or 'mb'.") + + return tuple(out) + + +def pos_idc(vec): + """ + Return a consistent positive version of a set of indices. + + Parameters + ---------- + vec : tuple of int or equivalent + Indices to convert. + + Returns + ------- + tuple of int + Positive version of indices. + + """ + for idx in vec: + if idx == 0: + continue + if idx > 0: + return tuple(vec) + else: + return tuple(-np.array(vec)) + + +def reduce_idc(vec): + """ + Reduce indices to lowest integers + + Parameters + ---------- + vec : tuple of int or equivalent + Indices to reduce. + + Returns + ------- + tuple of int + The reduced indices. + + """ + return tuple((np.array(vec) / np.gcd.reduce(vec)).astype(np.int8)) + + +def safe_int_cast(vec, tol=1e-3): + """ + Cast a tuple of floats to integers, raising an error if rounding is + over a tolerance. + + Parameters + ---------- + vec : tuple of float or equivalent + Vector to cast. + tol : float + Tolerance above which an error is raised. + + Returns + ------- + tuple of int + + Raises + ------ + ValueError + If the rounding is over the tolerance for any value. + + """ + vec = np.array(vec) + vec_rounded = vec.round() + + if np.any(np.abs(vec - vec_rounded) > tol): + raise ValueError('Rounding too large', np.abs(vec - vec_rounded)) + + return tuple(vec_rounded.astype(np.int8)) + + +def str_idx(idx): + """ + String representation of an index with overbars. + + Parameters + ---------- + idx : int + + Returns + ------- + str + + """ + if not isinstance(idx, (int, np.integer)): + raise ValueError("Index must be an integer.") + + return str(idx) if idx >= 0 else str(-idx) + u'\u0305' diff --git a/build/lib/defdap/ebsd.py b/build/lib/defdap/ebsd.py new file mode 100644 index 0000000..3a6a8fa --- /dev/null +++ b/build/lib/defdap/ebsd.py @@ -0,0 +1,1916 @@ +# Copyright 2023 Mechanics of Microstructures Group +# at The University of Manchester +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +from skimage import morphology as mph +import networkx as nx + +import copy +from warnings import warn + +from defdap.utils import Datastore +from defdap.file_readers import EBSDDataLoader +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 +from defdap.utils import report_progress + + +class Map(base.Map): + """ + Class to encapsulate an EBSD map and useful analysis and plotting + methods. + + Attributes + ---------- + step_size : float + Step size in micron. + phases : list of defdap.crystal.Phase + List of phases. + mis_ori : numpy.ndarray + Map of misorientation. + mis_ori_axis : list of numpy.ndarray + Map of misorientation axis components. + origin : tuple(int) + Map origin (x, y). Used by linker class where origin is a + homologue point of the maps. + + data : defdap.utils.Datastore + Must contain after loading data (maps): + phase : numpy.ndarray + 1-based, 0 is non-indexed points + euler_angle : numpy.ndarray + stored as (3, y_dim, x_dim) in radians + Generated data: + orientation : numpy.ndarray of defdap.quat.Quat + Quaterion for each point of map. Shape (y_dim, x_dim). + grain_boundaries : BoundarySet + phase_boundaries : BoundarySet + grains : numpy.ndarray of int + Map of grains. Grain numbers start at 1 here but everywhere else + grainID starts at 0. Regions that are smaller than the minimum + grain size are given value -2. Remnant boundary points are -1. + KAM : numpy.ndarray + Kernal average misorientaion map. + GND : numpy.ndarray + GND scalar map. + Nye_tensor : numpy.ndarray + 3x3 Nye tensor at each point. + Derived data: + Grain list data to map data from all grains + + """ + MAPNAME = 'ebsd' + + def __init__(self, *args, **kwargs): + """ + Initialise class and load EBSD data. + + Parameters + ---------- + *args, **kwarg + Passed to base constructor + + """ + # Initialise variables + self.step_size = None + self.phases = [] + + # Call base class constructor + super(Map, self).__init__(*args, **kwargs) + + self.mis_ori = None + self.mis_ori_axis = None + self.origin = (0, 0) + + # Phase used for the maps crystal structure and c_over_a. So old + # functions still work for the 'main' phase in the map. 0-based + self.primary_phase_id = 0 + + # Use euler map for defining homologous points + self.plot_default = self.plot_euler_map + self.homog_map_name = 'band_contrast' + self.highlight_alpha = 1 + + self.data.add_generator( + 'orientation', self.calc_quat_array, unit='', type='map', + order=0, default_component='IPF_x', + ) + self.data.add_generator( + ('phase_boundaries', 'grain_boundaries'), self.find_boundaries, + type='boundaries', + ) + self.data.add_generator( + 'grains', self.find_grains, unit='', type='map', order=0 + ) + self.data.add_generator( + 'KAM', self.calc_kam, unit='rad', type='map', order=0, + plot_params={ + 'plot_colour_bar': True, + 'clabel': 'KAM', + } + ) + self.data.add_generator( + ('GND', 'Nye_tensor'), self.calc_nye, + unit='', type='map', + metadatas=({ + 'order': 0, + 'plot_params': { + 'plot_colour_bar': True, + 'clabel': 'GND content', + } + }, { + 'order': 2, + 'save': False, + 'default_component': (0, 0), + 'plot_params': { + 'plot_colour_bar': True, + 'clabel': 'Nye tensor', + } + }) + ) + + @report_progress("loading EBSD data") + def load_data(self, file_name, data_type=None): + """Load in EBSD data from file. + + Parameters + ---------- + file_name : pathlib.Path + Path to EBSD file + data_type : str, {'OxfordBinary', 'OxfordText', 'EdaxAng', 'PythonDict'} + Format of EBSD data file. + + """ + data_loader = EBSDDataLoader.get_loader(data_type, file_name) + data_loader.load(file_name) + + metadata_dict = data_loader.loaded_metadata + self.shape = metadata_dict['shape'] + self.step_size = metadata_dict['step_size'] + self.phases = metadata_dict['phases'] + + self.data.update(data_loader.loaded_data) + + # write final status + yield (f"Loaded EBSD data (dimensions: {self.x_dim} x {self.y_dim} " + f"pixels, step size: {self.step_size} um)") + + def save(self, file_name, data_type=None, file_dir=""): + """Save EBSD map to file. + + Parameters + ---------- + file_name : str + Name of file to save to, it must not already exist. + data_type : str, {'OxfordText'} + Format of EBSD data file to save. + file_dir : str + Directory to save the file to. + + """ + data_writer = EBSDDataWriter.get_writer(data_type) + + data_writer.metadata['shape'] = self.shape + data_writer.metadata['step_size'] = self.step_size + data_writer.metadata['phases'] = self.phases + + data_writer.data['phase'] = self.data.phase + data_writer.data['quat'] = self.data.orientation + data_writer.data['band_contrast'] = self.data.band_contrast + + data_writer.write(file_name, file_dir=file_dir) + + @property + def crystal_sym(self): + """Crystal symmetry of the primary phase. + + Returns + ------- + str + Crystal symmetry + + """ + return self.primary_phase.crystal_structure.name + + @property + def c_over_a(self): + """C over A ratio of the primary phase + + Returns + ------- + float or None + C over A ratio if hexagonal crystal structure otherwise None + + """ + return self.primary_phase.c_over_a + + @property + def num_phases(self): + return len(self.phases) or None + + @property + def primary_phase(self): + """Primary phase of the EBSD map. + + Returns + ------- + defdap.crystal.Phase + Primary phase + + """ + return self.phases[self.primary_phase_id] + + @property + def scale(self): + return self.step_size + + @report_progress("rotating EBSD data") + def rotate_data(self): + """Rotate map by 180 degrees and transform quats accordingly. + + """ + + self.data.euler_angle = self.data.euler_angle[:, ::-1, ::-1] + self.data.band_contrast = self.data.band_contrast[::-1, ::-1] + self.data.band_slope = self.data.band_slope[::-1, ::-1] + self.data.phase = self.data.phase[::-1, ::-1] + self.calc_quat_array() + + # Rotation from old coord system to new + transform_quat = Quat.from_axis_angle(np.array([0, 0, 1]), np.pi).conjugate + + # Perform vectorised multiplication + quats = Quat.multiply_many_quats(self.data.orientation.flatten(), transform_quat) + self.data.orientation = np.array(quats).reshape(self.shape) + + yield 1. + + def calc_euler_colour(self, map_data, phases=None, bg_colour=None): + if phases is None: + phases = self.phases + phase_ids = range(len(phases)) + else: + phase_ids = phases + phases = [self.phases[i] for i in phase_ids] + + if bg_colour is None: + bg_colour = np.array([0., 0., 0.]) + + map_colours = np.tile(bg_colour, self.shape + (1,)) + + for phase, phase_id in zip(phases, phase_ids): + if phase.crystal_structure.name == 'cubic': + norm = np.array([2 * np.pi, np.pi / 2, np.pi / 2]) + elif phase.crystal_structure.name == 'hexagonal': + norm = np.array([np.pi, np.pi, np.pi / 3]) + else: + ValueError("Only hexagonal and cubic symGroup supported") + + # Apply normalisation for each phase + phase_mask = self.data.phase == phase_id + 1 + map_colours[phase_mask] = map_data[:, phase_mask].T / norm + + return map_colours + + def calc_ipf_colour(self, map_data, direction, phases=None, + bg_colour=None): + if phases is None: + phases = self.phases + phase_ids = range(len(phases)) + else: + phase_ids = phases + phases = [self.phases[i] for i in phase_ids] + + if bg_colour is None: + bg_colour = np.array([0., 0., 0.]) + + map_colours = np.tile(bg_colour, self.shape + (1,)) + + for phase, phase_id in zip(phases, phase_ids): + # calculate IPF colours for phase + phase_mask = self.data.phase == phase_id + 1 + map_colours[phase_mask] = Quat.calc_ipf_colours( + map_data[phase_mask], direction, phase.crystal_structure.name + ).T + + return map_colours + + def plot_euler_map(self, phases=None, bg_colour=None, **kwargs): + """Plot an orientation map in Euler colouring + + Parameters + ---------- + phases : list of int + Which phases to plot for + kwargs + All arguments are passed to :func:`defdap.plotting.MapPlot.create`. + + Returns + ------- + defdap.plotting.MapPlot + + """ + # Set default plot parameters then update with any input + plot_params = {} + plot_params.update(kwargs) + + map_colours = self.calc_euler_colour( + self.data.euler_angle, phases=phases, bg_colour=bg_colour + ) + + return MapPlot.create(self, map_colours, **plot_params) + + def plot_ipf_map(self, direction, phases=None, bg_colour=None, **kwargs): + """ + Plot a map with points coloured in IPF colouring, + with respect to a given sample direction. + + Parameters + ---------- + direction : np.array len 3 + Sample direction. + phases : list of int + Which phases to plot IPF data for. + bg_colour : np.array len 3 + Colour of background (i.e. for phases not plotted). + kwargs + Other arguments passed to :func:`defdap.plotting.MapPlot.create`. + + Returns + ------- + defdap.plotting.MapPlot + + """ + # Set default plot parameters then update with any input + plot_params = {} + plot_params.update(kwargs) + + map_colours = self.calc_ipf_colour( + self.data.orientation, direction, phases=phases, + bg_colour=bg_colour + ) + + return MapPlot.create(self, map_colours, **plot_params) + + def plot_phase_map(self, **kwargs): + """Plot a phase map. + + Parameters + ---------- + kwargs + All arguments passed to :func:`defdap.plotting.MapPlot.create`. + + Returns + ------- + defdap.plotting.MapPlot + + """ + # Set default plot parameters then update with any input + plot_params = { + 'vmin': 0, + 'vmax': self.num_phases + } + plot_params.update(kwargs) + + plot = MapPlot.create(self, self.data.phase, **plot_params) + + # add a legend to the plot + phase_ids = list(range(0, self.num_phases + 1)) + phase_names = ["Non-indexed"] + [phase.name for phase in self.phases] + plot.add_legend(phase_ids, phase_names, loc=2, borderaxespad=0.) + + return plot + + @report_progress("calculating KAM") + def calc_kam(self): + """ + Calculates Kernel Average Misorientaion (KAM) for the EBSD map, + based on a 3x3 kernel. Crystal symmetric equivalences are not + considered. Stores result as `KAM`. + + """ + quat_comps = np.empty((4, ) + self.shape) + + for i, row in enumerate(self.data.orientation): + for j, quat in enumerate(row): + quat_comps[:, i, j] = quat.quat_coef + + kam = np.empty(self.shape) + + # Start with rows. Calculate misorientation with neighbouring rows. + # First and last row only in one direction + kam[0] = abs(np.einsum("ij,ij->j", + quat_comps[:, 0], quat_comps[:, 1])) + kam[-1] = abs(np.einsum("ij,ij->j", + quat_comps[:, -1], quat_comps[:, -2])) + for i in range(1, self.y_dim - 1): + kam[i] = (abs(np.einsum("ij,ij->j", + quat_comps[:, i], quat_comps[:, i + 1])) + + abs(np.einsum("ij,ij->j", + quat_comps[:, i], quat_comps[:, i - 1])) + ) / 2 + kam[kam > 1] = 1 + + # Do the same for columns + kam[:, 0] += abs(np.einsum("ij,ij->j", + quat_comps[:, :, 0], quat_comps[:, :, 1])) + kam[:, -1] += abs(np.einsum("ij,ij->j", + quat_comps[:, :, -1], quat_comps[:, :, -2])) + for i in range(1, self.x_dim - 1): + kam[:, i] += (abs(np.einsum("ij,ij->j", + quat_comps[:, :, i], + quat_comps[:, :, i + 1])) + + abs(np.einsum("ij,ij->j", + quat_comps[:, :, i], + quat_comps[:, :, i - 1])) + ) / 2 + kam /= 2 + kam[kam > 1] = 1 + + yield 1. + return 2 * np.arccos(kam) + + @report_progress("calculating Nye tensor") + def calc_nye(self): + """ + Calculates Nye tensor and related GND density for the EBSD map. + Stores result as `Nye_tensor` and `GND`. Uses the crystal + symmetry of the primary phase. + + """ + syms = self.primary_phase.crystal_structure.symmetries + num_syms = len(syms) + + # array to store quat components of initial and symmetric equivalents + quat_comps = np.empty((num_syms, 4) + self.shape) + + # populate with initial quat components + for i, row in enumerate(self.data.orientation): + for j, quat in enumerate(row): + quat_comps[0, :, i, j] = quat.quat_coef + + # loop of over symmetries and apply to initial quat components + # (excluding first symmetry as this is the identity transformation) + for i, sym in enumerate(syms[1:], start=1): + # sym[i] * quat for all points (* is quaternion product) + quat_comps[i, 0] = (quat_comps[0, 0] * sym[0] - quat_comps[0, 1] * sym[1] - + quat_comps[0, 2] * sym[2] - quat_comps[0, 3] * sym[3]) + quat_comps[i, 1] = (quat_comps[0, 0] * sym[1] + quat_comps[0, 1] * sym[0] - + quat_comps[0, 2] * sym[3] + quat_comps[0, 3] * sym[2]) + quat_comps[i, 2] = (quat_comps[0, 0] * sym[2] + quat_comps[0, 2] * sym[0] - + quat_comps[0, 3] * sym[1] + quat_comps[0, 1] * sym[3]) + quat_comps[i, 3] = (quat_comps[0, 0] * sym[3] + quat_comps[0, 3] * sym[0] - + quat_comps[0, 1] * sym[2] + quat_comps[0, 2] * sym[1]) + + # swap into positive hemisphere if required + quat_comps[i, :, quat_comps[i, 0] < 0] *= -1 + + # Arrays to store neighbour misorientation in positive x and y direction + mis_ori_x = np.zeros((num_syms,) + self.shape) + mis_ori_y = np.zeros((num_syms, ) + self.shape) + + # loop over symmetries calculating misorientation to initial + for i in range(num_syms): + for j in range(self.x_dim - 1): + mis_ori_x[i, :, j] = abs(np.einsum("ij,ij->j", quat_comps[0, :, :, j], quat_comps[i, :, :, j + 1])) + + for j in range(self.y_dim - 1): + mis_ori_y[i, j, :] = abs(np.einsum("ij,ij->j", quat_comps[0, :, j, :], quat_comps[i, :, j + 1, :])) + + mis_ori_x[mis_ori_x > 1] = 1 + mis_ori_y[mis_ori_y > 1] = 1 + + # find min misorientation (max here as misorientaion is cos of this) + arg_mis_ori_x = np.argmax(mis_ori_x, axis=0) + arg_mis_ori_y = np.argmax(mis_ori_y, axis=0) + mis_ori_x = np.max(mis_ori_x, axis=0) + mis_ori_y = np.max(mis_ori_y, axis=0) + + # convert to misorientation in degrees + mis_ori_x = 360 * np.arccos(mis_ori_x) / np.pi + mis_ori_y = 360 * np.arccos(mis_ori_y) / np.pi + + # calculate relative elastic distortion tensors at each point in the two directions + betaderx = np.zeros((3, 3) + self.shape) + betadery = betaderx + for i in range(self.x_dim - 1): + for j in range(self.y_dim - 1): + q0x = Quat(quat_comps[0, 0, j, i], quat_comps[0, 1, j, i], + quat_comps[0, 2, j, i], quat_comps[0, 3, j, i]) + qix = Quat(quat_comps[arg_mis_ori_x[j, i], 0, j, i + 1], + quat_comps[arg_mis_ori_x[j, i], 1, j, i + 1], + quat_comps[arg_mis_ori_x[j, i], 2, j, i + 1], + quat_comps[arg_mis_ori_x[j, i], 3, j, i + 1]) + misoquatx = qix.conjugate * q0x + # change stepsize to meters + betaderx[:, :, j, i] = (Quat.rot_matrix(misoquatx) - np.eye(3)) / self.step_size / 1e-6 + q0y = Quat(quat_comps[0, 0, j, i], quat_comps[0, 1, j, i], + quat_comps[0, 2, j, i], quat_comps[0, 3, j, i]) + qiy = Quat(quat_comps[arg_mis_ori_y[j, i], 0, j + 1, i], + quat_comps[arg_mis_ori_y[j, i], 1, j + 1, i], + quat_comps[arg_mis_ori_y[j, i], 2, j + 1, i], + quat_comps[arg_mis_ori_y[j, i], 3, j + 1, i]) + misoquaty = qiy.conjugate * q0y + # change stepsize to meters + betadery[:, :, j, i] = (Quat.rot_matrix(misoquaty) - np.eye(3)) / self.step_size / 1e-6 + + # Calculate the Nye Tensor + alpha = np.empty((3, 3) + self.shape) + bavg = 1.4e-10 # Burgers vector + alpha[0, 2] = (betadery[0, 0] - betaderx[0, 1]) / bavg + alpha[1, 2] = (betadery[1, 0] - betaderx[1, 1]) / bavg + alpha[2, 2] = (betadery[2, 0] - betaderx[2, 1]) / bavg + alpha[:, 1] = betaderx[:, 2] / bavg + alpha[:, 0] = -1 * betadery[:, 2] / bavg + + # Calculate 3 possible L1 norms of Nye tensor for total + # disloction density + alpha_total3 = np.empty(self.shape) + alpha_total5 = np.empty(self.shape) + alpha_total9 = np.empty(self.shape) + alpha_total3 = 30 / 10. * ( + abs(alpha[0, 2]) + abs(alpha[1, 2]) + abs(alpha[2, 2]) + ) + alpha_total5 = 30 / 14. * ( + abs(alpha[0, 2]) + abs(alpha[1, 2]) + abs(alpha[2, 2]) + + abs(alpha[1, 0]) + abs(alpha[0, 1]) + ) + alpha_total9 = 30 / 20. * ( + abs(alpha[0, 2]) + abs(alpha[1, 2]) + abs(alpha[2, 2]) + + abs(alpha[0, 0]) + abs(alpha[1, 0]) + abs(alpha[2, 0]) + + abs(alpha[0, 1]) + abs(alpha[1, 1]) + abs(alpha[2, 1]) + ) + alpha_total3[abs(alpha_total3) < 1] = 1e12 + alpha_total5[abs(alpha_total3) < 1] = 1e12 + alpha_total9[abs(alpha_total3) < 1] = 1e12 + + # choose from the different alpha_totals according to preference; + # see Ruggles GND density paper + + yield 1. + return alpha_total9, alpha + + @report_progress("building quaternion array") + def calc_quat_array(self): + """Build quaternion array + + """ + # create the array of quat objects + quats = Quat.create_many_quats(self.data.euler_angle) + + yield 1. + return quats + + def filter_data(self, misori_tol=5): + # Kuwahara filter + print("8 quadrants") + misori_tol *= np.pi / 180 + misori_tol = np.cos(misori_tol / 2) + + # store quat components in array + quat_comps = np.empty((4,) + self.shape) + for idx in np.ndindex(self.shape): + quat_comps[(slice(None),) + idx] = self.data.orientation[idx].quat_coef + + # misorientation in each quadrant surrounding a point + mis_oris = np.zeros((8,) + self.shape) + + for i in range(2, self.shape[0] - 2): + for j in range(2, self.shape[1] - 2): + + ref_quat = quat_comps[:, i, j] + quadrants = [ + quat_comps[:, i - 2:i + 1, j - 2:j + 1], # UL + quat_comps[:, i - 2:i + 1, j - 1:j + 2], # UC + quat_comps[:, i - 2:i + 1, j:j + 3], # UR + quat_comps[:, i - 1:i + 2, j:j + 3], # MR + quat_comps[:, i:i + 3, j:j + 3], # LR + quat_comps[:, i:i + 3, j - 1:j + 2], # LC + quat_comps[:, i:i + 3, j - 2:j + 1], # LL + quat_comps[:, i - 1:i + 2, j - 2:j + 1] # ML + ] + + for k, quats in enumerate(quadrants): + mis_oris_quad = np.abs( + np.einsum("ijk,i->jk", quats, ref_quat) + ) + mis_oris_quad = mis_oris_quad[mis_oris_quad > misori_tol] + mis_oris[k, i, j] = mis_oris_quad.mean() + + min_mis_ori_quadrant = np.argmax(mis_oris, axis=0) + # minMisOris = np.max(mis_oris, axis=0) + # minMisOris[minMisOris > 1.] = 1. + # minMisOris = 2 * np.arccos(minMisOris) + + quat_comps_new = np.copy(quat_comps) + + for i in range(2, self.shape[0] - 2): + for j in range(2, self.shape[1] - 2): + # if minMisOris[i, j] < misOriTol: + # continue + + ref_quat = quat_comps[:, i, j] + quadrants = [ + quat_comps[:, i - 2:i + 1, j - 2:j + 1], # UL + quat_comps[:, i - 2:i + 1, j - 1:j + 2], # UC + quat_comps[:, i - 2:i + 1, j:j + 3], # UR + quat_comps[:, i - 1:i + 2, j:j + 3], # MR + quat_comps[:, i:i + 3, j:j + 3], # LR + quat_comps[:, i:i + 3, j - 1:j + 2], # LC + quat_comps[:, i:i + 3, j - 2:j + 1], # LL + quat_comps[:, i - 1:i + 2, j - 2:j + 1] # ML + ] + quats = quadrants[min_mis_ori_quadrant[i, j]] + + mis_oris_quad = np.abs( + np.einsum("ijk,i->jk", quats, ref_quat) + ) + quats = quats[:, mis_oris_quad > misori_tol] + + avOri = np.einsum("ij->i", quats) + # avOri /= np.sqrt(np.dot(avOri, avOri)) + + quat_comps_new[:, i, j] = avOri + + quat_comps_new /= np.sqrt(np.einsum("ijk,ijk->jk", quat_comps_new, quat_comps_new)) + + quat_array_new = np.empty(self.shape, dtype=Quat) + + for idx in np.ndindex(self.shape): + quat_array_new[idx] = Quat(quat_comps_new[(slice(None),) + idx]) + + self.data.orientation = quat_array_new + + return quats + + @report_progress("finding grain boundaries") + def find_boundaries(self, misori_tol=10): + """Find grain and phase boundaries + + Parameters + ---------- + misori_tol : float + Critical misorientation in degrees. + + """ + # TODO: what happens with non-indexed points + # TODO: grain boundaries should be calculated per crystal structure + misori_tol *= np.pi / 180 + syms = self.primary_phase.crystal_structure.symmetries + num_syms = len(syms) + + # array to store quat components of initial and symmetric equivalents + quat_comps = np.empty((num_syms, 4) + self.shape) + + # populate with initial quat components + for i, row in enumerate(self.data.orientation): + for j, quat in enumerate(row): + quat_comps[0, :, i, j] = quat.quat_coef + + # loop of over symmetries and apply to initial quat components + # (excluding first symmetry as this is the identity transformation) + for i, sym in enumerate(syms[1:], start=1): + # sym[i] * quat for all points (* is quaternion product) + quat_comps[i, 0] = ( + quat_comps[0, 0]*sym[0] - quat_comps[0, 1]*sym[1] - + quat_comps[0, 2]*sym[2] - quat_comps[0, 3]*sym[3] + ) + quat_comps[i, 1] = ( + quat_comps[0, 0]*sym[1] + quat_comps[0, 1]*sym[0] - + quat_comps[0, 2]*sym[3] + quat_comps[0, 3]*sym[2] + ) + quat_comps[i, 2] = ( + quat_comps[0, 0]*sym[2] + quat_comps[0, 2]*sym[0] - + quat_comps[0, 3]*sym[1] + quat_comps[0, 1]*sym[3] + ) + quat_comps[i, 3] = ( + quat_comps[0, 0]*sym[3] + quat_comps[0, 3]*sym[0] - + quat_comps[0, 1]*sym[2] + quat_comps[0, 2]*sym[1] + ) + # swap into positive hemisphere if required + quat_comps[i, :, quat_comps[i, 0] < 0] *= -1 + + # Arrays to store neighbour misorientation in positive x and y + # directions + misori_x = np.ones((num_syms, ) + self.shape) + misori_y = np.ones((num_syms, ) + self.shape) + + # loop over symmetries calculating misorientation to initial + for i in range(num_syms): + for j in range(self.shape[1] - 1): + misori_x[i, :, j] = abs(np.einsum( + "ij,ij->j", quat_comps[0, :, :, j], quat_comps[i, :, :, j+1] + )) + + for j in range(self.shape[0] - 1): + misori_y[i, j] = abs(np.einsum( + "ij,ij->j", quat_comps[0, :, j], quat_comps[i, :, j+1] + )) + + misori_x[misori_x > 1] = 1 + misori_y[misori_y > 1] = 1 + + # find max dot product and then convert to misorientation angle + misori_x = 2 * np.arccos(np.max(misori_x, axis=0)) + misori_y = 2 * np.arccos(np.max(misori_y, axis=0)) + + # PHASE boundary POINTS + phase_im = self.data.phase + pb_im_x = np.not_equal(phase_im, np.roll(phase_im, -1, axis=1)) + pb_im_x[:, -1] = False + pb_im_y = np.not_equal(phase_im, np.roll(phase_im, -1, axis=0)) + pb_im_y[-1] = False + + phase_boundaries = BoundarySet.from_image(self, pb_im_x, pb_im_y) + grain_boundaries = BoundarySet.from_image( + self, + (misori_x > misori_tol) | pb_im_x, + (misori_y > misori_tol) | pb_im_y + ) + + yield 1. + return phase_boundaries, grain_boundaries + + @report_progress("constructing neighbour network") + def build_neighbour_network(self): + # create network + nn = nx.Graph() + nn.add_nodes_from(self.grains) + + points_x = self.data.grain_boundaries.points_x + points_y = self.data.grain_boundaries.points_y + total_points_x = len(points_x) + total_points = total_points_x + len(points_y) + + for i, points in enumerate((points_x, points_y)): + for i_point, (x, y) in enumerate(points): + # report progress + yield (i_point + i * total_points_x) / total_points + + if (x == 0 or y == 0 or x == self.shape[1] - 1 or + y == self.shape[0] - 1): + # exclude boundary pixels of map + continue + + grain_id = self.data.grains[y, x] - 1 + nei_grain_id = self.data.grains[y + i, x - i + 1] - 1 + if nei_grain_id == grain_id: + # ignore if neighbour is same as grain + continue + if nei_grain_id < 0 or grain_id < 0: + # ignore if not a grain (boundary points -1 and + # points in small grains -2) + continue + + grain = self[grain_id] + nei_grain = self[nei_grain_id] + try: + # look up boundary segment if it exists + b_seg = nn[grain][nei_grain]['boundary'] + except KeyError: + # neighbour relation doesn't exist so add it + b_seg = BoundarySegment(self, grain, nei_grain) + nn.add_edge(grain, nei_grain, boundary=b_seg) + + # add the boundary point + b_seg.addBoundaryPoint((x, y), i, grain) + + self.neighbour_network = nn + + def plot_phase_boundary_map(self, dilate=False, **kwargs): + """Plot phase boundary map. + + Parameters + ---------- + dilate : bool + If true, dilate boundary. + kwargs + All other arguments are passed to :func:`defdap.plotting.MapPlot.create`. + + Returns + ------- + defdap.plotting.MapPlot + + """ + # Set default plot parameters then update with any input + plot_params = { + 'vmax': 1, + 'plot_colour_bar': True, + 'cmap': 'gray' + } + plot_params.update(kwargs) + + boundaries_image = self.data.phase_boundaries.image.astype(int) + if dilate: + boundaries_image = mph.binary_dilation(boundaries_image) + + plot = MapPlot.create(self, boundaries_image, **plot_params) + + return plot + + def plot_boundary_map(self, **kwargs): + """Plot grain boundary map. + + Parameters + ---------- + kwargs + All arguments are passed to :func:`defdap.plotting.MapPlot.create`. + + Returns + ------- + defdap.plotting.MapPlot + + """ + # Set default plot parameters then update with any input + plot_params = { + 'plot_gbs': True, + 'boundaryColour': 'black' + } + plot_params.update(kwargs) + + plot = MapPlot.create(self, None, **plot_params) + + return plot + + @report_progress("finding grains") + def find_grains(self, min_grain_size=10): + """Find grains and assign IDs. + + Parameters + ---------- + min_grain_size : int + Minimum grain area in pixels. + + """ + # Initialise the grain map + # TODO: Look at grain map compared to boundary map + grains = np.zeros(self.shape, dtype=int) + grain_list = [] + + boundary_im_x = self.data.grain_boundaries.image_x + boundary_im_y = self.data.grain_boundaries.image_y + + # List of points where no grain has be set yet + points_left = self.data.phase != 0 + total_points = points_left.sum() + found_point = 0 + next_point = points_left.tobytes().find(b'\x01') + + # Start counter for grains + grain_index = 1 + group_id = Datastore.generate_id() + + # Loop until all points (except boundaries) have been assigned + # to a grain or ignored + i = 0 + 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, 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 + # values in grain map to -2 + for point in grain.data.point: + grains[point[1], point[0]] = -2 + else: + # add grain to list and increment grain index + grain_list.append(grain) + grain_index += 1 + + # find next search point + points_left_sub = points_left.reshape(-1)[next_point + 1:] + found_point = points_left_sub.tobytes().find(b'\x01') + next_point += found_point + 1 + + # report progress + i += 1 + if i == defaults['find_grain_report_freq']: + yield 1. - points_left_sub.sum() / total_points + i = 0 + + # Assign phase to each grain + for grain in grain_list: + phase_vals = grain.grain_data(self.data.phase) + if np.max(phase_vals) != np.min(phase_vals): + warn(f"Grain {grain.grain_id} could not be assigned a " + f"phase, phase vals not constant.") + continue + phase_id = phase_vals[0] - 1 + if not (0 <= phase_id < self.num_phases): + warn(f"Grain {grain.grain_id} could not be assigned a " + f"phase, invalid phase {phase_id}.") + continue + grain.phase_id = phase_id + grain.phase = self.phases[phase_id] + + ## TODO: this will get duplicated if find grains called again + self.data.add_derivative( + grain_list[0].data, self.grain_data_to_map, pass_ref=True, + in_props={ + 'type': 'list' + }, + out_props={ + 'type': 'map' + } + ) + + self._grains = grain_list + return grains + + def plot_grain_map(self, **kwargs): + """Plot a map with grains coloured. + + Parameters + ---------- + kwargs + All arguments are passed to :func:`defdap.plotting.MapPlot.create`. + + Returns + ------- + defdap.plotting.MapPlot + + """ + # Set default plot parameters then update with any input + plot_params = { + 'clabel': "Grain number" + } + plot_params.update(kwargs) + + plot = MapPlot.create(self, self.data.grains, **plot_params) + + return plot + + @report_progress("calculating grain mean orientations") + def calc_grain_av_oris(self): + """Calculate the average orientation of grains. + + """ + numGrains = len(self) + for iGrain, grain in enumerate(self): + grain.calc_average_ori() + + # report progress + yield (iGrain + 1) / numGrains + + @report_progress("calculating grain misorientations") + def calc_grain_mis_ori(self, calc_axis=False): + """Calculate the misorientation within grains. + + Parameters + ---------- + calc_axis : bool + Calculate the misorientation axis if True. + + """ + num_grains = len(self) + for i_grain, grain in enumerate(self): + grain.build_mis_ori_list(calc_axis=calc_axis) + + # report progress + yield (i_grain + 1) / num_grains + + def plot_mis_ori_map(self, component=0, **kwargs): + """Plot misorientation map. + + Parameters + ---------- + component : int, {0, 1, 2, 3} + 0 gives misorientation, 1, 2, 3 gives rotation about x, y, z + kwargs + All other arguments are passed to :func:`defdap.plotting.MapPlot.create`. + + Returns + ------- + defdap.plotting.MapPlot + + """ + if component in [1, 2, 3]: + self.mis_ori = np.zeros(self.shape) + # Calculate misorientation axis if not calculated + if np.any([grain.mis_ori_axis_list is None for grain in self]): + self.calc_grain_mis_ori(calc_axis=True) + for grain in self: + for point, mis_ori_axis in zip(grain.data.point, np.array(grain.mis_ori_axis_list)): + self.mis_ori[point[1], point[0]] = mis_ori_axis[component - 1] + + mis_ori = self.mis_ori * 180 / np.pi + clabel = r"Rotation around {:} axis ($^\circ$)".format( + ['X', 'Y', 'Z'][component-1] + ) + else: + self.mis_ori = np.ones(self.shape) + # Calculate misorientation if not calculated + if np.any([grain.mis_ori_list is None for grain in self]): + self.calc_grain_mis_ori(calc_axis=False) + for grain in self: + for point, mis_ori in zip(grain.data.point, grain.mis_ori_list): + self.mis_ori[point[1], point[0]] = mis_ori + + mis_ori = np.arccos(self.mis_ori) * 360 / np.pi + clabel = r"Grain reference orienation deviation (GROD) ($^\circ$)" + + # Set default plot parameters then update with any input + plot_params = { + 'plot_colour_bar': True, + 'clabel': clabel + } + plot_params.update(kwargs) + + plot = MapPlot.create(self, mis_ori, **plot_params) + + return plot + + @report_progress("calculating grain average Schmid factors") + def calc_average_grain_schmid_factors(self, load_vector, slip_systems=None): + """ + Calculates Schmid factors for all slip systems, for all grains, + based on average grain orientation. + + Parameters + ---------- + load_vector : + Loading vector, e.g. [1, 0, 0]. + slip_systems : list, optional + Slip planes to calculate Schmid factor for, maximum of all + planes calculated if not given. + + """ + num_grains = len(self) + for iGrain, grain in enumerate(self): + grain.calc_average_schmid_factors(load_vector, slip_systems=slip_systems) + + # report progress + yield (iGrain + 1) / num_grains + + @report_progress("calculating RDR values") + def calc_rdr(self): + """Calculates Relative Displacent Ratio values for all grains""" + num_grains = len(self) + for iGrain, grain in enumerate(self): + grain.calc_rdr() + + # report progress + yield (iGrain + 1) / num_grains + + def plot_average_grain_schmid_factors_map(self, planes=None, directions=None, + **kwargs): + """ + Plot maximum Schmid factor map, based on average grain + orientation (for all slip systems unless specified). + + Parameters + ---------- + planes : list, optional + Plane ID(s) to consider. All planes considered if not given. + directions : list, optional + Direction ID(s) to consider. All directions considered if not given. + kwargs + All other arguments are passed to :func:`defdap.plotting.MapPlot.create`. + + Returns + ------- + defdap.plotting.MapPlot + + """ + # Set default plot parameters then update with any input + plot_params = { + 'vmin': 0, + 'vmax': 0.5, + 'cmap': 'gray', + 'plot_colour_bar': True, + 'clabel': "Schmid factor" + } + plot_params.update(kwargs) + + if self[0].average_schmid_factors is None: + raise Exception("Run 'calc_average_grain_schmid_factors' first") + + grains_sf_max = [] + for grain in self: + current_sf = [] + + if planes is not None: + for plane in planes: + if directions is not None: + for direction in directions: + current_sf.append( + grain.average_schmid_factors[plane][direction] + ) + else: + current_sf += grain.average_schmid_factors[plane] + else: + for sf_group in grain.average_schmid_factors: + current_sf += sf_group + + grains_sf_max.append(max(current_sf)) + + plot = self.plot_grain_data_map(grain_data=grains_sf_max, bg=0.5, + **plot_params) + + return plot + + +class Grain(base.Grain): + """ + Class to encapsulate a grain in an EBSD map and useful analysis and + plotting methods. + + Attributes + ---------- + ebsd_map : defdap.ebsd.Map + EBSD map this grain is a member of. + owner_map : defdap.ebsd.Map + EBSD map this grain is a member of. + phase_id : int + + phase : defdap.crystal.Phase + + data : defdap.utils.Datastore + Must contain after creating: + point : list of tuples + (x, y) + Generated data: + GROD : numpy.ndarray + + GROD_axis : numpy.ndarray + + Derived data: + Map data to list data from the map the grain is part of + + + mis_ori_list : list + MisOri at each point in grain. + mis_ori_axis_list : list + MisOri axes at each point in grain. + ref_ori : defdap.quat.Quat + Average ori of grain + average_mis_ori + Average mis_ori of grain. + average_schmid_factors : list + List of list Schmid factors (grouped by slip plane). + slip_trace_angles : list + Slip trace angles in screen plane. + slip_trace_inclinations : list + Angle between slip plane and screen plane. + + """ + def __init__(self, grain_id, ebsdMap, group_id): + # Call base class constructor + super(Grain, self).__init__(grain_id, ebsdMap, group_id) + + self.ebsd_map = self.owner_map # ebsd map this grain is a member of + self.mis_ori_list = None # list of mis_ori at each point in grain + self.mis_ori_axis_list = None # list of mis_ori axes at each point in grain + self.ref_ori = None # (quat) average ori of grain + self.average_mis_ori = None # average mis_ori of grain + + self.average_schmid_factors = None # list of list Schmid factors (grouped by slip plane) + self.slip_trace_angles = None # list of slip trace angles + self.slip_trace_inclinations = None + + self.plot_default = self.plot_unit_cell + + self.data.add_generator( + ('GROD', 'GROD_axis'), self.calc_grod, + type='list', + metadatas=({ + 'unit': 'rad', + 'order': 0, + 'plot_params': { + 'plot_colour_bar': True, + 'clabel': 'GROD', + } + }, { + 'unit': '', + 'order': 1, + 'default_component': 0, + 'plot_params': { + 'plot_colour_bar': True, + 'clabel': 'GROD axis', + } + }) + ) + + @property + def crystal_sym(self): + """Temporary""" + return self.phase.crystal_structure.name + + def calc_average_ori(self): + """Calculate the average orientation of a grain. + + """ + quat_comps_sym = Quat.calc_sym_eqvs(self.data.orientation, self.crystal_sym) + + self.ref_ori = Quat.calc_average_ori(quat_comps_sym) + + def build_mis_ori_list(self, calc_axis=False): + """Calculate the misorientation within given grain. + + Parameters + ---------- + calc_axis : bool + Calculate the misorientation axis if True. + + """ + quat_comps_sym = Quat.calc_sym_eqvs(self.data.orientation, self.crystal_sym) + + if self.ref_ori is None: + self.ref_ori = Quat.calc_average_ori(quat_comps_sym) + + mis_ori_array, min_quat_comps = Quat.calcMisOri(quat_comps_sym, self.ref_ori) + + self.average_mis_ori = mis_ori_array.mean() + self.mis_ori_list = list(mis_ori_array) + + if calc_axis: + # Now for axis calculation + ref_ori_inv = self.ref_ori.conjugate + + mis_ori_axis = np.empty((3, min_quat_comps.shape[1])) + dq = np.empty((4, min_quat_comps.shape[1])) + + # ref_ori_inv * minQuat for all points (* is quaternion product) + # change to minQuat * ref_ori_inv + dq[0, :] = (ref_ori_inv[0] * min_quat_comps[0, :] - ref_ori_inv[1] * min_quat_comps[1, :] - + ref_ori_inv[2] * min_quat_comps[2, :] - ref_ori_inv[3] * min_quat_comps[3, :]) + + dq[1, :] = (ref_ori_inv[1] * min_quat_comps[0, :] + ref_ori_inv[0] * min_quat_comps[1, :] + + ref_ori_inv[3] * min_quat_comps[2, :] - ref_ori_inv[2] * min_quat_comps[3, :]) + + dq[2, :] = (ref_ori_inv[2] * min_quat_comps[0, :] + ref_ori_inv[0] * min_quat_comps[2, :] + + ref_ori_inv[1] * min_quat_comps[3, :] - ref_ori_inv[3] * min_quat_comps[1, :]) + + dq[3, :] = (ref_ori_inv[3] * min_quat_comps[0, :] + ref_ori_inv[0] * min_quat_comps[3, :] + + ref_ori_inv[2] * min_quat_comps[1, :] - ref_ori_inv[1] * min_quat_comps[2, :]) + + dq[:, dq[0] < 0] = -dq[:, dq[0] < 0] + + # numpy broadcasting taking care of different array sizes + mis_ori_axis[:, :] = (2 * dq[1:4, :] * np.arccos(dq[0, :])) / np.sqrt(1 - np.power(dq[0, :], 2)) + + # hack it back into a list. Need to change self.*List to be arrays, it was a bad decision to + # make them lists in the beginning + self.mis_ori_axis_list = [] + for row in mis_ori_axis.transpose(): + self.mis_ori_axis_list.append(row) + + def calc_grod(self): + quat_comps = Quat.calc_sym_eqvs(self.data.orientation, self.crystal_sym) + + if self.ref_ori is None: + self.ref_ori = Quat.calc_average_ori(quat_comps) + + misori, quat_comps = Quat.calcMisOri(quat_comps, self.ref_ori) + misori = 2 * np.arccos(misori) + + ref_ori_inv = self.ref_ori.conjugate + dq = np.empty((4, len(self))) + # ref_ori_inv * quat_comps for all points + # change to quat_comps * ref_ori_inv + dq[0] = (ref_ori_inv[0]*quat_comps[0] - ref_ori_inv[1]*quat_comps[1] - + ref_ori_inv[2]*quat_comps[2] - ref_ori_inv[3]*quat_comps[3]) + dq[1] = (ref_ori_inv[1]*quat_comps[0] + ref_ori_inv[0]*quat_comps[1] + + ref_ori_inv[3]*quat_comps[2] - ref_ori_inv[2]*quat_comps[3]) + dq[2] = (ref_ori_inv[2]*quat_comps[0] + ref_ori_inv[0]*quat_comps[2] + + ref_ori_inv[1]*quat_comps[3] - ref_ori_inv[3]*quat_comps[1]) + dq[3] = (ref_ori_inv[3]*quat_comps[0] + ref_ori_inv[0]*quat_comps[3] + + ref_ori_inv[2]*quat_comps[1] - ref_ori_inv[1]*quat_comps[2]) + dq[:, dq[0] < 0] *= -1 + misori_axis = (2 * dq[1:4] * np.arccos(dq[0])) / np.sqrt(1 - dq[0]**2) + + return misori, misori_axis + + def plot_ref_ori(self, direction=np.array([0, 0, 1]), **kwargs): + """Plot the average grain orientation on an IPF. + + Parameters + ---------- + direction : numpy.ndarray + Sample direction for IPF. + kwargs + All other arguments are passed to :func:`defdap.quat.Quat.plot_ipf`. + + Returns + ------- + defdap.plotting.PolePlot + + """ + plot_params = {'marker': '+'} + plot_params.update(kwargs) + return Quat.plot_ipf([self.ref_ori], direction, self.crystal_sym, + **plot_params) + + def plot_ori_spread(self, direction=np.array([0, 0, 1]), **kwargs): + """Plot all orientations within a given grain, on an IPF. + + Parameters + ---------- + direction : numpy.ndarray + Sample direction for IPF. + kwargs + All other arguments are passed to :func:`defdap.quat.Quat.plot_ipf`. + + Returns + ------- + defdap.plotting.PolePlot + + """ + plot_params = {'marker': '.'} + plot_params.update(kwargs) + return Quat.plot_ipf(self.data.orientation, direction, self.crystal_sym, + **plot_params) + + def plot_unit_cell(self, fig=None, ax=None, plot=None, **kwargs): + """Plot an unit cell of the average grain orientation. + + Parameters + ---------- + fig : matplotlib.figure.Figure + Matplotlib figure to plot on + ax : matplotlib.figure.Figure + Matplotlib figure to plot on + plot : defdap.plotting.PolePlot + defdap plot to plot the figure to. + kwargs + All other arguments are passed to :func:`defdap.quat.Quat.plot_unit_cell`. + + """ + crystal_structure = self.ebsd_map.phases[self.phase_id].crystal_structure + plot = Quat.plot_unit_cell(self.ref_ori, fig=fig, ax=ax, plot=plot, + crystal_structure=crystal_structure, **kwargs) + + return plot + + def plot_mis_ori(self, component=0, **kwargs): + """Plot misorientation map for a given grain. + + Parameters + ---------- + component : int, {0, 1, 2, 3} + 0 gives misorientation, 1, 2, 3 gives rotation about x, y, z. + kwargs + All other arguments are passed to :func:`defdap.ebsd.plot_grain_data`. + + Returns + ------- + defdap.plotting.GrainPlot + + """ + component = int(component) + + # Set default plot parameters then update with any input + plot_params = { + 'plot_colour_bar': True + } + if component == 0: + if self.mis_ori_list is None: self.build_mis_ori_list() + plot_params['clabel'] = r"Grain reference orientation " \ + r"deviation (GROD) ($^\circ$)" + plot_data = np.rad2deg(2 * np.arccos(self.mis_ori_list)) + + elif 0 < component < 4: + if self.mis_ori_axis_list is None: self.build_mis_ori_list(calc_axis=True) + plot_params['clabel'] = r"Rotation around {:} ($^\circ$)".format( + ['X', 'Y', 'Z'][component-1] + ) + plot_data = np.rad2deg(np.array(self.mis_ori_axis_list)[:, component - 1]) + + else: + raise ValueError("Component must between 0 and 3") + plot_params.update(kwargs) + + plot = self.plot_grain_data(grain_data=plot_data, **plot_params) + + return plot + + # define load axis as unit vector + def calc_average_schmid_factors(self, load_vector, slip_systems=None): + """Calculate Schmid factors for grain, using average orientation. + + Parameters + ---------- + load_vector : numpy.ndarray + Loading vector, i.e. [1, 0, 0] + slip_systems : list, optional + Slip planes to calculate Schmid factor for. Maximum for all planes + used if not set. + + """ + if slip_systems is None: + slip_systems = self.phase.slip_systems + if self.ref_ori is None: + self.calc_average_ori() + + # orientation of grain + grain_av_ori = self.ref_ori + + # Transform the load vector into crystal coordinates + load_vector_crystal = grain_av_ori.transform_vector(load_vector) + + self.average_schmid_factors = [] + # flatten list of lists + # slip_systems = chain.from_iterable(slip_systems) + + # Loop over groups of slip systems with same slip plane + for i, slip_system_group in enumerate(slip_systems): + self.average_schmid_factors.append([]) + # Then loop over individual slip systems + for slip_system in slip_system_group: + schmidFactor = abs(np.dot(load_vector_crystal, slip_system.slip_plane) * + np.dot(load_vector_crystal, slip_system.slip_dir)) + self.average_schmid_factors[i].append(schmidFactor) + + return + + def calc_rdr(self): + """Calculate Relative Displacement Ratio values.""" + self.rdr = [] + + # Loop over groups of slip systems with same slip plane + for i, slip_system_group in enumerate(self.phase.slip_systems): + self.rdr.append([]) + # Then loop over individual slip systems + for slip_system in slip_system_group: + slip_dir_sample = self.ref_ori.conjugate.transform_vector(slip_system.slip_dir) + self.rdr[i].append(-slip_dir_sample[0] / slip_dir_sample[1]) + + @property + def slip_traces(self): + """Returns list of slip trace angles. + + Returns + ------- + list + Slip trace angles based on grain orientation in calc_slip_traces. + + """ + if self.slip_trace_angles is None: + self.calc_slip_traces() + + return self.slip_trace_angles + + def print_slip_traces(self): + """Print a list of slip planes (with colours) and slip directions + + """ + self.calc_slip_traces() + + if self.average_schmid_factors is None: + raise Exception("Run 'calc_average_grain_schmid_factors' on the EBSD map first") + + for ss_group, colour, sf_group, slip_trace in zip( + self.phase.slip_systems, + self.phase.slip_trace_colours, + self.average_schmid_factors, + self.slip_traces + ): + print('{0}\tColour: {1}\tAngle: {2:.2f}'.format(ss_group[0].slip_plane_label, colour, slip_trace * 180 / np.pi)) + for ss, sf in zip(ss_group, sf_group): + print(' {0} SF: {1:.3f}'.format(ss.slip_dir_label, sf)) + + def calc_slip_traces(self, slip_systems=None): + """Calculates list of slip trace angles based on grain orientation. + + Parameters + ------- + slip_systems : defdap.crystal.SlipSystem, optional + + """ + if slip_systems is None: + slip_systems = self.phase.slip_systems + if self.ref_ori is None: + self.calc_average_ori() + + screen_plane_norm = np.array((0, 0, 1)) # in sample orientation frame + + grain_av_ori = self.ref_ori # orientation of grain + + screen_plane_norm_crystal = grain_av_ori.transform_vector(screen_plane_norm) + + self.slip_trace_angles = [] + self.slip_trace_inclinations = [] + # Loop over each group of slip systems + for slip_system_group in slip_systems: + # Take slip plane from first in group + slip_plane_norm = slip_system_group[0].slip_plane + # planeLabel = slip_system_group[0].slip_plane_label + + # Calculate intersection of slip plane with plane of screen + intersection_crystal = np.cross(screen_plane_norm_crystal, slip_plane_norm) + + # Calculate angle between slip plane and screen plane + inclination = np.arccos(np.dot(screen_plane_norm_crystal, slip_plane_norm)) + if inclination > np.pi / 2: + inclination = np.pi - inclination + # print("{} inclination: {:.1f}".format(planeLabel, inclination * 180 / np.pi)) + + # Transform intersection back into sample coordinates and normalise + intersection = grain_av_ori.conjugate.transform_vector(intersection_crystal) + intersection = intersection / np.sqrt(np.dot(intersection, intersection)) + + # Calculate trace angle. Starting vertical and proceeding + # counter clockwise + if intersection[0] > 0: + intersection *= -1 + trace_angle = np.arccos(np.dot(intersection, np.array([0, 1.0, 0]))) + + # Append to list + self.slip_trace_angles.append(trace_angle) + self.slip_trace_inclinations.append(inclination) + + +class BoundarySet(object): + # boundaries : numpy.ndarray + # Map of boundaries. -1 for a boundary, 0 otherwise. + # phaseBoundaries : numpy.ndarray + # Map of phase boundaries. -1 for boundary, 0 otherwise. + def __init__(self, ebsd_map, points_x, points_y): + self.ebsd_map = ebsd_map + self.points_x = set(points_x) + self.points_y = set(points_y) + + @classmethod + def from_image(cls, ebsd_map, image_x, image_y): + return cls( + ebsd_map, + zip(*image_x.transpose().nonzero()), + zip(*image_y.transpose().nonzero()) + ) + + @classmethod + def from_boundary_segments(cls, b_segs): + points_x = [] + points_y = [] + for b_seg in b_segs: + points_x += b_seg.boundary_points_x + points_y += b_seg.boundary_points_y + + return cls(b_segs[0].ebsdMap, points_x, points_y) + + @property + def points(self): + return self.points_x.union(self.points_y) + + def _image(self, points): + image = np.zeros(self.ebsd_map.shape, dtype=bool) + image[tuple(zip(*points))[::-1]] = True + return image + + @property + def image_x(self): + return self._image(self.points_x) + + @property + def image_y(self): + return self._image(self.points_y) + + @property + def image(self): + return self._image(self.points) + + @property + def lines(self): + _, _, lines = self.boundary_points_to_lines( + boundary_points_x=self.points_x, + boundary_points_y=self.points_y + ) + return lines + + @staticmethod + def boundary_points_to_lines(*, boundary_points_x=None, + boundary_points_y=None): + boundary_data = {} + if boundary_points_x is not None: + boundary_data['x'] = boundary_points_x + if boundary_points_y is not None: + boundary_data['y'] = boundary_points_y + if not boundary_data: + raise ValueError("No boundaries provided.") + + deltas = { + 'x': (0.5, -0.5, 0.5, 0.5), + 'y': (-0.5, 0.5, 0.5, 0.5) + } + all_lines = [] + for mode, points in boundary_data.items(): + lines = [] + for i, j in points: + lines.append(( + (i + deltas[mode][0], j + deltas[mode][1]), + (i + deltas[mode][2], j + deltas[mode][3]) + )) + all_lines.append(lines) + + if len(all_lines) == 2: + all_lines.append(all_lines[0] + all_lines[1]) + return tuple(all_lines) + else: + return all_lines[0] + + +class BoundarySegment(object): + def __init__(self, ebsdMap, grain1, grain2): + self.ebsdMap = ebsdMap + + self.grain1 = grain1 + self.grain2 = grain2 + + # list of boundary points (x, y) for horizontal (X) and + # vertical (Y) boundaries + self.boundary_points_x = [] + self.boundary_points_y = [] + # Boolean value for each point above, True if boundary point is + # in grain1 and False if in grain2 + self.boundary_point_owners_x = [] + self.boundary_point_owners_y = [] + + def __eq__(self, right): + if type(self) is not type(right): + raise NotImplementedError() + + return ((self.grain1 is right.grain1 and + self.grain2 is right.grain2) or + (self.grain1 is right.grain2 and + self.grain2 is right.grain1)) + + def __len__(self): + return len(self.boundary_points_x) + len(self.boundary_points_y) + + def addBoundaryPoint(self, point, kind, owner_grain): + if kind == 0: + self.boundary_points_x.append(point) + self.boundary_point_owners_x.append(owner_grain is self.grain1) + elif kind == 1: + self.boundary_points_y.append(point) + self.boundary_point_owners_y.append(owner_grain is self.grain1) + else: + raise ValueError("Boundary point kind is 0 for x and 1 for y") + + def boundary_point_pairs(self, kind): + """Return pairs of points either side of the boundary. The first + point is always in grain1 + """ + if kind == 0: + boundary_points = self.boundary_points_x + boundary_point_owners = self.boundary_point_owners_x + delta = (1, 0) + else: + boundary_points = self.boundary_points_y + boundary_point_owners = self.boundary_point_owners_y + delta = (0, 1) + + boundary_point_pairs = [] + for point, owner in zip(boundary_points, boundary_point_owners): + other_point = (point[0] + delta[0], point[1] + delta[1]) + if owner: + boundary_point_pairs.append((point, other_point)) + else: + boundary_point_pairs.append((other_point, point)) + + return boundary_point_pairs + + @property + def boundary_point_pairs_x(self): + """Return pairs of points either side of the boundary. The first + point is always in grain1 + """ + return self.boundary_point_pairs(0) + + @property + def boundary_point_pairs_y(self): + """Return pairs of points either side of the boundary. The first + point is always in grain1 + """ + return self.boundary_point_pairs(1) + + @property + def boundary_lines(self): + """Return line points along this boundary segment""" + _, _, lines = BoundarySet.boundary_points_to_lines( + boundary_points_x=self.boundary_points_x, + boundary_points_y=self.boundary_points_y + ) + return lines + + def misorientation(self): + mis_ori, minSymm = self.grain1.ref_ori.mis_ori( + self.grain2.ref_ori, self.ebsdMap.crystal_sym, return_quat=2 + ) + mis_ori = 2 * np.arccos(mis_ori) + mis_ori_axis = self.grain1.ref_ori.mis_ori_axis(minSymm) + + # should this be a unit vector already? + mis_ori_axis /= np.sqrt(np.dot(mis_ori_axis, mis_ori_axis)) + + return mis_ori, mis_ori_axis + + # compVector = np.array([1., 1., 1.]) + # deviation = np.arccos( + # np.dot(mis_ori_axis, np.array([1., 1., 1.])) / + # (np.sqrt(np.dot(mis_ori_axis, mis_ori_axis) * np.dot(compVector, + # compVector)))) + # print(deviation * 180 / np.pi) + + +class Linker(object): + """Class for linking multiple EBSD maps of the same region for analysis of deformation. + + Attributes + ---------- + ebsd_maps : list(ebsd.Map) + List of `ebsd.Map` objects that are linked. + links : list(tuple(int)) + List of grain link. Each link is stored as a tuple of + grain IDs (one from each map stored in same order of maps). + plots : list(plotting.MapPlot) + List of last opened plot of each map. + + """ + def __init__(self, ebsd_maps): + """Initialise linker and set ebsd maps + + Parameters + ---------- + ebsd_maps : list(ebsd.Map) + List of `ebsd.Map` objects that are linked. + + """ + self.ebsd_maps = ebsd_maps + self.links = [] + self.plots = None + + def set_origin(self, **kwargs): + """Interactive tool to set origin of each EBSD map. + + Parameters + ---------- + kwargs + Keyword arguments passed to :func:`defdap.ebsd.Map.plot_default` + + """ + self.plots = [] + for ebsd_map in self.ebsd_maps: + plot = ebsd_map.plot_default(make_interactive=True, **kwargs) + plot.add_event_handler('button_press_event', self.click_set_origin) + plot.add_points([ebsd_map.origin[0]], [ebsd_map.origin[1]], + c='w', s=60, marker='x') + self.plots.append(plot) + + def click_set_origin(self, event, plot): + """Event handler for clicking to set origin of map. + + Parameters + ---------- + event + Click event. + plot : defdap.plotting.MapPlot + Plot to capture clicks from. + + """ + # check if click was on the map + if event.inaxes is not plot.ax: + return + + origin = (int(event.xdata), int(event.ydata)) + plot.calling_map.origin = origin + plot.add_points([origin[0]], [origin[1]], update_layer=0) + print(f"Origin set to ({origin[0]}, {origin[1]})") + + def start_linking(self): + """Start interactive grain linking process of each EBSD map. + + """ + self.plots = [] + for ebsd_map in self.ebsd_maps: + plot = ebsd_map.locate_grain(click_event=self.click_grain_guess) + + # Add make link button to axes + plot.add_button('Make link', self.make_link, + color='0.85', hovercolor='0.95') + + self.plots.append(plot) + + def click_grain_guess(self, event, plot): + """Guesses grain position in other maps, given click on one. + + Parameters + ---------- + event + Click handler. + plot : defdap.plotting.Plot + Plot to capture clicks from. + + """ + # check if click was on the map + if event.inaxes is not plot.ax: + return + + curr_ebsd_map = plot.callingMap + + if curr_ebsd_map is self.ebsd_maps[0]: + # clicked on 'master' map so highlight and guess grain on others + + # set current grain in 'master' ebsd map + self.ebsd_maps[0].click_grain_id(event, plot, False) + + # guess at grain in other maps + for ebsd_map, plot in zip(self.ebsd_maps[1:], self.plots[1:]): + # calculated position relative to set origin of the + # map, scaled from step size of maps + x0m = curr_ebsd_map.origin[0] + y0m = curr_ebsd_map.origin[1] + x0 = ebsd_map.origin[0] + y0 = ebsd_map.origin[1] + scaling = curr_ebsd_map.step_size / ebsd_map.step_size + + x = int((event.xdata - x0m) * scaling + x0) + y = int((event.ydata - y0m) * scaling + y0) + + grain_id = int(ebsd_map.data.grains[y, x]) - 1 + grain = self[grain_id] + ebsd_map.sel_grain = grain + print(grain_id) + + # update the grain highlights layer in the plot + plot.add_grain_highlights([grain_id], + alpha=ebsd_map.highlight_alpha) + + else: + # clicked on other map so correct guessed selected grain + curr_ebsd_map.click_grain_id(event, plot, False) + + def make_link(self, event, plot): + """Make a link between the EBSD maps after clicking. + + """ + # create empty list for link + curr_link = [] + + for i, ebsd_map in enumerate(self.ebsd_maps): + if ebsd_map.sel_grain is not None: + curr_link.append(ebsd_map.sel_grain.grain_id) + else: + raise Exception(f"No grain selected in map {i + 1}.") + + curr_link = tuple(curr_link) + if curr_link not in self.links: + self.links.append(curr_link) + print("Link added " + str(curr_link)) + + def reset_links(self): + """Reset links. + + """ + self.links = [] + +# Analysis routines + + def set_ref_ori_from_master(self): + """Loop over each map (not first/reference) and each link. + Sets refOri of linked grains to refOri of grain in first map. + + """ + for i, ebsd_map in enumerate(self.ebsd_maps[1:], start=1): + for link in self.links: + ebsd_map[link[i]].ref_ori = copy.deepcopy( + self.ebsd_maps[0][link[0]].ref_ori + ) + + def update_misori(self, calc_axis=False): + """Recalculate misorientation for linked grain (not for first map) + + Parameters + ---------- + calc_axis : bool + Calculate the misorientation axis if True. + + """ + for i, ebsd_map in enumerate(self.ebsd_maps[1:], start=1): + for link in self.links: + ebsd_map[link[i]].build_mis_ori_list(calc_axis=calc_axis) diff --git a/build/lib/defdap/experiment.py b/build/lib/defdap/experiment.py new file mode 100644 index 0000000..f51b805 --- /dev/null +++ b/build/lib/defdap/experiment.py @@ -0,0 +1,338 @@ +import numpy as np +from skimage import transform as tf +from skimage import morphology as mph + + +class Experiment(object): + def __init__(self): + self.frame_relations = {} + self.increments = [] + + def __getitem__(self, key): + return self.increments[key] + + def add_increment(self, **kwargs): + inc = Increment(self, **kwargs) + self.increments.append(inc) + return inc + + def iter_over_maps(self, map_name): + for i, inc in enumerate(self.increments): + map_obj = inc.maps.get(map_name) + if map_obj is None: + continue + yield i, map_obj + + def link_frames(self, frame_1, frame_2, transform_props): + self.frame_relations[(frame_1, frame_2)] = transform_props + + def get_frame_transform(self, frame_1, frame_2): + transform_lookup = { + 'piecewise_affine': tf.PiecewiseAffineTransform, + 'projective': tf.ProjectiveTransform, + 'polynomial': tf.PolynomialTransform, + 'affine': tf.AffineTransform, + } + + forward = (frame_1, frame_2) in self.frame_relations + reverse = (frame_2, frame_1) in self.frame_relations + if forward and reverse: + raise ValueError('Why are frame relations in both senses stored?') + if not (forward or reverse): + raise ValueError('Frames are not linked.') + + frames = (frame_1, frame_2) if forward else (frame_2, frame_1) + transform_props = self.frame_relations[frames] + calc_inverse = transform_props['type'] == 'polynomial' + transform = transform_lookup[transform_props['type']]() + + if reverse and calc_inverse: + frames = frames[::-1] + + transform.estimate( + np.array(frames[0].homog_points), + np.array(frames[1].homog_points), + **{k: v for k, v in transform_props.items() if k != 'type'} + ) + + if reverse and not calc_inverse: + transform = transform.inverse + + return transform + + def warp_image(self, map_data, frame_1, frame_2, crop=True, **kwargs): + """Warps a map to the DIC frame. + + Parameters + ---------- + map_data : numpy.ndarray + Data to warp. + crop : bool, optional + Crop to size of DIC map if true. + kwargs + All other arguments passed to :func:`skimage.transform.warp`. + + Returns + ---------- + numpy.ndarray + Map (i.e. EBSD map data) warped to the DIC frame. + + """ + transform = self.get_frame_transform(frame_2, frame_1) + + if not crop and isinstance(transform, tf.AffineTransform): + # copy transform and change translation to give an extra + # 5% border to show the entire image after rotation/shearing + input_shape = np.array(map_data.shape) + transform = tf.AffineTransform(matrix=np.copy(transform.params)) + transform.params[0:2, 2] = -0.05 * input_shape + output_shape = input_shape * 1.4 / transform.scale + kwargs['output_shape'] = output_shape.astype(int) + + return tf.warp(map_data, transform, **kwargs) + + def warp_lines(self, lines, frame_1, frame_2): + """Warp a set of lines to the DIC reference frame. + + Parameters + ---------- + lines : list of tuples + Lines to warp. Each line is represented as a tuple of start + and end coordinates (x, y). + + Returns + ------- + list of tuples + List of warped lines with same representation as input. + + """ + # Transform + transform = self.get_frame_transform(frame_1, frame_2) + lines = transform(np.array(lines).reshape(-1, 2)).reshape(-1, 2, 2) + # Round to nearest + lines = np.round(lines - 0.5) + 0.5 + lines = [(tuple(line[0]), tuple(line[1])) for line in lines] + return lines + + def warp_points(self, points_img, frame_1, frame_2, **kwargs): + input_shape = np.array(points_img.shape) + points_img = self.warp_image(points_img, frame_1, frame_2, crop=False, + **kwargs) + + points_img = mph.skeletonize(points_img > 0.1) + mph.remove_small_objects(points_img, min_size=10, connectivity=2, + out=points_img) + + # remove 5% border if required + transform = self.get_frame_transform(frame_2, frame_1) + if isinstance(transform, tf.AffineTransform): + # the crop is defined in EBSD coords so need to transform it + crop = np.matmul( + np.linalg.inv(transform.params[0:2, 0:2]), + transform.params[0:2, 2] + 0.05*input_shape + ) + crop = crop.round().astype(int) + points_img = points_img[crop[1]:crop[1] + kwargs['output_shape'][0], + crop[0]:crop[0] + kwargs['output_shape'][1]] + + return zip(*points_img.transpose().nonzero()) + + +class Increment(object): + # def __init__(self, experiment, **kwargs): + def __init__(self, experiment, **kwargs): + + self.maps = {} + # ex: (name, map, frame) + # default behaviour for no frame, different frame for + # each EBSD map, initial increment frame for DIC maps + + self.experiment = experiment + self.metadata = kwargs + + def add_map(self, name, map_obj): + self.maps[name] = map_obj + + +class Frame(object): + def __init__(self): + # self.maps = [] + self.homog_points = [] + + def set_homog_points(self, points): + """ + + Parameters + ---------- + points : numpy.ndarray, optional + Array of (x,y) homologous points to set explicitly. + """ + self.homog_points = points + + def set_homog_point(self, map_obj, map_name=None, **kwargs): + """ + Interactive tool to set homologous points. Right-click on a point + then click 'save point' to append to the homologous points list. + + Parameters + ---------- + map_name : str, optional + Map data to plot for selecting points. + points : numpy.ndarray, optional + Array of (x,y) homologous points to set explicitly. + kwargs : dict, optional + Keyword arguments passed to :func:`defdap.base.Map.plotHomog` + + """ + if map_name is None: + map_name = map_obj.homog_map_name + + binning = map_obj.data.get_metadata(map_name, 'binning', 1) + plot = map_obj.plot_map(map_name, make_interactive=True, **kwargs) + + # Plot stored homog points if there are any + if len(self.homog_points) > 0: + homog_points = np.array(self.homog_points) * binning + plot.add_points(homog_points[:, 0], homog_points[:, 1], c='y', s=60) + else: + # add empty points layer to update later + plot.add_points([None], [None], c='y', s=60) + + # add empty points layer for current selected point + plot.add_points([None], [None], c='w', s=60, marker='x') + + plot.add_event_handler('button_press_event', self.homog_click) + plot.add_event_handler('key_press_event', self.homog_key) + plot.add_button("Save point", + lambda e, p: self.homog_click_save(e, p, binning), + color="0.85", hovercolor="blue") + + return plot + + @staticmethod + def homog_click(event, plot): + """Event handler for capturing position when clicking on a map. + + Parameters + ---------- + event : + Click event. + plot : defdap.plotting.MapPlot + Plot to monitor. + + """ + # check if click was on the map + if event.inaxes is not plot.ax: + return + + # right mouse click or shift + left mouse click + # shift click doesn't work in osx backend + if event.button == 3 or (event.button == 1 and event.key == 'shift'): + plot.add_points([int(event.xdata)], [int(event.ydata)], update_layer=1) + + @staticmethod + def homog_key(event, plot): + """Event handler for moving position using keyboard after clicking on + a map. + + Parameters + ---------- + event : + Keypress event. + plot : defdap.plotting.MapPlot + Plot to monitor. + + """ + arrow_keys = ['left', 'right', 'up', 'down'] + keys = event.key.split('+') + key = keys[-1] + if key not in arrow_keys: + return + + # get the selected point + sel_point = plot.img_layers[plot.points_layer_ids[1]].get_offsets()[0] + if sel_point[0] is None or sel_point[1] is None: + return + + move = 10 if len(keys) == 2 and keys[0] == 'shift' else 1 + if key == arrow_keys[0]: + sel_point[0] -= move + elif key == arrow_keys[1]: + sel_point[0] += move + elif key == arrow_keys[2]: + sel_point[1] -= move + elif key == arrow_keys[3]: + sel_point[1] += move + + plot.add_points([sel_point[0]], [sel_point[1]], update_layer=1) + + def homog_click_save(self, event, plot, binning): + """Append the selected point on the map to homogPoints. + + Parameters + ---------- + event : + Button click event. + plot : defdap.plotting.MapPlot + Plot to monitor. + binning : int, optional + Binning applied to image, if applicable. + + """ + # get the selected point + sel_point = plot.img_layers[plot.points_layer_ids[1]].get_offsets()[0] + if sel_point[0] is None or sel_point[1] is None: + return + + # remove selected point from plot + plot.add_points([None], [None], update_layer=1) + + # then scale and add to homog points list + sel_point = tuple((sel_point / binning).round().astype(int)) + self.homog_points.append(sel_point) + + # update the plotted homog points + homog_points = np.array(self.homog_points) * binning + plot.add_points(homog_points[:, 0], homog_points[:, 1], update_layer=0) + + def update_homog_points(self, homog_idx, new_point=None, delta=None): + """ + Update a homog point by either over writing it with a new point or + incrementing the current values. + + Parameters + ---------- + homog_idx : int + ID (place in list) of point to update or -1 for all. + new_point : tuple, optional + (x, y) coordinates of new point. + delta : tuple, optional + Increments to current point (dx, dy). + + """ + if type(homog_idx) is not int: + raise Exception("homog_idx must be an integer.") + if homog_idx >= len(self.homog_points): + raise Exception("homog_idx is out of range.") + + # Update all points + if homog_idx < 0: + for i in range(len(self.homog_points)): + self.update_homog_points(homog_idx=i, delta=delta) + return + + # Update a single point + # overwrite point + if new_point is not None: + if type(new_point) is not tuple and len(new_point) != 2: + raise Exception("newPoint must be a 2 component tuple") + # increment current point + elif delta is not None: + if type(delta) is not tuple and len(delta) != 2: + raise Exception("delta must be a 2 component tuple") + new_point = list(self.homog_points[homog_idx]) + new_point[0] += delta[0] + new_point[1] += delta[1] + new_point = tuple(new_point) + + self.homog_points[homog_idx] = new_point diff --git a/build/lib/defdap/file_readers.py b/build/lib/defdap/file_readers.py new file mode 100644 index 0000000..0b90cb6 --- /dev/null +++ b/build/lib/defdap/file_readers.py @@ -0,0 +1,839 @@ +# Copyright 2023 Mechanics of Microstructures Group +# at The University of Manchester +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +from numpy.lib.recfunctions import structured_to_unstructured +import pandas as pd +from abc import ABC, abstractmethod +import pathlib +import re + +from typing import TextIO, Dict, List, Callable, Any, Type, Optional + +from defdap.crystal import Phase +from defdap.quat import Quat +from defdap.utils import Datastore + + +class EBSDDataLoader(ABC): + """Class containing methods for loading and checking EBSD data + + """ + def __init__(self) -> None: + # required metadata + self.loaded_metadata = { + 'shape': (0, 0), + 'step_size': 0., + 'acquisition_rotation': Quat(1.0, 0.0, 0.0, 0.0), + 'phases': [], + 'edx': {'Count': 0}, + } + # required data + self.loaded_data = Datastore() + self.loaded_data.add( + 'phase', None, unit='', type='map', order=0, + comment='1-based, 0 is non-indexed points', + plot_params={ + 'vmin': 0, + } + ) + self.loaded_data.add( + 'euler_angle', None, unit='rad', type='map', order=1, + default_component='all_euler' + ) + self.data_format = None + + @staticmethod + def get_loader(data_type: str, file_name: pathlib.Path) -> 'Type[EBSDDataLoader]': + if data_type is None: + data_type = { + '.crc': 'oxfordbinary', + '.cpr': 'oxfordbinary', + '.ctf': 'oxfordtext', + '.ang': 'edaxang', + }.get(file_name.suffix, 'oxfordbinary') + + data_type = data_type.lower() + try: + loader = { + 'oxfordbinary': OxfordBinaryLoader, + 'oxfordtext': OxfordTextLoader, + 'edaxang': EdaxAngLoader, + 'pythondict': PythonDictLoader, + }[data_type] + except KeyError: + raise ValueError(f"No loader for EBSD data of type {data_type}.") + return loader() + + def check_metadata(self) -> None: + """ + Checks that the number of phases from metadata matches + the amount of phases loaded. + + """ + for phase in self.loaded_metadata['phases']: + assert type(phase) is Phase + + def check_data(self) -> None: + shape = self.loaded_metadata['shape'] + + assert self.loaded_data.phase.shape == shape + assert self.loaded_data.euler_angle.shape == (3,) + shape + # assert self.loaded_data['bandContrast'].shape == mapShape + + @abstractmethod + def load(self, file_name: pathlib.Path) -> None: + pass + + +class OxfordTextLoader(EBSDDataLoader): + def load(self, file_name: pathlib.Path) -> None: + """ Read an Oxford Instruments .ctf file, which is a HKL single + orientation file. + + Parameters + ---------- + file_name + Path to file + + """ + # open data file and read in metadata + if not file_name.is_file(): + raise FileNotFoundError(f"Cannot open file {file_name}") + + def parse_phase() -> Phase: + line_split = line.split('\t') + dims = line_split[0].split(';') + dims = tuple(round(float(s), 3) for s in dims) + angles = line_split[1].split(';') + angles = tuple(round(float(s), 3) * np.pi / 180 for s in angles) + lattice_params = dims + angles + phase = Phase( + line_split[2], + int(line_split[3]), + int(line_split[4]), + lattice_params + ) + return phase + + # default values for acquisition rotation in case missing in in file + acq_eulers = [0., 0., 0.] + with open(str(file_name), 'r') as ctf_file: + for i, line in enumerate(ctf_file): + if 'XCells' in line: + x_dim = int(line.split()[-1]) + elif 'YCells' in line: + y_dim = int(line.split()[-1]) + elif 'XStep' in line: + self.loaded_metadata['step_size'] = float(line.split()[-1]) + elif 'AcqE1' in line: + acq_eulers[0] = float(line.split()[-1]) + elif 'AcqE2' in line: + acq_eulers[1] = float(line.split()[-1]) + elif 'AcqE3' in line: + acq_eulers[2] = float(line.split()[-1]) + elif 'Phases' in line: + num_phases = int(line.split()[-1]) + self.loaded_data['phase', 'plot_params']['vmax'] = num_phases + for j in range(num_phases): + line = next(ctf_file) + self.loaded_metadata['phases'].append(parse_phase()) + # phases are last in the header, so read the column + # headings then break out the loop + header_text = next(ctf_file) + num_header_lines = i + j + 3 + break + + shape = (y_dim, x_dim) + self.loaded_metadata['shape'] = shape + self.loaded_metadata['acquisition_rotation'] = Quat.from_euler_angles( + *(np.array(acq_eulers) * np.pi / 180) + ) + + self.check_metadata() + + # Construct data format from table header + field_lookup = { + 'Phase': ('phase', 'uint8'), + 'X': ('x', 'float32'), + 'Y': ('y', 'float32'), + 'Bands': ('numBands', 'uint8'), + 'Error': ('error', 'uint8'), + 'Euler1': ('ph1', 'float32'), + 'Euler2': ('phi', 'float32'), + 'Euler3': ('ph2', 'float32'), + 'MAD': ('MAD', 'float32'), # Mean Angular Deviation + 'BC': ('BC', 'uint8'), # Band Contrast + 'BS': ('BS', 'uint8'), # Band Slope + } + + keep_col_names = ('phase', 'ph1', 'phi', 'ph2', 'BC', 'BS', 'MAD') + data_format = [] + load_cols = [] + try: + for i, col_title in enumerate(header_text.split()): + if field_lookup[col_title][0] in keep_col_names: + data_format.append(field_lookup[col_title]) + load_cols.append(i) + except KeyError: + raise TypeError("Unknown data in EBSD file.") + self.data_format = np.dtype(data_format) + + # now read the data from file + data = np.loadtxt( + str(file_name), dtype=self.data_format, usecols=load_cols, + skiprows=num_header_lines + ) + + self.loaded_data.add( + 'band_contrast', data['BC'].reshape(shape), + unit='', type='map', order=0, + plot_params={ + 'plot_colour_bar': True, + 'cmap': 'gray', + 'clabel': 'Band contrast', + } + + ) + self.loaded_data.add( + 'band_slope', data['BS'].reshape(shape), + unit='', type='map', order=0, + plot_params={ + 'plot_colour_bar': True, + 'cmap': 'gray', + 'clabel': 'Band slope', + } + ) + self.loaded_data.add( + 'mean_angular_deviation', data['MAD'].reshape(shape), + unit='', type='map', order=0, + plot_params={ + 'plot_colour_bar': True, + 'clabel': 'Mean angular deviation', + } + ) + self.loaded_data.phase = data['phase'].reshape(shape) + + euler_angle = structured_to_unstructured( + data[['ph1', 'phi', 'ph2']].reshape(shape)).transpose((2, 0, 1)) + euler_angle *= np.pi / 180 + self.loaded_data.euler_angle = euler_angle + + self.check_data() + + +class EdaxAngLoader(EBSDDataLoader): + def load(self, file_name: pathlib.Path) -> None: + """ Read an EDAX .ang file. + + Parameters + ---------- + file_name + Path to file + + """ + # open data file and read in metadata + if not file_name.is_file(): + raise FileNotFoundError(f"Cannot open file {file_name}") + + i_phase = 1 + # parse header lines (starting with #) + with open(str(file_name), 'r') as ang_file: + while True: + line = ang_file.readline() + + if not line.startswith('#'): + # end of header + break + # remove # + line = line[1:].strip() + + if line.startswith('Phase'): + if int(line.split()[1]) != i_phase: + raise ValueError('Phases not sequential in file?') + + phase_lines = read_until_string( + ang_file, '#', exact=True, + line_process=lambda l: l[1:].strip() + ) + self.loaded_metadata['phases'].append( + EdaxAngLoader.parse_phase(phase_lines) + ) + i_phase += 1 + + elif line.startswith('GRID'): + if line.split()[-1] != 'SqrGrid': + raise ValueError('Only square grids supported') + elif line.startswith('XSTEP'): + self.loaded_metadata['step_size'] = float(line.split()[-1]) + elif line.startswith('NCOLS_ODD'): + xdim = int(line.split()[-1]) + elif line.startswith('NROWS'): + ydim = int(line.split()[-1]) + + shape = (ydim, xdim) + self.loaded_metadata['shape'] = shape + + self.check_metadata() + + # Construct fixed data format + self.data_format = np.dtype([ + ('ph1', 'float32'), + ('phi', 'float32'), + ('ph2', 'float32'), + # ('x', 'float32'), + # ('y', 'float32'), + ('IQ', 'float32'), + ('CI', 'float32'), + ('phase', 'uint8'), + # ('SE_signal', 'float32'), + ('FF', 'float32'), + ]) + load_cols = (0, 1, 2, 5, 6, 7, 8, 9) + + # now read the data from file + data = np.loadtxt( + str(file_name), dtype=self.data_format, comments='#', + usecols=load_cols + ) + + self.loaded_data.add( + 'image_quality', data['IQ'].reshape(shape), + unit='', type='map', order=0, + plot_params={ + 'plot_colour_bar': True, + 'clabel': 'Image quality', + } + ) + self.loaded_data.add( + 'confidence_index', data['CI'].reshape(shape), + unit='', type='map', order=0, + plot_params={ + 'plot_colour_bar': True, + 'clabel': 'Confidence index', + } + ) + self.loaded_data.add( + 'fit_factor', data['FF'].reshape(shape), + unit='', type='map', order=0, + plot_params={ + 'plot_colour_bar': True, + 'clabel': 'Fit factor', + } + ) + self.loaded_data.phase = data['phase'].reshape(shape) + 1 + self.loaded_data['phase', 'plot_params']['vmax'] = len(self.loaded_metadata['phases']) + + # flatten the structured dtype + euler_angle = structured_to_unstructured( + data[['ph1', 'phi', 'ph2']].reshape(shape)).transpose((2, 0, 1)) + euler_angle[0] -= np.pi / 2 + euler_angle[0, euler_angle[0] < 0.] += 2 * np.pi + self.loaded_data.euler_angle = euler_angle + + self.check_data() + + @staticmethod + def parse_phase(lines) -> Phase: + for line in lines: + line = line.split() + + if line[0] == 'MaterialName': + name = line[1] + if line[0] == 'Symmetry': + point_group = line[1] + if point_group in ('43', 'm3m'): + # cubic high + laue_group = 11 + # can't determine but set to BCC for now + space_group = 229 + elif point_group == '6/mmm': + # hex high + laue_group = 9 + space_group = None + else: + raise ValueError(f'Unknown crystal symmetry {point_group}') + elif line[0] == 'LatticeConstants': + dims = line[1:4] + dims = tuple(round(float(s), 3) for s in dims) + angles = line[4:7] + angles = tuple(round(float(s), 3) * np.pi / 180 + for s in angles) + lattice_params = dims + angles + + return Phase(name, laue_group, space_group, lattice_params) + + +class OxfordBinaryLoader(EBSDDataLoader): + def load(self, file_name: pathlib.Path) -> None: + """Read Oxford Instruments .cpr/.crc file pair. + + Parameters + ---------- + file_name + Path to file + + """ + self.load_oxford_cpr(file_name) + self.load_oxford_crc(file_name) + + def load_oxford_cpr(self, file_name: pathlib.Path) -> None: + """ + Read an Oxford Instruments .cpr file, which is a metadata file + describing EBSD data. + + Parameters + ---------- + file_name + Path to file + + """ + comment_char = ';' + + file_name = file_name.with_suffix('.cpr') + if not file_name.is_file(): + raise FileNotFoundError("Cannot open file {}".format(file_name)) + + # CPR file is split into groups, load each group into a + # hierarchical dict + + metadata = dict() + group_pat = re.compile(r"\[(.+)\]") + + def parse_line(line: str, group_dict: Dict) -> None: + try: + key, val = line.strip().split('=') + group_dict[key] = val + except ValueError: + pass + + with open(str(file_name), 'r') as cpr_file: + while True: + line = cpr_file.readline() + if not line: + # End of file + break + if line.strip() == '' or line.strip()[0] == comment_char: + # Skip comment or empty line + continue + + group_name = group_pat.match(line.strip()).group(1) + group_dict = dict() + read_until_string(cpr_file, '[', comment_char=comment_char, + line_process=lambda l: parse_line(l, group_dict)) + metadata[group_name] = group_dict + + # Create phase objects and move metadata to object metadata dict + + x_dim = int(metadata['Job']['xCells']) + y_dim = int(metadata['Job']['yCells']) + self.loaded_metadata['shape'] = (y_dim, x_dim) + self.loaded_metadata['step_size'] = float(metadata['Job']['GridDistX']) + self.loaded_metadata['acquisition_rotation'] = Quat.from_euler_angles( + float(metadata['Acquisition Surface']['Euler1']) * np.pi / 180., + float(metadata['Acquisition Surface']['Euler2']) * np.pi / 180., + float(metadata['Acquisition Surface']['Euler3']) * np.pi / 180. + ) + num_phases = int(metadata['Phases']['Count']) + + for i in range(num_phases): + phase_metadata = metadata['Phase{:}'.format(i + 1)] + self.loaded_metadata['phases'].append(Phase( + phase_metadata['StructureName'], + int(phase_metadata['LaueGroup']), + int(phase_metadata.get('SpaceGroup', 0)), + ( + round(float(phase_metadata['a']), 3), + round(float(phase_metadata['b']), 3), + round(float(phase_metadata['c']), 3), + round(float(phase_metadata['alpha']), 3) * np.pi / 180, + round(float(phase_metadata['beta']), 3) * np.pi / 180, + round(float(phase_metadata['gamma']), 3) * np.pi / 180 + ) + )) + self.loaded_data['phase', 'plot_params']['vmax'] = num_phases + + # Deal with EDX data + edx_fields = {} + if 'EDX Windows' in metadata: + self.loaded_metadata['edx'] = metadata['EDX Windows'] + count = int(self.loaded_metadata['edx']['Count']) + self.loaded_metadata['edx']['Count'] = count + for i in range(1, count + 1): + name = self.loaded_metadata['edx'][f"Window{i}"] + edx_fields[100+i] = (f'EDX {name}', 'float32') + + self.check_metadata() + + # Construct binary data format from listed fields + unknown_field_count = 0 + data_format = [('phase', 'uint8')] + field_lookup = { + 3: ('ph1', 'float32'), + 4: ('phi', 'float32'), + 5: ('ph2', 'float32'), + 6: ('MAD', 'float32'), # Mean Angular Deviation + 7: ('BC', 'uint8'), # Band Contrast + 8: ('BS', 'uint8'), # Band Slope + 10: ('numBands', 'uint8'), + 11: ('AFI', 'uint8'), # Advanced Fit index. legacy + 12: ('IB6', 'float32') # ? + } + field_lookup.update(edx_fields) + try: + for i in range(int(metadata['Fields']['Count'])): + field_id = int(metadata['Fields']['Field{:}'.format(i + 1)]) + data_format.append(field_lookup[field_id]) + except KeyError: + print(f'\nUnknown field in file with key {field_id}. ' + f'Assuming float32 data.') + unknown_field_count += 1 + data_format.append((f'unknown_{unknown_field_count}', 'float32')) + + self.data_format = np.dtype(data_format) + + def load_oxford_crc(self, file_name: pathlib.Path) -> None: + """Read binary EBSD data from an Oxford Instruments .crc file + + Parameters + ---------- + file_name + Path to file + + """ + shape = self.loaded_metadata['shape'] + + file_name = file_name.with_suffix('.crc') + if not file_name.is_file(): + raise FileNotFoundError("Cannot open file {}".format(file_name)) + + # load binary data from file + data = np.fromfile(str(file_name), self.data_format, count=-1) + + self.loaded_data.add( + 'band_contrast', data['BC'].reshape(shape), + unit='', type='map', order=0, + plot_params={ + 'plot_colour_bar': True, + 'cmap': 'gray', + 'clabel': 'Band contrast', + } + ) + self.loaded_data.add( + 'band_slope', data['BS'].reshape(shape), + unit='', type='map', order=0, + plot_params={ + 'plot_colour_bar': True, + 'cmap': 'gray', + 'clabel': 'Band slope', + } + ) + self.loaded_data.add( + 'mean_angular_deviation', + data['MAD'].reshape(shape), + unit='', type='map', order=0, + plot_params={ + 'plot_colour_bar': True, + 'clabel': 'Mean angular deviation', + } + ) + self.loaded_data.phase = data['phase'].reshape(shape) + + # flatten the structured dtype + self.loaded_data.euler_angle = structured_to_unstructured( + data[['ph1', 'phi', 'ph2']].reshape(shape)).transpose((2, 0, 1)) + + if self.loaded_metadata['edx']['Count'] > 0: + EDXFields = [key for key in data.dtype.fields.keys() if key.startswith('EDX')] + for field in EDXFields: + self.loaded_data.add( + field, + data[field].reshape(shape), + unit='counts', type='map', order=0, + plot_params={ + 'plot_colour_bar': True, + 'clabel': field + ' counts', + } + ) + + self.check_data() + + +class PythonDictLoader(EBSDDataLoader): + def load(self, data_dict: Dict[str, Any]) -> None: + """Construct EBSD data from a python dictionary. + + Parameters + ---------- + data_dict + Dictionary with keys: + 'step_size' + 'phases' + 'phase' + 'euler_angle' + 'band_contrast' + + """ + self.loaded_metadata['shape'] = data_dict['phase'].shape + self.loaded_metadata['step_size'] = data_dict['step_size'] + assert type(data_dict['phases']) is list + self.loaded_metadata['phases'] = data_dict['phases'] + self.check_metadata() + + self.loaded_data.add( + 'band_contrast', data_dict['band_contrast'], + unit='', type='map', order=0 + ) + self.loaded_data.phase = data_dict['phase'] + self.loaded_data['phase', 'plot_params']['vmax'] = len(self.loaded_metadata['phases']) + self.loaded_data.euler_angle = data_dict['euler_angle'] + self.check_data() + + +class DICDataLoader(ABC): + """Class containing methods for loading and checking HRDIC data + + """ + def __init__(self) -> None: + self.loaded_metadata = { + 'format': '', + 'version': '', + 'binning': '', + 'shape': (0, 0), + } + # required data + self.loaded_data = Datastore() + self.loaded_data.add( + 'coordinate', None, unit='px', type='map', order=1, + default_component='magnitude', + plot_params={ + 'plot_colour_bar': True, + 'clabel': 'Coordinate', + } + ) + self.loaded_data.add( + 'displacement', None, unit='px', type='map', order=1, + default_component='magnitude', + plot_params={ + 'plot_colour_bar': True, + 'clabel': 'Displacement', + } + ) + + @staticmethod + def get_loader(data_type: str) -> 'Type[DICDataLoader]': + if data_type is None: + data_type = "Davis" + + data_type = data_type.lower() + try: + loader = { + 'davis': DavisLoader, + 'openpiv': OpenPivLoader, + }[data_type] + except KeyError: + raise ValueError(f"No loader for DIC data of type {data_type}.") + return loader() + + def checkMetadata(self) -> None: + return + + def check_data(self) -> None: + """ Calculate size of map from loaded data and check it matches + values from metadata. + + """ + # check binning + binning = self.loaded_metadata['binning'] + binning_x = min(abs(np.diff(self.loaded_data.coordinate[0].flat))) + binning_y = max(abs(np.diff(self.loaded_data.coordinate[1].flat))) + if not (binning_x == binning_y == binning): + raise ValueError( + f'Binning of data and header do not match `{binning_x}`, ' + f'`{binning_y}`, `{binning}`' + ) + + # check shape + coord = self.loaded_data.coordinate + shape = (coord.max(axis=(1, 2)) - coord.min(axis=(1, 2))) / binning + 1 + shape = tuple(shape[::-1].astype(int)) + if shape != self.loaded_metadata['shape']: + raise ValueError( + f'Dimensions of data and header do not match `{shape}, ' + f'`{self.loaded_metadata["shape"]}`' + ) + + @abstractmethod + def load(self, file_name: pathlib.Path) -> None: + pass + + +class DavisLoader(DICDataLoader): + def load(self, file_name: pathlib.Path) -> None: + """ Load from Davis .txt file. + + Parameters + ---------- + file_name + Path to file + + """ + if not file_name.is_file(): + raise FileNotFoundError("Cannot open file {}".format(file_name)) + + with open(str(file_name), 'r') as f: + header = f.readline() + metadata = header.split() + + # Software name and version + self.loaded_metadata['format'] = metadata[0].strip('#') + self.loaded_metadata['version'] = metadata[1] + # Sub-window width in pixels + self.loaded_metadata['binning'] = int(metadata[3]) + # shape of map (from header) + self.loaded_metadata['shape'] = (int(metadata[4]), int(metadata[5])) + + self.checkMetadata() + + data = pd.read_table(str(file_name), delimiter='\t', skiprows=1, + header=None).values + data = data.reshape(self.loaded_metadata['shape'] + (-1,)) + data = data.transpose((2, 0, 1)) + + self.loaded_data.coordinate = data[:2] + self.loaded_data.displacement = data[2:] + + self.check_data() + + @staticmethod + def load_davis_image_data(file_name: pathlib.Path) -> np.ndarray: + """ A .txt file from DaVis containing a 2D image + + Parameters + ---------- + file_name + Path to file + + Returns + ------- + np.ndarray + Array of data. + + """ + if not file_name.is_file(): + raise FileNotFoundError("Cannot open file {}".format(file_name)) + + data = pd.read_table(str(file_name), delimiter='\t', skiprows=1, + header=None) + + return np.array(data) + + +class OpenPivLoader(DICDataLoader): + def load(self, file_name: pathlib.Path) -> None: + """ Load from Open PIV .txt file. + + Parameters + ---------- + file_name + Path to file + + """ + if not file_name.is_file(): + raise FileNotFoundError(f"Cannot open file {file_name}") + + with open(str(file_name), 'r') as f: + header = f.readline()[1:].split() + data = np.loadtxt(f) + col = { + 'x': 0, + 'y': 1, + 'u': 2, + 'v': 3, + } + + # Software name and version + self.loaded_metadata['format'] = 'OpenPIV' + self.loaded_metadata['version'] = 'n/a' + + # Sub-window width in pixels + binning_x = int(np.min(np.abs(np.diff(data[:, col['x']])))) + binning_y = int(np.max(np.abs(np.diff(data[:, col['y']])))) + assert binning_x == binning_y + binning = binning_x + self.loaded_metadata['binning'] = binning + + # shape of map (from header) + shape = data[:, [col['y'], col['x']]].max(axis=0) + binning / 2 + assert np.allclose(shape % binning, 0.) + shape = tuple((shape / binning).astype(int).tolist()) + self.loaded_metadata['shape'] = shape + + self.checkMetadata() + + data = data.reshape(shape + (-1,))[::-1].transpose((2, 0, 1)) + + self.loaded_data.coordinate = data[[col['x'], col['y']]] + self.loaded_data.displacement = data[[col['u'], col['v']]] + + self.check_data() + + +def read_until_string( + file: TextIO, + term_string: str, + comment_char: str = '*', + line_process: Optional[Callable[[str], Any]] = None, + exact: bool = False +) -> List[Any]: + """Read lines in a file until a line starting with the `termString` + is encountered. The file position is returned before the line starting + with the `termString` when found. Comment and empty lines are ignored. + + Parameters + ---------- + file + An open python text file object. + term_string + String to terminate reading. + comment_char + Character at start of a comment line to ignore. + line_process + Function to apply to each line when loaded. + exact + A line must exactly match `termString` to stop. + + Returns + ------- + list + List of processed lines loaded from file. + + """ + lines = [] + while True: + curr_pos = file.tell() # save position in file + line = file.readline() + if (not line + or (exact and line.strip() == term_string) + or (not exact and line.strip().startswith(term_string))): + file.seek(curr_pos) # return to before prev line + break + if line.strip() == '' or line.strip()[0] == comment_char: + # Skip comment or empty line + continue + if line_process is not None: + line = line_process(line) + lines.append(line) + return lines diff --git a/build/lib/defdap/file_writers.py b/build/lib/defdap/file_writers.py new file mode 100644 index 0000000..00ffeb7 --- /dev/null +++ b/build/lib/defdap/file_writers.py @@ -0,0 +1,125 @@ +# Copyright 2023 Mechanics of Microstructures Group +# at The University of Manchester +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import pathlib + +from typing import Type + +from defdap.quat import Quat + + +class EBSDDataWriter(object): + def __init__(self) -> None: + self.metadata = { + 'shape': (0, 0), + 'step_size': 0., + 'acquisition_rotation': Quat(1.0, 0.0, 0.0, 0.0), + 'phases': [] + } + self.data = { + 'phase': None, + 'quat': None, + 'band_contrast': None + } + self.data_format = None + + @staticmethod + def get_writer(datatype: str) -> "Type[EBSDDataLoader]": + if datatype is None: + datatype = "OxfordText" + + if datatype == "OxfordText": + return OxfordTextWriter() + else: + raise ValueError(f"No loader for EBSD data of type {datatype}.") + + +class OxfordTextWriter(EBSDDataWriter): + def write(self, file_name: str, file_dir: str = "") -> None: + """ Write an Oxford Instruments .ctf file, which is a HKL single + orientation file. + + Parameters + ---------- + file_name + File name. + file_dir + Path to file. + + """ + + # check output file + file_name = "{}.ctf".format(file_name) + file_path = pathlib.Path(file_dir) / pathlib.Path(file_name) + if file_path.exists(): + raise FileExistsError(f"File already exits {file_path}") + + shape = self.metadata['shape'] + step_size = self.metadata['step_size'] + + # convert quats to Euler angles + out_euler_array = np.zeros(shape + (3,)) + for idx in np.ndindex(shape): + out_euler_array[idx] = self.data['quat'][idx].euler_angles() + out_euler_array *= 180 / np.pi + acq_rot = self.metadata['acquisition_rotation'].euler_angles() + acq_rot *= 180 / np.pi + + # create coordinate grids + x_grid, y_grid = np.meshgrid( + np.arange(0, shape[1]) * step_size, + np.arange(0, shape[0]) * step_size, + indexing='xy' + ) + + with open(str(file_path), 'w') as ctf_file: + # write header + ctf_file.write("Channel Text File\n") + ctf_file.write("Prj\t\n") + ctf_file.write("Author\t\n") + ctf_file.write("JobMode\tGrid\n") + ctf_file.write(f"XCells\t{shape[1]}\n") + ctf_file.write(f"YCells\t{shape[0]}\n") + ctf_file.write(f"XStep\t{step_size :.4f}\n") + ctf_file.write(f"YStep\t{step_size :.4f}\n") + ctf_file.write(f"AcqE1\t{acq_rot[0]:.4f}\n") + ctf_file.write(f"AcqE2\t{acq_rot[1]:.4f}\n") + ctf_file.write(f"AcqE3\t{acq_rot[2]:.4f}\n") + ctf_file.write( + "Euler angles refer to Sample Coordinate system (CS0)!\n") + ctf_file.write(f"Phases\t{len(self.metadata['phases'])}\n") + for phase in self.metadata['phases']: + dims = "{:.3f};{:.3f};{:.3f}".format(*phase.lattice_params[:3]) + angles = (f * 180 / np.pi for f in phase.lattice_params[3:]) + angles = "{:.3f};{:.3f};{:.3f}".format(*angles) + + ctf_file.write(f"{dims}\t{angles}\t{phase.name}" + f"\t{phase.laue_group}\t0\t\t\t\n") + + ctf_file.write("Phase\tX\tY\tBands\tError\tEuler1\tEuler2" + "\tEuler3\tMAD\tBC\tBS\n") + + for x, y, phase, eulers, bc in zip( + x_grid.flat, y_grid.flat, + self.data['phase'].flat, + out_euler_array.reshape((shape[0] * shape[1], 3)), + self.data['band_contrast'].flat, + ): + error = 3 if phase == 0 else 0 + ctf_file.write( + f"{phase}\t{x:.3f}\t{y:.3f}\t10\t{error}\t{eulers[0]:.3f}" + f"\t{eulers[1]:.3f}\t{eulers[2]:.3f}\t0.0000\t{bc}\t0\n" + ) diff --git a/build/lib/defdap/hrdic.py b/build/lib/defdap/hrdic.py new file mode 100644 index 0000000..a409d59 --- /dev/null +++ b/build/lib/defdap/hrdic.py @@ -0,0 +1,928 @@ +# Copyright 2023 Mechanics of Microstructures Group +# at The University of Manchester +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path + +import numpy as np +from matplotlib.pyplot import imread +import inspect + +from skimage import transform as tf +from skimage import measure + +from scipy.stats import mode +from scipy.ndimage import binary_dilation + +import peakutils + +from defdap._accelerated import flood_fill_dic +from defdap.utils import Datastore +from defdap.file_readers import DICDataLoader, DavisLoader +from defdap import base + +from defdap import defaults +from defdap.plotting import MapPlot, GrainPlot +from defdap.inspector import GrainInspector +from defdap.utils import report_progress + + +class Map(base.Map): + """ + Class to encapsulate DIC map data and useful analysis and plotting + methods. + + Attributes + ---------- + format : str + Software name. + version : str + Software version. + binning : int + Sub-window size in pixels. + xdim : int + Size of map along x (from header). + ydim : int + Size of map along y (from header). + shape : tuple + Size of map (after cropping, like *Dim). + corrVal : numpy.ndarray + Correlation value. + ebsd_map : defdap.ebsd.Map + EBSD map linked to DIC map. + highlight_alpha : float + Alpha (transparency) of grain highlight. + bseScale : float + Size of a pixel in the correlated images. + path : str + File path. + fname : str + File name. + crop_dists : numpy.ndarray + Crop distances (default all zeros). + + data : defdap.utils.Datastore + Must contain after loading data (maps): + coordinate : numpy.ndarray + X and Y coordinates + displacement : numpy.ndarray + X and Y displacements + Generated data: + f : numpy.ndarray + Components of the deformation gradient (0=x, 1=y). + e : numpy.ndarray + Components of the green strain (0=x, 1=y). + max_shear : numpy.ndarray + Max shear component np.sqrt(((e11 - e22) / 2.)**2 + e12**2). + Derived data: + Grain list data to map data from all grains + + """ + MAPNAME = 'hrdic' + + def __init__(self, *args, **kwargs): + """Initialise class and import DIC data from file. + + Parameters + ---------- + *args, **kwarg + Passed to base constructor + + """ + # Initialise variables + self.format = None # Software name + self.version = None # Software version + self.binning = None # Sub-window size in pixels + self.xdim = None # size of map along x (from header) + self.ydim = None # size of map along y (from header) + + # Call base class constructor + super(Map, self).__init__(*args, **kwargs) + + self.corr_val = None # correlation value + + self.ebsd_map = None # EBSD map linked to DIC map + self.highlight_alpha = 0.6 + self.bse_scale = None # size of pixels in pattern images + self.bse_scale = None # size of pixels in pattern images + self.crop_dists = np.array(((0, 0), (0, 0)), dtype=int) + + ## TODO: cropping, have metadata to state if saved data is cropped, if + ## not cropped then crop on accesss. Maybe mark cropped data as invalid + ## if crop distances change + + # Deformation gradient + f = np.gradient(self.data.displacement, self.binning, axis=(1, 2)) + f = np.array(f).transpose((1, 0, 2, 3))[:, ::-1] + f[0, 0] += 1 + f[1, 1] += 1 + self.data.add( + 'f', f, unit='', type='map', order=2, default_component=(0, 0), + plot_params={ + 'plot_colour_bar': True, + 'clabel': 'Deformation gradient', + } + ) + + # Green strain + e = 0.5 * (np.einsum('ki...,kj...->ij...', f, f)) + e[0, 0] -= 0.5 + e[1, 1] -= 0.5 + self.data.add( + 'e', e, unit='', type='map', order=2, default_component=(0, 0), + plot_params={ + 'plot_colour_bar': True, + 'clabel': 'Green strain', + } + ) + + # max shear component + max_shear = np.sqrt(((e[0, 0] - e[1, 1]) / 2.) ** 2 + e[0, 1] ** 2) + self.data.add( + 'max_shear', max_shear, unit='', type='map', order=0, + plot_params={ + 'plot_colour_bar': True, + 'clabel': 'Effective shear strain', + } + ) + + # pattern image + self.data.add_generator( + 'pattern', self.load_pattern, unit='', type='map', order=0, + save=False, + plot_params={ + 'cmap': 'gray' + } + ) + + self.data.add_generator( + 'grains', self.find_grains, unit='', type='map', order=0, + cropped=True + ) + + self.plot_default = lambda *args, **kwargs: self.plot_map(map_name='max_shear', + plot_gbs=True, *args, **kwargs + ) + self.homog_map_name = 'max_shear' + + @property + def original_shape(self): + return self.ydim, self.xdim + + @property + def crystal_sym(self): + return self.ebsd_map.crystal_sym + + @report_progress("loading HRDIC data") + def load_data(self, file_name, data_type=None): + """Load DIC data from file. + + Parameters + ---------- + file_name : pathlib.Path + Name of file including extension. + data_type : str, {'Davis', 'OpenPIV'} + Type of data file. + + """ + data_loader = DICDataLoader.get_loader(data_type) + data_loader.load(file_name) + + metadata_dict = data_loader.loaded_metadata + self.format = metadata_dict['format'] # Software name + self.version = metadata_dict['version'] # Software version + self.binning = metadata_dict['binning'] # Sub-window width in pixels + # *dim are full size of data. shape (old *Dim) are size after cropping + # *dim are full size of data. shape (old *Dim) are size after cropping + self.shape = metadata_dict['shape'] + self.xdim = metadata_dict['shape'][1] # size of map along x (from header) + self.ydim = metadata_dict['shape'][0] # size of map along y (from header) + + self.data.update(data_loader.loaded_data) + + # write final status + yield (f"Loaded {self.format} {self.version} data " + f"(dimensions: {self.xdim} x {self.xdim} pixels, " + f"sub-window size: {self.binning} x {self.binning} pixels)") + + def load_corr_val_data(self, file_name, data_type=None): + """Load correlation value for DIC data + + Parameters + ---------- + file_name : pathlib.Path or str + Path to file. + data_type : str, {'DavisImage'} + Type of data file. + + """ + data_type = "DavisImage" if data_type is None else data_type + + data_loader = DavisLoader() + if data_type == "DavisImage": + loaded_data = data_loader.load_davis_image_data(Path(file_name)) + else: + raise Exception("No loader found for this DIC data.") + + self.corr_val = loaded_data + + assert self.xdim == self.corr_val.shape[1], \ + "Dimensions of imported data and dic data do not match" + assert self.ydim == self.corr_val.shape[0], \ + "Dimensions of imported data and dic data do not match" + + def retrieve_name(self): + """Gets the first name assigned to the a map, as a string + + """ + for fi in reversed(inspect.stack()): + names = [key for key, val in fi.frame.f_locals.items() if val is self] + if len(names) > 0: + return names[0] + + def set_scale(self, scale): + """Sets the scale of the map. + + Parameters + ---------- + scale : float + Length of pixel in original BSE image in micrometres. + + """ + self.bse_scale = scale + + @property + def scale(self): + """Returns the number of micrometers per pixel in the DIC map. + + """ + if self.bse_scale is None: + # raise ValueError("Map scale not set. Set with setScale()") + return None + + return self.bse_scale * self.binning + + def print_stats_table(self, percentiles, components): + """Print out a statistics table for a DIC map + + Parameters + ---------- + percentiles : list of float + list of percentiles to print i.e. 0, 50, 99. + components : list of str + list of map components to print i.e. e, f, max_shear. + + """ + + # Check that components are valid + if not set(components).issubset(self.data): + str_format = '{}, ' * (len(self.data) - 1) + '{}' + raise Exception("Components must be: " + str_format.format(*self.data)) + + # Print map info + print('\033[1m', end=''), # START BOLD + print("{0} (dimensions: {1} x {2} pixels, sub-window size: {3} " + "x {3} pixels, number of points: {4})\n".format( + self.retrieve_name(), self.x_dim, self.y_dim, + self.binning, self.x_dim * self.y_dim + )) + + # Print header + str_format = '{:10} ' + '{:12}' * len(percentiles) + print(str_format.format('Component', *percentiles)) + print('\033[0m', end='') # END BOLD + + # Print table + str_format = '{:10} ' + '{:12.4f}' * len(percentiles) + for c in components: + # Iterate over tensor components (i.e. e11, e22, e12) + for i in np.ndindex(self.data[c].shape[:len(np.shape(self.data[c]))-2]): + per = [np.nanpercentile(self.data[c][i], p) for p in percentiles] + print(str_format.format(c+''.join([str(t+1) for t in i]), *per)) + + def set_crop(self, *, left=None, right=None, top=None, bottom=None, + update_homog_points=False): + """Set a crop for the DIC map. + + Parameters + ---------- + left : int + Distance to crop from left in pixels (formally `xMin`) + right : int + Distance to crop from right in pixels (formally `xMax`) + top : int + Distance to crop from top in pixels (formally `yMin`) + bottom : int + Distance to crop from bottom in pixels (formally `yMax`) + update_homog_points : bool, optional + If true, change homologous points to reflect crop. + + """ + # changes in homog points + dx = 0 + dy = 0 + + # update crop distances + if left is not None: + left = int(left) + dx = self.crop_dists[0, 0] - left + self.crop_dists[0, 0] = left + if right is not None: + self.crop_dists[0, 1] = int(right) + if top is not None: + top = int(top) + dy = self.crop_dists[1, 0] - top + self.crop_dists[1, 0] = top + if bottom is not None: + self.crop_dists[1, 1] = int(bottom) + + # update homogo points if required + if update_homog_points and (dx != 0 or dy != 0): + self.frame.update_homog_points(homog_idx=-1, delta=(dx, dy)) + + # set new cropped dimensions + x_dim = self.xdim - self.crop_dists[0, 0] - self.crop_dists[0, 1] + y_dim = self.ydim - self.crop_dists[1, 0] - self.crop_dists[1, 1] + self.shape = (y_dim, x_dim) + + def crop(self, map_data, binning=None): + """ Crop given data using crop parameters stored in map + i.e. cropped_data = DicMap.crop(DicMap.data_to_crop). + + Parameters + ---------- + map_data : numpy.ndarray + Bap data to crop. + binning : int + True if mapData is binned i.e. binned BSE pattern. + """ + binning = 1 if binning is None else binning + + min_y = int(self.crop_dists[1, 0] * binning) + max_y = int((self.ydim - self.crop_dists[1, 1]) * binning) + + min_x = int(self.crop_dists[0, 0] * binning) + max_x = int((self.xdim - self.crop_dists[0, 1]) * binning) + + return map_data[..., min_y:max_y, min_x:max_x] + + def link_ebsd_map(self, ebsd_map, transform_type="affine", **kwargs): + """Calculates the transformation required to align EBSD dataset to DIC. + + Parameters + ---------- + ebsd_map : defdap.ebsd.Map + EBSD map object to link. + transform_type : str, optional + affine, piecewiseAffine or polynomial. + kwargs + All arguments are passed to `estimate` method of the transform. + + """ + self.ebsd_map = ebsd_map + kwargs.update({'type': transform_type.lower()}) + self.experiment.link_frames(self.frame, ebsd_map.frame, kwargs) + self.data.add_derivative( + self.ebsd_map.data, + lambda boundaries: BoundarySet.from_ebsd_boundaries( + self, boundaries + ), + in_props={ + 'type': 'boundaries' + } + ) + + def check_ebsd_linked(self): + """Check if an EBSD map has been linked. + + Returns + ---------- + bool + Returns True if EBSD map linked. + + Raises + ---------- + Exception + If EBSD map not linked. + + """ + if self.ebsd_map is None: + raise Exception("No EBSD map linked.") + return True + + def warp_to_dic_frame(self, map_data, **kwargs): + """Warps a map to the DIC frame. + + Parameters + ---------- + map_data : numpy.ndarray + Data to warp. + kwargs + All other arguments passed to :func:`defdap.experiment.Experiment.warp_map`. + + Returns + ---------- + numpy.ndarray + Map (i.e. EBSD map data) warped to the DIC frame. + + """ + # Check a EBSD map is linked + self.check_ebsd_linked() + return self.experiment.warp_image( + map_data, self.ebsd_map.frame, self.frame, output_shape=self.shape, + **kwargs + ) + + # TODO: fix component stuff + def generate_threshold_mask(self, mask, dilation=0, preview=True): + """ + Generate a dilated mask, based on a boolean array and previews the appication of + this mask to the max shear map. + + Parameters + ---------- + mask: numpy.array(bool) + A boolean array where points to be removed are True + dilation: int, optional + Number of pixels to dilate the mask by. Useful to remove anomalous points + around masked values. No dilation applied if not specified. + preview: bool + If true, show the mask and preview the masked effective shear strain map. + + Examples + ---------- + To remove data points in dic_map where `max_shear` is above 0.8, use: + + >>> mask = dic_map.data.max_shear > 0.8 + + To remove data points in dic_map where e11 is above 1 or less than -1, use: + + >>> mask = (dic_map.data.e[0, 0] > 1) | (dic_map.data.e[0, 0] < -1) + + To remove data points in dic_map where corrVal is less than 0.4, use: + + >>> mask = dic_map.corr_val < 0.4 + + Note: correlation value data needs to be loaded seperately from the DIC map, + see :func:`defdap.hrdic.load_corr_val_data` + + """ + self.mask = mask + + if dilation != 0: + self.mask = binary_dilation(self.mask, iterations=dilation) + + num_removed = np.sum(self.mask) + num_total = self.xdim * self.ydim + num_removed_crop = np.sum(self.crop(self.mask)) + num_total_crop = self.x_dim * self.y_dim + + print('Filtering will remove {0} \ {1} ({2:.3f} %) datapoints in map' + .format(num_removed, num_total, (num_removed / num_total) * 100)) + print( + 'Filtering will remove {0} \ {1} ({2:.3f} %) datapoints in cropped map' + .format(num_removed_crop, num_total_crop, + (num_removed_crop / num_total_crop * 100))) + + if preview == True: + plot1 = MapPlot.create(self, self.crop(self.mask), cmap='binary') + plot1.set_title('Removed datapoints in black') + plot2 = MapPlot.create(self, + self.crop( + np.where(self.mask == True, np.nan, + self.data.max_shear)), + plot_colour_bar='True', + clabel="Effective shear strain") + plot2.set_title('Effective shear strain preview') + print( + 'Use apply_threshold_mask function to apply this filtering to data') + + def apply_threshold_mask(self): + """ Apply mask to all DIC map data by setting masked values to nan. + + """ + for comp in ('max_shear', + 'e11', 'e12', 'e22', + 'f11', 'f12', 'f21', 'e22', + 'x_map', 'y_map'): + # self.data[comp] = np.where(self.mask == True, np.nan, self.data[comp]) + self.data[comp][self.mask] = np.nan + + def set_pattern(self, img_path, window_size): + """Set the path to the image of the pattern. + + Parameters + ---------- + path : str + Path to image. + window_size : int + Size of pixel in pattern image relative to pixel size of DIC data + i.e 1 means they are the same size and 2 means the pixels in + the pattern are half the size of the dic data. + + """ + path = self.file_name.parent / img_path + self.data['pattern', 'path'] = path + self.data['pattern', 'binning'] = window_size + + def load_pattern(self): + print('Loading img') + path = self.data.get_metadata('pattern', 'path') + binning = self.data.get_metadata('pattern', 'binning', 1) + if path is None: + raise FileNotFoundError("First set path to pattern image.") + + img = imread(path) + exp_shape = tuple(v * binning for v in self.original_shape) + if img.shape != exp_shape: + raise ValueError( + f'Incorrect size of pattern image. For binning of {binning} ' + f'expected size {exp_shape[::-1]} but got {img.shape[::-1]}' + ) + return img + + def plot_grain_av_max_shear(self, **kwargs): + """Plot grain map with grains filled with average value of max shear. + This uses the max shear values stored in grain objects, to plot other data + use :func:`~defdap.hrdic.Map.plotGrainAv`. + + Parameters + ---------- + kwargs + All arguments are passed to :func:`defdap.base.Map.plot_grain_data_map`. + + """ + # Set default plot parameters then update with any input + plot_params = { + 'clabel': "Effective shear strain" + } + plot_params.update(kwargs) + + plot = self.plot_grain_data_map( + map_data=self.data.max_shear, **plot_params + ) + + return plot + + @report_progress("finding grains") + def find_grains(self, algorithm=None, min_grain_size=10): + """Finds grains in the DIC map. + + Parameters + ---------- + algorithm : str {'warp', 'floodfill'} + Use floodfill or warp algorithm. + min_grain_size : int + Minimum grain area in pixels for floodfill algorithm. + """ + # Check a EBSD map is linked + self.check_ebsd_linked() + + if algorithm is None: + algorithm = defaults['hrdic_grain_finding_method'] + algorithm = algorithm.lower() + + grain_list = [] + group_id = Datastore.generate_id() + + if algorithm == 'warp': + # Warp EBSD grain map to DIC frame + grains = self.warp_to_dic_frame( + self.ebsd_map.data.grains, order=0, preserve_range=True + ) + + # Find all unique values (these are the EBSD grain IDs in the DIC area, sorted) + ebsd_grain_ids = np.unique(grains) + neg_vals = ebsd_grain_ids[ebsd_grain_ids <= 0] + ebsd_grain_ids = ebsd_grain_ids[ebsd_grain_ids > 0] + + # Map the EBSD IDs to the DIC IDs (keep the same mapping for values <= 0) + old = np.concatenate((neg_vals, ebsd_grain_ids)) + 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) + + # Make grain object + grain = Grain(dic_grain_id, self, group_id) + + # Find (x,y) coordinates and corresponding max shears of grain + 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 + grain.ebsd_grain = self.ebsd_map[ebsd_grain_id - 1] + grain.ebsd_map = self.ebsd_map + grain_list.append(grain) + + elif algorithm == 'floodfill': + # Initialise the grain map + grains = -np.copy(self.data.grain_boundaries.image.astype(int)) + + # List of points where no grain has been set yet + points_left = grains == 0 + 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') + + # Start counter for grains + grain_index = 1 + # Loop until all points (except boundaries) have been assigned + # to a grain or ignored + i = 0 + 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_dic( + (seed[1], seed[0]), grain_index, points_left, + 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 + # values in grain map to -2 + for point in grain.data.point: + grains[point[1], point[0]] = -2 + else: + # add grain to list and increment grain index + grain_list.append(grain) + grain_index += 1 + + # find next search point + points_left_sub = points_left.reshape(-1)[next_point + 1:] + found_point = points_left_sub.tobytes().find(b'\x01') + next_point += found_point + 1 + + # report progress + i += 1 + if i == defaults['find_grain_report_freq']: + yield 1. - points_left_sub.sum() / total_points + i = 0 + + # Now link grains to those in ebsd Map + # Warp DIC grain map to EBSD frame + warped_dic_grains = self.experiment.warp_image( + grains.astype(float), self.frame, self.ebsd_map.frame, + output_shape=self.ebsd_map.shape, order=0 + ).astype(int) + for i, grain in enumerate(grain_list): + # Find grain by masking the native ebsd grain image with + # selected grain from the warped dic grain image. The modal + # value is the EBSD grain label. + mode_id, _ = mode( + self.ebsd_map.data.grains[warped_dic_grains == i+1], + keepdims=False + ) + grain.ebsd_grain = self.ebsd_map[mode_id - 1] + grain.ebsd_map = self.ebsd_map + + else: + raise ValueError(f"Unknown grain finding algorithm '{algorithm}'.") + + ## TODO: this will get duplicated if find grains called again + self.data.add_derivative( + grain_list[0].data, self.grain_data_to_map, pass_ref=True, + in_props={ + 'type': 'list' + }, + out_props={ + 'type': 'map' + } + ) + + self._grains = grain_list + return grains + + def grain_inspector(self, vmax=0.1, correction_angle=0, rdr_line_length=3): + """Run the grain inspector interactive tool. + + Parameters + ---------- + vmax : float + Maximum value of the colour map. + correction_angle: float + Correction angle in degrees to subtract from measured angles to account + for small rotation between DIC and EBSD frames. Approximately the rotation + component of affine transform. + rdr_line_length: int + Length of lines perpendicular to slip trace used to calculate RDR. + + """ + GrainInspector(selected_dic_map=self, vmax=vmax, correction_angle=correction_angle, + rdr_line_length=rdr_line_length) + + +class Grain(base.Grain): + """ + Class to encapsulate DIC grain data and useful analysis and plotting + methods. + + Attributes + ---------- + dicMap : defdap.hrdic.Map + DIC map this grain is a member of + ownerMap : defdap.hrdic.Map + DIC map this grain is a member of + maxShearList : list + List of maximum shear values for grain. + ebsd_grain : defdap.ebsd.Grain + EBSD grain ID that this DIC grain corresponds to. + ebsd_map : defdap.ebsd.Map + EBSD map that this DIC grain belongs to. + points_list : numpy.ndarray + Start and end points for lines drawn using defdap.inspector.GrainInspector. + groups_list : + Groups, angles and slip systems detected for + lines drawn using defdap.inspector.GrainInspector. + + data : defdap.utils.Datastore + Must contain after creating: + point : list of tuples + (x, y) in cropped map + Generated data: + + Derived data: + Map data to list data from the map the grain is part of + + """ + def __init__(self, grain_id, dicMap, group_id): + # Call base class constructor + super(Grain, self).__init__(grain_id, dicMap, group_id) + + self.dic_map = self.owner_map # DIC map this grain is a member of + self.ebsd_grain = None + self.ebsd_map = None + + self.points_list = [] # Lines drawn for STA + self.groups_list = [] # Unique angles drawn for STA + + self.plot_default = lambda *args, **kwargs: self.plot_max_shear( + plot_colour_bar=True, plot_scale_bar=True, plot_slip_traces=True, + plot_slip_bands=True, *args, **kwargs + ) + + def plot_max_shear(self, **kwargs): + """Plot a maximum shear map for a grain. + + Parameters + ---------- + kwargs + All arguments are passed to :func:`defdap.base.plot_grain_data`. + + Returns + ------- + defdap.plotting.GrainPlot + + """ + # Set default plot parameters then update with any input + plot_params = { + 'plot_colour_bar': True, + 'clabel': "Effective shear strain" + } + plot_params.update(kwargs) + + plot = self.plot_grain_data(grain_data=self.data.max_shear, **plot_params) + + return plot + + @property + def ref_ori(self): + """Returns average grain orientation. + + Returns + ------- + defdap.quat.Quat + + """ + return self.ebsd_grain.ref_ori + + @property + def slip_traces(self): + """Returns list of slip trace angles based on EBSD grain orientation. + + Returns + ------- + list + + """ + return self.ebsd_grain.slip_traces + + def calc_slip_traces(self, slip_systems=None): + """Calculates list of slip trace angles based on EBSD grain orientation. + + Parameters + ------- + slip_systems : defdap.crystal.SlipSystem, optional + + """ + self.ebsd_grain.calc_slip_traces(slip_systems=slip_systems) + + def calc_slip_bands(self, grain_map_data, thres=None, min_dist=None): + """Use Radon transform to detect slip band angles. + + Parameters + ---------- + grain_map_data : numpy.ndarray + Data to find bands in. + thres : float, optional + Normalised threshold for peaks. + min_dist : int, optional + Minimum angle between bands. + + Returns + ---------- + list(float) + Detected slip band angles + + """ + if thres is None: + thres = 0.3 + if min_dist is None: + min_dist = 30 + grain_map_data = np.nan_to_num(grain_map_data) + + if grain_map_data.min() < 0: + print("Negative values in data, taking absolute value.") + # grain_map_data = grain_map_data**2 + grain_map_data = np.abs(grain_map_data) + # array to hold shape / support of grain + supp_gmd = np.zeros(grain_map_data.shape) + supp_gmd[grain_map_data != 0]=1 + sin_map = tf.radon(grain_map_data, circle=False) + #profile = np.max(sin_map, axis=0) # old method + supp_map = tf.radon(supp_gmd, circle=False) + supp_1 = np.zeros(supp_map.shape) + supp_1[supp_map>0]=1 + # minimum diameter of grain + mindiam = np.min(np.sum(supp_1, axis=0), axis=0) + crop_map = np.zeros(sin_map.shape) + # only consider radon rays that cut grain with mindiam*2/3 or more, + # and scale by length of the cut + selection = supp_map > mindiam * 2 / 3 + crop_map[selection] = sin_map[selection] / supp_map[selection] + supp_crop = np.zeros(crop_map.shape) + supp_crop[crop_map>0] = 1 + + # raise to power to accentuate local peaks + profile = np.sum(crop_map**4, axis=0) / np.sum(supp_crop, axis=0) + + x = np.arange(180) + + indexes = peakutils.indexes(profile, thres=thres, min_dist=min_dist) + peaks = x[indexes] + # peaks = peakutils.interpolate(x, profile, ind=indexes) + print("Number of bands detected: {:}".format(len(peaks))) + + slip_band_angles = peaks + slip_band_angles = slip_band_angles * np.pi / 180 + return slip_band_angles + + +class BoundarySet(object): + def __init__(self, dic_map, points, lines): + self.dic_map = dic_map + self.points = set(points) + self.lines = lines + + @classmethod + def from_ebsd_boundaries(cls, dic_map, ebsd_boundaries): + if len(ebsd_boundaries.points) == 0: + return cls(dic_map, [], []) + + points = dic_map.experiment.warp_points( + ebsd_boundaries.image.astype(float), + dic_map.ebsd_map.frame, dic_map.frame, + output_shape=dic_map.shape + ) + lines = dic_map.experiment.warp_lines( + ebsd_boundaries.lines, dic_map.ebsd_map.frame, dic_map.frame + ) + return cls(dic_map, points, lines) + + def _image(self, points): + image = np.zeros(self.dic_map.shape, dtype=bool) + image[tuple(zip(*points))[::-1]] = True + return image + + @property + def image(self): + return self._image(self.points) diff --git a/build/lib/defdap/inspector.py b/build/lib/defdap/inspector.py new file mode 100644 index 0000000..84191e2 --- /dev/null +++ b/build/lib/defdap/inspector.py @@ -0,0 +1,663 @@ +# Copyright 2023 Mechanics of Microstructures Group +# at The University of Manchester +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +from scipy.stats import linregress +from skimage.draw import line as skimage_line +import ast + +from defdap.plotting import Plot, GrainPlot +from defdap import hrdic + +from typing import List + + +class GrainInspector: + """ + Class containing the interactive grain inspector tool for slip trace analysis + and relative displacement ratio analysis. + + """ + + def __init__(self, + selected_dic_map: 'hrdic.Map', + vmax: float, + correction_angle: float = 0, + rdr_line_length: int = 3): + """ + + Parameters + ---------- + selected_dic_map + DIC map to run grain inspector on. + vmax + Maximum effective shear strain in colour scale. + correction_angle + Angle (in degrees) to subtract from drawn line angle. + rdr_line_length + Length on lines perpendicular to slip trace (can be any odd number above default 3). + """ + # Initialise some values + self.grain_id = 0 + self.selected_dic_map = selected_dic_map + self.selected_ebsd_map = self.selected_dic_map.ebsd_map + self.selected_dic_grain = self.selected_dic_map[self.grain_id] + self.selected_ebsd_grain = self.selected_dic_grain.ebsd_grain + self.vmax = vmax + self.correction_angle = correction_angle + self.rdr_line_length = rdr_line_length + self.filename = str(self.selected_dic_map.retrieve_name()) + '_RDR.txt' + + # Plot window + self.plot = Plot(ax=None, make_interactive=True, figsize=(13, 8), title='Grain Inspector') + div_frac = 0.7 + + # Remove key bindings for figure to suppress errors + self.plot.fig.canvas.mpl_disconnect(self.plot.fig.canvas.manager.key_press_handler_id) + + # Buttons + self.plot.add_button( + 'Save\nLine', self.save_line, (div_frac, 0.48, 0.05, 0.04)) + self.plot.add_button( + 'Previous\nGrain', lambda e, p: self.goto_grain(self.grain_id - 1, p), (div_frac, 0.94, 0.05, 0.04)) + self.plot.add_button( + 'Next\nGrain', lambda e, p: self.goto_grain(self.grain_id + 1, p), (div_frac + 0.06, 0.94, 0.05, 0.04)) + self.plot.add_button( + 'Run All STA', self.batch_run_sta, (0.85, 0.07, 0.11, 0.04)) + self.plot.add_button( + 'Clear\nAll Lines', self.clear_all_lines, (div_frac + 0.2, 0.48, 0.05, 0.04)) + self.plot.add_button( + 'Load\nFile', self.load_file, (0.85, 0.02, 0.05, 0.04)) + self.plot.add_button( + 'Save\nFile', self.save_file, (0.91, 0.02, 0.05, 0.04)) + + # Text boxes + self.plot.add_text_box(label='', loc=(0.7, 0.02, 0.13, 0.04), + change_handler=self.update_filename, initial=self.filename) + self.plot.add_text_box(label='Go to \ngrain ID:', loc=(div_frac + 0.17, 0.94, 0.05, 0.04), + submit_handler=self.goto_grain) + self.plot.add_text_box(label='Remove\nID:', loc=(div_frac + 0.1, 0.48, 0.05, 0.04), + submit_handler=self.remove_line) + self.rdr_group_text_box = self.plot.add_text_box(label='Run RDR only\non group:', loc=(0.78, 0.07, 0.05, 0.04), + submit_handler=self.run_rdr_group) + + # Axes + self.max_shear_axis = self.plot.add_axes((0.05, 0.4, 0.65, 0.55)) + self.slip_trace_axis = self.plot.add_axes((0.25, 0.05, 0.5, 0.3)) + self.unit_cell_axis = self.plot.add_axes((0.05, 0.055, 0.2, 0.3), proj='3d') + self.grain_info_axis = self.plot.add_axes((div_frac, 0.86, 0.25, 0.06)) + self.line_info_axis = self.plot.add_axes((div_frac, 0.55, 0.25, 0.3)) + self.groups_info_axis = self.plot.add_axes((div_frac, 0.15, 0.25, 0.3)) + self.grain_plot = self.selected_dic_map[self.grain_id].plot_max_shear(fig=self.plot.fig, + ax=self.max_shear_axis, + vmax=self.vmax, + plot_scale_bar=True, + plot_colour_bar=True) + self.plot.ax.axis('off') + + # Draw the stuff that will need to be redrawn often in a separate function + self.redraw() + + def goto_grain(self, + event: int, + plot): + """ Go to a specified grain ID. + + Parameters + ---------- + event + Grain ID to go to. + + """ + # Go to grain ID specified in event + self.grain_id = int(event) + self.grain_plot.arrow = None + self.selected_dic_grain = self.selected_dic_map[self.grain_id] + self.selected_ebsd_grain = self.selected_dic_grain.ebsd_grain + self.redraw() + + def save_line(self, + event: np.ndarray, + plot): + """ Save the start point, end point and angle of drawn line into the grain. + + Parameters + ---------- + event + Start x, start y, end x, end y point of line passed from drawn line. + + """ + + # Get angle of lines + line_angle = 90 - np.rad2deg(np.arctan2(self.grain_plot.p2[1] - self.grain_plot.p1[1], + self.grain_plot.p2[0] - self.grain_plot.p1[0])) + if line_angle > 180: + line_angle -= 180 + elif line_angle < 0: + line_angle += 180 + + line_angle -= self.correction_angle + + # Two decimal places + points = [float("{:.2f}".format(point)) for point in self.grain_plot.p1 + self.grain_plot.p2] + line_angle = float("{:.2f}".format(line_angle)) + + # Save drawn line to the DIC grain + self.selected_dic_grain.points_list.append([points, line_angle, -1]) + + # Group lines and redraw + self.group_lines() + self.redraw_line() + + def group_lines(self, + grain: 'defdap.hrdic.Grain' = None): + """ + Group the lines drawn in the current grain item using a mean shift algorithm, + save the average angle and then detect the active slip planes. + + groups_list is a list of line groups: [id, angle, [slip plane id], [angular deviation] + + Parameters + ---------- + grain + Grain for which to group the slip lines. + + """ + + if grain is None: + grain = self.selected_dic_grain + + if grain.points_list == []: + grain.groups_list = [] + else: + for i, line in enumerate(grain.points_list): + angle = line[1] + if i == 0: + line[2] = 0 # Make group 0 for first detected angle + grain.groups_list = [[0, angle, 0, 0, 0]] + next_group = 1 + else: # If there is more that one angle + if np.any(np.abs(np.array([x[1] for x in grain.groups_list]) - angle) < 10): + # If within +- 5 degrees of existing group, set that as the group + group = np.argmin(np.abs(np.array([x[1] for x in grain.groups_list]) - angle)) + grain.points_list[i][2] = group + new_average = float('{0:.2f}'.format( + np.average([x[1] for x in grain.points_list if x[2] == group]))) + grain.groups_list[group][1] = new_average + else: + # Make new group and set + grain.groups_list.append([next_group, angle, 0, 0, 0]) + line[2] = next_group + next_group += 1 + + # Detect active slip systems in each group + for group in grain.groups_list: + active_planes = [] + deviation = [] + experimental_angle = group[1] + for idx, theoretical_angle in enumerate(np.rad2deg(grain.ebsd_grain.slip_trace_angles)): + if theoretical_angle - 5 < experimental_angle < theoretical_angle + 5: + active_planes.append(idx) + deviation.append(float('{0:.2f}'.format(experimental_angle - theoretical_angle))) + group[2] = active_planes + group[3] = deviation + + def clear_all_lines(self, + event, + plot): + """ Clear all lines in a given grain. + + """ + + self.selected_dic_grain.points_list = [] + self.selected_dic_grain.groups_list = [] + self.redraw() + + def remove_line(self, + event: int, + plot): + """ Remove single line [runs after submitting a text box]. + + Parameters + ---------- + event + Line ID to remove. + + """ + # Remove single line + del self.selected_dic_grain.points_list[int(event)] + self.group_lines() + self.redraw() + + def redraw(self): + """Draw items which need to be redrawn when changing grain ID. + + """ + + # Plot max shear for grain + self.max_shear_axis.clear() + self.grain_plot = self.selected_dic_map[self.grain_id].plot_max_shear( + fig=self.plot.fig, ax=self.max_shear_axis, vmax=self.vmax, plot_colour_bar=False, plot_scale_bar=True) + + # Draw unit cell + self.unit_cell_axis.clear() + self.selected_ebsd_grain.plot_unit_cell(fig=self.plot.fig, ax=self.unit_cell_axis) + + # Write grain info text + self.grain_info_axis.clear() + self.grain_info_axis.axis('off') + grain_info_text = 'Grain ID: {0} / {1}\n'.format(self.grain_id, len(self.selected_dic_map.grains) - 1) + grain_info_text += 'Min: {0:.2f} % Mean:{1:.2f} % Max: {2:.2f} %'.format( + np.min(self.selected_dic_grain.data.max_shear) * 100, + np.mean(self.selected_dic_grain.data.max_shear) * 100, + np.max(self.selected_dic_grain.data.max_shear) * 100) + self.plot.add_text(self.grain_info_axis, 0, 1, grain_info_text, va='top', ha='left', + fontsize=10, fontfamily='monospace') + + # Detect lines + self.plot.add_event_handler('button_press_event', lambda e, p: self.grain_plot.line_slice(e, p)) + self.plot.add_event_handler('button_release_event', lambda e, p: self.grain_plot.line_slice(e, p)) + + self.redraw_line() + + def redraw_line(self): + """ + Draw items which need to be redrawn when adding a line. + + """ + # Write lines text and draw lines + title_text = 'List of lines' + lines_text = 'ID x0 y0 x1 y1 Angle Group\n' \ + '-----------------------------------------\n' + if self.selected_dic_grain.points_list: + for idx, points in enumerate(self.selected_dic_grain.points_list): + lines_text += '{0:<3} {1:<5.0f} {2:<5.0f} {3:<5.0f} {4:<5.0f} {5:<7.1f} {6:<5}\n'.format( + idx, *points[0], points[1], points[2]) + self.grain_plot.add_arrow(start_end=points[0], clear_previous=False, persistent=True, label=idx) + + self.line_info_axis.clear() + self.line_info_axis.axis('off') + self.plot.add_text(self.line_info_axis, 0, 1, title_text, va='top', + fontsize=10, fontfamily='monospace', weight='bold') + self.plot.add_text(self.line_info_axis, 0, 0.9, lines_text, va='top', + fontsize=10, fontfamily='monospace') + + # Write groups info text + title_text = 'List of groups' + + groupsTxt = 'ID Av. Angle System Dev RDR\n' \ + '----------------------------------------\n' + if self.selected_dic_grain.groups_list: + for idx, group in enumerate(self.selected_dic_grain.groups_list): + groupsTxt += '{0:<3} {1:<10.1f} {2:<7} {3:<12} {4:.2f}\n'.format( + idx, + group[1], + ','.join([str(np.round(i, 1)) for i in group[2]]), + ','.join([str(np.round(i, 1)) for i in group[3]]), + group[4]) + + self.groups_info_axis.clear() + self.groups_info_axis.axis('off') + self.plot.add_text(self.groups_info_axis, 0, 1, title_text, va='top', fontsize=10, fontfamily='monospace', + weight='bold') + self.plot.add_text(self.groups_info_axis, 0, 0.9, groupsTxt, va='top', fontsize=10, fontfamily='monospace') + + # Draw slip traces + self.slip_trace_axis.clear() + self.slip_trace_axis.set_aspect('equal', 'box') + slipPlot = GrainPlot(fig=self.plot.fig, + calling_grain=self.selected_dic_map[self.grain_id], ax=self.slip_trace_axis) + traces = slipPlot.add_slip_traces(top_only=True) + self.slip_trace_axis.axis('off') + + # Draw slip bands + bands = [elem[1] for elem in self.selected_dic_grain.groups_list] + if self.selected_dic_grain.groups_list != None: + slipPlot.add_slip_bands(top_only=True, angles=list(np.deg2rad(bands))) + + def run_rdr_group(self, + event: int, + plot): + """ Run RDR on a specified group, upon submitting a text box. + + Parameters + ---------- + event + Group ID specified from text box. + + """ + # Run RDR for group of lines + if event != '': + self.calc_rdr(grain=self.selected_dic_grain, group=int(event)) + self.rdr_group_text_box.set_val('') + + def batch_run_sta(self, + event, + plot): + """ Run slip trace analysis on all grains which hve slip trace lines drawn. + + """ + + # Print header + print("Grain\tEul1\tEul2\tEul3\tMaxSF\tGroup\tAngle\tSystem\tDev\tRDR") + + # Print information for each grain + for idx, grain in enumerate(self.selected_dic_map): + if grain.points_list != []: + for group in grain.groups_list: + maxSF = np.max([item for sublist in grain.ebsd_grain.average_schmid_factors for item in sublist]) + eulers = self.selected_ebsd_grain.ref_ori.euler_angles() * 180 / np.pi + text = '{0}\t{1:.1f}\t{2:.1f}\t{3:.1f}\t{4:.3f}\t'.format( + idx, eulers[0], eulers[1], eulers[2], maxSF) + text += '{0}\t{1:.1f}\t{2}\t{3}\t{4:.2f}'.format( + group[0], group[1], group[2], np.round(group[3], 3), group[4]) + print(text) + + def calc_rdr(self, + grain, + group: int, + show_plot: bool = True): + """ Calculates the relative displacement ratio for a given grain and group. + + Parameters + ---------- + grain + DIC grain to run RDR on. + group + group ID to run RDR on. + show_plot + if True, show plot window. + + """ + + u_list, v_list, x_list, y_list = [], [], [], [] + + # Get all lines belonging to group + point_array = np.array(grain.points_list, dtype=object) + points = list(point_array[:, 0][point_array[:, 2] == group]) + angle = grain.groups_list[group][1] + + # Lookup deviation from (0,0) for 3 points along line perpendicular to slip line (x_new,y_new) + x_new = np.array([[-1, 0, 1], [1, 0, -1], [0, 0, 0], [1, 0, -1], [-1, 0, 1]])[int(np.round(angle / 45, 0))] + y_new = np.array([[0, 0, 0], [-1, 0, 1], [-1, 0, 1], [1, 0, -1], [0, 0, 0]])[int(np.round(angle / 45, 0))] + tuples = list(zip(x_new, y_new)) + + # Allow increasing line length from default 3 to any odd number + num = np.arange(0, int((self.rdr_line_length - 1) / 2)) + 1 + coordinateOffsets = np.unique(np.array([np.array(tuples)*i for i in num]).reshape(-1, 2), axis=0) + + # For each slip trace line + for point in points: + x0, y0, x1, y1 = point + + # Calculate positions for each pixel along slip trace line + x, y = skimage_line(int(x0), int(y0), int(x1), int(y1)) + + # Get x and y coordinates for points to be samples for RDR + xmap = np.array(x).T[:, None] + coordinateOffsets[:,0] + self.selected_dic_grain.extreme_coords[0] + ymap = np.array(y).T[:, None] + coordinateOffsets[:,1] + self.selected_dic_grain.extreme_coords[1] + + x_list.extend(xmap - self.selected_dic_grain.extreme_coords[0]) + y_list.extend(ymap - self.selected_dic_grain.extreme_coords[1]) + + # Get u and v values at each coordinate + u = self.selected_dic_map.crop(self.selected_dic_map.data.displacement[0])[ymap, xmap] + v = self.selected_dic_map.crop(self.selected_dic_map.data.displacement[1])[ymap, xmap] + + # Subtract mean u and v value for each row + u_list.extend(u - np.mean(u, axis=1)[:, None]) + v_list.extend(v - np.mean(v, axis=1)[:, None]) + + # Linear regression of ucentered against vcentered + lin_reg_result = linregress(x=np.array(v_list).flatten(), y=np.array(u_list).flatten()) + + # Save measured RDR + grain.groups_list[group][4] = lin_reg_result.slope + + if show_plot: + self.plot_rdr(grain, group, u_list, v_list, x_list, y_list, lin_reg_result) + + def plot_rdr(self, + grain, + group: int, + u_list: List[float], + v_list: List[float], + x_list: List[List[int]], + y_list: List[List[int]], + lin_reg_result: List): + """ + Plot rdr figure, including location of perpendicular lines and scatter plot of ucentered vs vcentered. + + Parameters + ---------- + grain + DIC grain to plot. + group + Group ID to plot. + u_list + List of ucentered values. + v_list + List of vcentered values. + x_list + List of all x values. + y_list + List of all y values. + lin_reg_result + Results from linear regression of ucentered vs vcentered + {slope, intercept, rvalue, pvalue, stderr}. + + """ + + # Draw window and axes + self.rdr_plot = Plot(ax=None, make_interactive=True, title='RDR Calculation', figsize=(15, 8)) + self.rdr_plot.ax.axis('off') + self.rdr_plot.grain_axis = self.rdr_plot.add_axes((0.05, 0.5, 0.3, 0.45)) + self.rdr_plot.text_axis = self.rdr_plot.add_axes((0.37, 0.05, 0.3, 0.85)) + self.rdr_plot.text_axis.axis('off') + self.rdr_plot.number_line_axis = self.rdr_plot.add_axes((0.64, 0.05, 0.3, 0.83)) + self.rdr_plot.number_line_axis.axis('off') + self.rdr_plot.plot_axis = self.rdr_plot.add_axes((0.05, 0.1, 0.3, 0.35)) + + # Draw grain plot + self.rdr_plot.grainPlot = self.selected_dic_grain.plot_grain_data( + grain_data=self.selected_dic_grain.data.max_shear, + fig=self.rdr_plot.fig, + ax=self.rdr_plot.grain_axis, + plot_colour_bar=False, + plot_scale_bar=True) + + self.rdr_plot.grainPlot.add_colour_bar(label='Effective Shear Strain', fraction=0.046, pad=0.04) + + # Draw all points + self.rdr_plot.grain_axis.plot(x_list, y_list, 'rx', lw=0.5) + for xlist, ylist in zip(x_list, y_list): + self.rdr_plot.grain_axis.plot(xlist, ylist, '-', lw=1) + + # Generate scatter plot + slope = lin_reg_result.slope + r_value = lin_reg_result.rvalue + intercept = lin_reg_result.intercept + std_err = lin_reg_result.stderr + + self.rdr_plot.plot_axis.scatter(x=v_list, y=u_list, marker='x', lw=1) + self.rdr_plot.plot_axis.plot( + [np.min(v_list), np.max(v_list)], + [slope * np.min(v_list) + intercept, slope * np.max(v_list) + intercept], '-') + self.rdr_plot.plot_axis.set_xlabel('v-centered') + self.rdr_plot.plot_axis.set_ylabel('u-centered') + self.rdr_plot.add_text(self.rdr_plot.plot_axis, 0.95, 0.01, + 'Slope = {0:.3f} ± {1:.3f}\nR-squared = {2:.3f}\nn={3}' + .format(slope, std_err, r_value ** 2, len(u_list)), + va='bottom', ha='right', + transform=self.rdr_plot.plot_axis.transAxes, fontsize=10, fontfamily='monospace'); + + self.selected_ebsd_grain.calc_slip_traces() + self.selected_ebsd_grain.calc_rdr() + + if self.selected_ebsd_grain.average_schmid_factors is None: + raise Exception("Run 'calc_average_grain_schmid_factors' first") + + # Write grain info + eulers = np.rad2deg(self.selected_ebsd_grain.ref_ori.euler_angles()) + text = 'Average angle: {0:.2f}\n'.format(grain.groups_list[group][1]) + text += 'Eulers: {0:.1f} {1:.1f} {2:.1f}\n\n'.format(eulers[0], eulers[1], eulers[2]) + + self.rdr_plot.add_text(self.rdr_plot.text_axis, 0.15, 1, text, fontsize=10, va='top', fontfamily='monospace') + + # Write slip system info + offset = 0 + + # Loop over groups of slip systems with same slip plane + for i, slip_system_group in enumerate(self.selected_ebsd_grain.phase.slip_systems): + slip_trace_angle = np.rad2deg(self.selected_ebsd_grain.slip_trace_angles[i]) + text = "Plane: {0:s} Angle: {1:.1f}\n".format(slip_system_group[0].slip_plane_label, + slip_trace_angle) + + # Then loop over individual slip systems + for j, slip_system in enumerate(slip_system_group): + schmid_factor = self.selected_ebsd_grain.average_schmid_factors[i][j] + + text = text + " {0:s} SF: {1:.3f} RDR: {2:.3f}\n".format( + slip_system.slip_dir_label, schmid_factor, self.selected_ebsd_grain.rdr[i][j]) + + if i in grain.groups_list[group][2]: + self.rdr_plot.add_text(self.rdr_plot.text_axis, 0.15, 0.9 - offset, text, va='top', + weight='bold', fontsize=10) + else: + self.rdr_plot.add_text(self.rdr_plot.text_axis, 0.15, 0.9 - offset, text, va='top', + fontsize=10) + + offset += 0.0275 * text.count('\n') + + # Finf all unique rdr values + unique_rdrs = set([item for sublist in self.selected_ebsd_grain.rdr for item in sublist]) + + # Plot number line + self.rdr_plot.number_line_axis.axvline(x=0, ymin=-20, ymax=20, c='k') + + # Theoretical values as blue points + self.rdr_plot.number_line_axis.plot(np.zeros(len(unique_rdrs)), list(unique_rdrs), + 'bo', label='Theoretical RDR values') + + # Measured values as red points + self.rdr_plot.number_line_axis.plot([0], slope, 'ro', label='Measured RDR value') + self.rdr_plot.add_text(self.rdr_plot.number_line_axis, -0.002, slope, '{0:.3f}'.format(float(slope)), + fontfamily='monospace', horizontalalignment='right', verticalalignment='center') + + self.rdr_plot.number_line_axis.legend(bbox_to_anchor=(1.15, 1.05)) + + # Label rdrs by slip system on number line + for unique_rdr in list(unique_rdrs): + if (unique_rdr > slope - 1.5) & (unique_rdr < slope + 1.5): + # Add number to the left of point + self.rdr_plot.add_text(self.rdr_plot.number_line_axis, -0.002, unique_rdr, + '{0:.3f}'.format(float(unique_rdr)), + fontfamily='monospace', horizontalalignment='right', verticalalignment='center') + + # Go through all planes and directions and add to string if they have the rdr from above loop + txt = '' + for i, slip_system_group in enumerate(self.selected_ebsd_grain.phase.slip_systems): + # Then loop over individual slip systems + for j, slip_system in enumerate(slip_system_group): + rdr = self.selected_ebsd_grain.rdr[i][j] + if rdr == unique_rdr: + txt += str('{0} {1} '.format(slip_system.slip_plane_label, slip_system.slip_dir_label)) + + self.rdr_plot.add_text(self.rdr_plot.number_line_axis, 0.002, unique_rdr - 0.01, + txt) + + self.rdr_plot.number_line_axis.set_ylim(slope - 1.5, slope + 1.5) + self.rdr_plot.number_line_axis.set_xlim(-0.01, 0.05) + + def update_filename(self, + event: str, + plot): + """ Update class variable filename, based on text input from textbox handler. + + event: + Text in textbox. + + """ + + self.filename = event + + def save_file(self, + event, + plot): + """ Save a file which contains definitions of slip lines drawn in grains + [(x0, y0, x1, y1), angle, groupID] + and groups of lines, defined by an average angle and identified sip plane + [groupID, angle, [slip plane id(s)], [angular deviation(s)]] + + """ + + with open(self.selected_dic_map.path + str(self.filename), 'w') as file: + file.write('# This is a file generated by defdap which contains ') + file.write('definitions of slip lines drawn in grains by grainInspector\n') + file.write('# [(x0, y0, x1, y1), angle, groupID]\n') + file.write('# and groups of lines, defined by an average angle and identified sip plane\n') + file.write('# [groupID, angle, [slip plane id], [angular deviation]\n\n') + + for i, grain in enumerate(self.selected_dic_map): + if grain.points_list != []: + file.write('Grain {0}\n'.format(i)) + file.write('{0} Lines\n'.format(len(grain.points_list))) + for point in grain.points_list: + file.write(str(point) + '\n') + file.write('{0} Groups\n'.format(len(grain.groups_list))) + for group in grain.groups_list: + file.write(str(group) + '\n') + file.write('\n') + + def load_file(self, + event, + plot): + """ Load a file which contains definitions of slip lines drawn in grains + [(x0, y0, x1, y1), angle, groupID] + and groups of lines, defined by an average angle and identified sip plane + [groupID, angle, [slip plane id(s)], [angular deviation(s)]] + + """ + + with open(self.selected_dic_map.path + str(self.filename), 'r') as file: + lines = file.readlines() + + # Parse file and make list of + # [start index, grain ID, number of lines, number of groups] + index_list = [] + for i, line in enumerate(lines): + if line[0] != '#' and len(line) > 1: + if 'Grain' in line: + grain_id = int(line.split(' ')[-1]) + start_index = i + if 'Lines' in line: + num_lines = int(line.split(' ')[0]) + if 'Groups' in line: + num_groups = int(line.split(' ')[0]) + index_list.append([start_index, grain_id, num_lines, num_groups]) + + # Write data from file into grain + for start_index, grain_id, num_lines, num_groups in index_list: + start_index_lines = start_index + 2 + grain_points = lines[start_index_lines:start_index_lines + num_lines] + for point in grain_points: + self.selected_dic_map[grain_id].points_list.append(ast.literal_eval(point.split('\\')[0])) + + start_index_groups = start_index + 3 + num_lines + grain_groups = lines[start_index_groups:start_index_groups + num_groups] + for group in grain_groups: + self.selected_dic_map[grain_id].groups_list.append(ast.literal_eval(group.split('\\')[0])) + + self.redraw() diff --git a/build/lib/defdap/plotting.py b/build/lib/defdap/plotting.py new file mode 100644 index 0000000..c32730b --- /dev/null +++ b/build/lib/defdap/plotting.py @@ -0,0 +1,1527 @@ +# Copyright 2023 Mechanics of Microstructures Group +# at The University of Manchester +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import matplotlib as mpl +import matplotlib.pyplot as plt + +from matplotlib.widgets import Button, TextBox +from matplotlib.collections import LineCollection +from matplotlib_scalebar.scalebar import ScaleBar +from mpl_toolkits.mplot3d.art3d import Poly3DCollection +from mpl_toolkits.mplot3d import Axes3D +from matplotlib.ticker import FuncFormatter + +from skimage import morphology as mph + +from defdap import defaults +from defdap import quat + +# TODO: add plot parameter to add to current figure + + +class Plot(object): + """ Class used for creating and manipulating plots. + + """ + + def __init__(self, ax=None, ax_params={}, fig=None, make_interactive=False, + title=None, **kwargs): + self.interactive = make_interactive + if make_interactive: + if fig is not None and ax is not None: + self.fig = fig + self.ax = ax + else: + # self.fig, self.ax = plt.subplots(**kwargs) + self.fig = plt.figure(**kwargs) + self.ax = self.fig.add_subplot(111, **ax_params) + self.btn_store = [] + self.txt_store = [] + self.txt_box_store = [] + self.p1 = [] + self.p2 = [] + + else: + self.fig = fig + # TODO: flag for new figure + if ax is None: + self.fig = plt.figure(**kwargs) + self.ax = self.fig.add_subplot(111, **ax_params) + else: + self.ax = ax + self.colour_bar = None + self.arrow = None + + if title is not None: + self.set_title(title) + + def check_interactive(self): + """Checks if current plot is interactive. + + Raises + ------- + Exception + If plot is not interactive + + """ + if not self.interactive: + raise Exception("Plot must be interactive") + + def add_event_handler(self, eventName, eventHandler): + self.check_interactive() + + self.fig.canvas.mpl_connect(eventName, lambda e: eventHandler(e, self)) + + def add_axes(self, loc, proj='2d'): + """Add axis to current plot + + Parameters + ---------- + loc + Location of axis. + proj : str, {2d, 3d} + 2D or 3D projection. + + Returns + ------- + matplotlib.Axes.axes + + """ + if proj == '2d': + return self.fig.add_axes(loc) + if proj == '3d': + return Axes3D(self.fig, rect=loc, proj_type='ortho', azim=270, elev=90) + + def add_button(self, label, click_handler, loc=(0.8, 0.0, 0.1, 0.07), **kwargs): + """Add a button to the plot. + + Parameters + ---------- + label : str + Label for the button. + click_handler + Click handler to assign. + loc : list(float), len 4 + Left, bottom, width, height. + kwargs + All other arguments passed to :class:`matplotlib.widgets.Button`. + + """ + self.check_interactive() + btn_ax = self.fig.add_axes(loc) + btn = Button(btn_ax, label, **kwargs) + btn.on_clicked(lambda e: click_handler(e, self)) + + self.btn_store.append(btn) + + def add_text_box(self, label, submit_handler=None, change_handler=None, loc=(0.8, 0.0, 0.1, 0.07), **kwargs): + """Add a text box to the plot. + + Parameters + ---------- + label : str + Label for the button. + submit_handler + Submit handler to assign. + change_handler + Change handler to assign. + loc : list(float), len 4 + Left, bottom, width, height. + kwargs + All other arguments passed to :class:`matplotlib.widgets.TextBox`. + + Returns + ------- + matplotlotlib.widgets.TextBox + + """ + self.check_interactive() + txt_box_ax = self.fig.add_axes(loc) + txt_box = TextBox(txt_box_ax, label, **kwargs) + if submit_handler != None: + txt_box.on_submit(lambda e: submit_handler(e, self)) + if change_handler != None: + txt_box.on_text_change(lambda e: change_handler(e, self)) + + self.txt_box_store.append(txt_box) + + return txt_box + + def add_text(self, ax, x, y, txt, **kwargs): + """Add text to the plot. + + Parameters + ---------- + ax : matplotlib.axes.Axes + Matplotlib axis to plot on. + x : float + x position. + y : float + y position. + txt : str + Text to write onto the plot. + + kwargs : + All other arguments passed to :func:`matplotlib.pyplot.text`. + + """ + txt = ax.text(x, y, txt, **kwargs) + self.txt_store.append(txt) + + def add_arrow(self, start_end, persistent=False, clear_previous=True, label=None): + """Add arrow to grain plot. + + Parameters + ---------- + start_end: 4-tuple + Starting (x, y), Ending (x, y). + persistent : + If persistent, do not clear arrow with clearPrev. + clear_previous : + Clear all non-persistent arrows. + label + Label to place near arrow. + + """ + + arrow_params = { + 'xy': start_end[0:2], # Arrow start coordinates + 'xycoords': 'data', + 'xytext': start_end[2:4], # Arrow end coordinates + 'textcoords': 'data', + 'arrowprops': dict(arrowstyle="<-", connectionstyle="arc3", + color='red', alpha=0.7, linewidth=2, + shrinkA=0, shrinkB=0) + } + + # If persisent, add the arrow onto the plot directly + if persistent: + self.ax.annotate("", **arrow_params) + + # If not persistent, save a reference so that it can be removed later + if not persistent: + if clear_previous and (self.arrow is not None): self.arrow.remove() + if None not in start_end: + self.arrow = self.ax.annotate("", **arrow_params) + + # Add a label if specified + if label is not None: + self.ax.annotate(label, xy=start_end[2:4], xycoords='data', + xytext=(15, 15), textcoords='offset pixels', + c='red', fontsize=14, fontweight='bold') + + def set_size(self, size): + """Set size of plot. + + Parameters + ---------- + size : float, float + Width and height in inches. + + """ + self.fig.set_size_inches(size[0], size[1], forward=True) + + def set_title(self, txt): + """Set title of plot. + + Parameters + ---------- + txt : str + Title to set. + + """ + if self.fig.canvas.manager is not None: + self.fig.canvas.manager.set_window_title(txt) + + def line_slice(self, event, plot, action=None): + """ Catch click and drag then draw an arrow. + + Parameters + ---------- + event : + Click event. + plot : defdap.plotting.Plot + Plot to capture clicks from. + action + Further action to perform. + + Examples + ---------- + To use, add a click and release event handler to your plot, pointing to this function: + + >>> plot.add_event_handler('button_press_event',lambda e, p: line_slice(e, p)) + >>> plot.add_event_handler('button_release_event', lambda e, p: line_slice(e, p)) + + """ + # check if click was on the map + if event.inaxes is not self.ax: + return + + if event.name == 'button_press_event': + self.p1 = (event.xdata, event.ydata) # save 1st point + elif event.name == 'button_release_event': + self.p2 = (event.xdata, event.ydata) # save 2nd point + self.add_arrow(start_end=(self.p1[0], self.p1[1], self.p2[0], self.p2[1])) + self.fig.canvas.draw_idle() + + if action is not None: + action(plot=self, startEnd=(self.p1[0], self.p1[1], self.p2[0], self.p2[1])) + + @property + def exists(self): + self.check_interactive() + + return plt.fignum_exists(self.fig.number) + + def clear(self): + """Clear plot. + + """ + self.check_interactive() + + if self.colour_bar is not None: + self.colour_bar.remove() + self.colour_bar = None + self.ax.clear() + self.draw() + + def draw(self): + """Draw plot + + """ + self.fig.canvas.draw() + + +class MapPlot(Plot): + """ Class for creating a map plot. + + """ + + def __init__(self, calling_map, fig=None, ax=None, ax_params={}, + make_interactive=False, **kwargs): + """Initialise a map plot. + + Parameters + ---------- + calling_map : Map + DIC or EBSD map which called this plot. + fig : matplotlib.figure.Figure + Matplotlib figure to plot on + ax : matplotlib.axes.Axes + Matplotlib axis to plot on + ax_params : + Passed to defdap.plotting.Plot as ax_params. + make_interactive : bool, optional + If true, make interactive + kwargs + Other arguments passed to :class:`defdap.plotting.Plot`. + """ + super(MapPlot, self).__init__( + ax, ax_params=ax_params, fig=fig, make_interactive=make_interactive, + **kwargs + ) + + self.calling_map = calling_map + self.img_layers = [] + self.highlights_layer_id = None + self.points_layer_ids = [] + + self.ax.set_xticks([]) + self.ax.set_yticks([]) + + def add_map(self, map_data, vmin=None, vmax=None, cmap='viridis', **kwargs): + """Add a map to a plot. + + Parameters + ---------- + map_data : numpy.ndarray + Map data to plot. + vmin : float + Minimum value for the colour scale. + vmax : float + Maximum value for the colour scale. + cmap + Colour map. + kwargs + Other arguments are passed to :func:`matplotlib.pyplot.imshow`. + + Returns + ------- + matplotlib.image.AxesImage + + """ + img = self.ax.imshow(map_data, vmin=vmin, vmax=vmax, + interpolation='None', cmap=cmap, **kwargs) + self.draw() + + self.img_layers.append(img) + + return img + + def add_colour_bar(self, label, layer=0, **kwargs): + """Add a colour bar to plot. + + Parameters + ---------- + label : str + Label for the colour bar. + layer : int + Layer ID. + kwargs + Other arguments are passed to :func:`matplotlib.pyplot.colorbar`. + + """ + img = self.img_layers[layer] + self.colour_bar = plt.colorbar(img, ax=self.ax, label=label, **kwargs) + + def add_scale_bar(self, scale=None): + """Add scale bar to plot. + + Parameters + ---------- + scale : float + Size of a pixel in microns. + + """ + if scale is None: + scale = self.calling_map.scale + self.ax.add_artist(ScaleBar(scale * 1e-6)) + + def add_grain_boundaries(self, kind="pixel", boundaries=None, colour=None, + dilate=False, draw=True, **kwargs): + """Add grain boundaries to the plot. + + Parameters + ---------- + kind : str, {"pixel", "line"} + Type of boundaries to plot, either a boundary image or a + collection of line segments. + boundaries : various, optional + Boundaries to plot, either a boundary image or a list of pairs + of coordinates representing the start and end of each boundary + segment. If not provided the boundaries are loaded from the + calling map. + + boundaries : various, defdap.ebsd.BoundarySet + Boundaries to plot. If not provided the boundaries are loaded from + the calling map. + + colour : various + One of: + - Colour of all boundaries as a string (only option pixel kind) + - Colour of all boundaries as RGBA tuple + - List of values to represent colour of each line relative to a + `norm` and `cmap` + dilate : bool + If true, dilate the grain boundaries. + kwargs + If line kind then other arguments are passed to + :func:`matplotlib.collections.LineCollection`. + + Returns + ------- + Various : + matplotlib.image.AxesImage if type is pixel + + """ + if colour is None: + colour = "white" + + if boundaries is None: + boundaries = self.calling_map.data.grain_boundaries + + if kind == "line": + if isinstance(colour, str): + colour = mpl.colors.to_rgba(colour) + + if len(colour) == len(boundaries.lines): + colour_array = colour + colour_lc = None + elif len(colour) == 4: + colour_array = None + colour_lc = colour + else: + ValueError('Issue with passed colour') + + lc = LineCollection(boundaries.lines, colors=colour_lc, **kwargs) + lc.set_array(colour_array) + img = self.ax.add_collection(lc) + + else: + boundaries_image = boundaries.image.astype(int) + + if dilate: + boundaries_image = mph.binary_dilation(boundaries_image) + + # create colourmap for boundaries going from transparent to + # opaque of the given colour + boundaries_cmap = mpl.colors.LinearSegmentedColormap.from_list( + 'my_cmap', ['white', colour], 256 + ) + boundaries_cmap._init() + boundaries_cmap._lut[:, -1] = np.linspace(0, 1, boundaries_cmap.N + 3) + + img = self.ax.imshow(boundaries_image, cmap=boundaries_cmap, + interpolation='None', vmin=0, vmax=1) + + if draw: + self.draw() + self.img_layers.append(img) + return img + + def add_grain_highlights(self, grain_ids, grain_colours=None, alpha=None, + new_layer=False): + """Highlight grains in the plot. + + Parameters + ---------- + grain_ids : list + List of grain IDs to highlight. + grain_colours : + Colour to use for grain highlight. + alpha : float + Alpha (transparency) to use for grain highlight. + new_layer : bool + If true, make a new layer in img_layers. + + Returns + ------- + matplotlib.image.AxesImage + + """ + if grain_colours is None: + grain_colours = ['white'] + if alpha is None: + alpha = self.calling_map.highlight_alpha + + outline = np.zeros(self.calling_map.shape, dtype=int) + for i, grainId in enumerate(grain_ids, start=1): + if i > len(grain_colours): + i = len(grain_colours) + + # outline of highlighted grain + grain = self.calling_map.grains[grainId] + grainOutline = grain.grain_outline(bg=0, fg=i) + x0, y0, xmax, ymax = grain.extreme_coords + + # add to highlight image + outline[y0:ymax + 1, x0:xmax + 1] += grainOutline + + # Custom colour map where 0 is transparent white for bg and + # then a patch for each grain colour + grain_colours.insert(0, 'white') + highlightsCmap = mpl.colors.ListedColormap(grain_colours) + highlightsCmap._init() + alphaMap = np.full(highlightsCmap.N + 3, alpha) + alphaMap[0] = 0 + highlightsCmap._lut[:, -1] = alphaMap + + if self.highlights_layer_id is None or new_layer: + img = self.ax.imshow(outline, interpolation='none', + cmap=highlightsCmap) + if self.highlights_layer_id is None: + self.highlights_layer_id = len(self.img_layers) + self.img_layers.append(img) + else: + img = self.img_layers[self.highlights_layer_id] + img.set_data(outline) + img.set_cmap(highlightsCmap) + img.autoscale() + + self.draw() + + return img + + def add_grain_numbers(self, fontsize=10, **kwargs): + """Add grain numbers to a map. + + Parameters + ---------- + fontsize : float + Font size. + kwargs + Pass other arguments to :func:`matplotlib.pyplot.text`. + + """ + for grain_id, grain in enumerate(self.calling_map): + x_centre, y_centre = grain.centre_coords(centre_type="com", + grain_coords=False) + + self.ax.text(x_centre, y_centre, grain_id, + fontsize=fontsize, **kwargs) + self.draw() + + def add_legend(self, values, labels, layer=0, **kwargs): + """Add a legend to a map. + + Parameters + ---------- + values : list + Values to find colour patched for. + labels : list + Labels to assign to values. + layer : int + Image layer to generate legend for. + kwargs + Pass other arguments to :func:`matplotlib.pyplot.legend`. + + """ + # Find colour values for given values + img = self.img_layers[layer] + colors = [img.cmap(img.norm(value)) for value in values] + + # Get colour patches for each phase and make legend + patches = [mpl.patches.Patch( + color=colors[i], label=labels[i] + ) for i in range(len(values))] + + self.ax.legend(handles=patches, **kwargs) + + def add_points(self, x, y, update_layer=None, **kwargs): + """Add points to plot. + + Parameters + ---------- + x : list of float + x coordinates + y : list of float + y coordinates + update_layer : int, optional + Layer to place points on + kwargs + Other arguments passed to :func:`matplotlib.pyplot.scatter`. + + """ + x, y = np.array(x), np.array(y) + if len(self.points_layer_ids) == 0 or update_layer is None: + points = self.ax.scatter(x, y, **kwargs) + self.points_layer_ids.append(len(self.img_layers)) + self.img_layers.append(points) + else: + points = self.img_layers[self.points_layer_ids[update_layer]] + points.set_offsets(np.hstack((x[:, np.newaxis], y[:, np.newaxis]))) + + self.draw() + + return points + + @classmethod + def create( + cls, calling_map, map_data, + fig=None, fig_params={}, ax=None, ax_params={}, + plot=None, make_interactive=False, + plot_colour_bar=False, vmin=None, vmax=None, cmap=None, clabel="", + plot_gbs=False, dilate_boundaries=False, boundary_colour=None, + plot_scale_bar=False, scale=None, + highlight_grains=None, highlight_colours=None, highlight_alpha=None, + **kwargs + ): + """Create a plot for a map. + + Parameters + ---------- + calling_map : base.Map + DIC or EBSD map which called this plot. + map_data : numpy.ndarray + Data to be plotted. + fig : matplotlib.figure.Figure + Matplotlib figure to plot on. + fig_params : + Passed to defdap.plotting.Plot. + ax : matplotlib.axes.Axes + Matplotlib axis to plot on. + ax_params : + Passed to defdap.plotting.Plot as ax_params. + plot : defdap.plotting.Plot + If none, use current plot. + make_interactive : + If true, make plot interactive + plot_colour_bar : bool + If true, plot a colour bar next to the map. + vmin : float, optional + Minimum value for the colour scale. + vmax : float, optional + Maximum value for the colour scale. + cmap : str + Colour map. + clabel : str + Label for the colour bar. + plot_gbs : bool + If true, plot the grain boundaries on the map. + dilate_boundaries : bool + If true, dilate the grain boundaries. + boundary_colour : str + Colour to use for the grain boundaries. + plot_scale_bar : bool + If true, plot a scale bar in the map. + scale : float + Size of pixel in microns. + highlight_grains : list(int) + List of grain IDs to highlight. + highlight_colours : str + Colour to highlight grains. + highlight_alpha : float + Alpha (transparency) by which to highlight grains. + kwargs : + All other arguments passed to :func:`defdap.plotting.MapPlot.add_map` + + Returns + ------- + defdap.plotting.MapPlot + + """ + if plot is None: + plot = cls(calling_map, fig=fig, ax=ax, ax_params=ax_params, + make_interactive=make_interactive, **fig_params) + + if map_data is not None: + plot.add_map(map_data, cmap=cmap, vmin=vmin, vmax=vmax, **kwargs) + + if plot_colour_bar: + plot.add_colour_bar(clabel) + + if plot_gbs: + plot.add_grain_boundaries( + colour=boundary_colour, dilate=dilate_boundaries, kind=plot_gbs + ) + + if highlight_grains is not None: + plot.add_grain_highlights( + highlight_grains, + grain_colours=highlight_colours, alpha=highlight_alpha + ) + + if plot_scale_bar: + plot.add_scale_bar(scale=scale) + + return plot + + +class GrainPlot(Plot): + """ Class for creating a map for a grain. + + """ + + def __init__(self, calling_grain, fig=None, ax=None, ax_params={}, + make_interactive=False, **kwargs): + super(GrainPlot, self).__init__( + ax, ax_params=ax_params, fig=fig, make_interactive=make_interactive, + **kwargs + ) + + self.calling_grain = calling_grain + self.img_layers = [] + + self.ax.set_xticks([]) + self.ax.set_yticks([]) + + def addMap(self, map_data, vmin=None, vmax=None, cmap='viridis', **kwargs): + """Add a map to a grain plot. + + Parameters + ---------- + map_data : numpy.ndarray + Grain data to plot + vmin : float + Minimum value for the colour scale. + vmax : float + Maximum value for the colour scale. + cmap + Colour map to use. + kwargs + Other arguments are passed to :func:`matplotlib.pyplot.imshow`. + + Returns + ------- + matplotlib.image.AxesImage + + """ + img = self.ax.imshow(map_data, vmin=vmin, vmax=vmax, + interpolation='None', cmap=cmap, **kwargs) + self.draw() + + self.img_layers.append(img) + + return img + + def add_colour_bar(self, label, layer=0, **kwargs): + """Add colour bar to grain plot. + + Parameters + ---------- + label : str + Label to add to colour bar. + layer : int + Layer on which to add colourbar. + kwargs + Other arguments passed to :func:`matplotlib.pyplot.colorbar`. + + """ + img = self.img_layers[layer] + self.colour_bar = plt.colorbar(img, ax=self.ax, label=label, **kwargs) + + def add_scale_bar(self, scale=None): + """Add scale bar to grain plot. + + Parameters + ---------- + scale : float + Size of pixel in micron. + + """ + if scale is None: + scale = self.calling_grain.owner_map.scale * 1e-6 + self.ax.add_artist(ScaleBar(scale)) + + def add_traces(self, angles, colours, top_only=False, pos=None, **kwargs): + """Add slip trace angles to grain plot. Illustrated by lines + crossing through central pivot point to create a circle. + + Parameters + ---------- + angles : list + Angles of slip traces. + colours : list + Colours to plot. + top_only : bool, optional + If true, plot only a semicircle instead of a circle. + pos : tuple + Position of slip traces. + kwargs + Other arguments are passed to :func:`matplotlib.pyplot.quiver` + + """ + if pos is None: + pos = self.calling_grain.centre_coords() + traces = np.array((-np.sin(angles), np.cos(angles))) + + # When plotting top half only, move all 'traces' to +ve y + # and set the pivot to be in the tail instead of centre + if top_only: + pivot = 'tail' + for idx, (x, y) in enumerate(zip(traces[0], traces[1])): + if x < 0 and y < 0: + traces[0][idx] *= -1 + traces[1][idx] *= -1 + self.ax.set_ylim(pos[1] - 0.001, pos[1] + 0.1) + self.ax.set_xlim(pos[0] - 0.1, pos[0] + 0.1) + else: + pivot = 'middle' + + for i, trace in enumerate(traces.T): + colour = colours[len(colours) - 1] if i >= len(colours) else colours[i] + self.ax.quiver( + pos[0], pos[1], + trace[0], trace[1], + scale=1, pivot=pivot, + color=colour, headwidth=1, + headlength=0, **kwargs + ) + self.draw() + + def add_slip_traces(self, top_only=False, colours=None, pos=None, **kwargs): + """Add slip traces to plot, based on the calling grain's slip systems. + + Parameters + ---------- + colours : list + Colours to plot. + top_only : bool, optional + If true, plot only a semicircle instead of a circle. + pos : tuple + Position of slip traces. + kwargs + Other arguments are passed to :func:`matplotlib.pyplot.quiver` + + """ + + if colours is None: + colours = self.calling_grain.ebsd_grain.phase.slip_trace_colours + slip_trace_angles = self.calling_grain.slip_traces + + self.add_traces(slip_trace_angles, colours, top_only, pos=pos, **kwargs) + + def add_slip_bands(self, top_only=False, grain_map_data=None, angles=None, pos=None, + thres=None, min_dist=None, **kwargs): + """Add lines representing slip bands detected by Radon transform + in :func:`~defdap.hrdic.grain.calc_slip_bands`. + + Parameters + ---------- + top_only : bool, optional + If true, plot only a semicircle instead of a circle. + grain_map_data : + Map data to pass to :func:`~defdap.hrdic.Grain.calc_slip_bands`. + angles : list(float), optional + List of angles to plot, otherwise, use angles + detected in :func:`~defdap.hrdic.Grain.calc_slip_bands`. + pos : tuple + Position in which to plot slip traces. + thres : float + Threshold to use in :func:`~defdap.hrdic.Grain.calc_slip_bands`. + min_dist : + Minimum angle between bands in :func:`~defdap.hrdic.Grain.calc_slip_bands`. + kwargs + Other arguments are passed to :func:`matplotlib.pyplot.quiver`. + + """ + + if angles is None: + slip_band_angles = self.calling_grain.calc_slip_bands(grain_map_data, + thres=thres, + min_dist=min_dist) + else: + slip_band_angles = angles + + self.add_traces(slip_band_angles, ["black"], top_only, pos=pos, **kwargs) + + @classmethod + def create( + cls, calling_grain, map_data, + fig=None, fig_params={}, ax=None, ax_params={}, + plot=None, make_interactive=False, + plot_colour_bar=False, vmin=None, vmax=None, cmap=None, clabel="", + plot_scale_bar=False, scale=None, + plot_slip_traces=False, plot_slip_bands=False, **kwargs + ): + """Create grain plot. + + Parameters + ---------- + calling_grain : base.Grain + DIC or EBSD grain which called this plot. + map_data : + Data to be plotted. + fig : matplotlib.figure.Figure + Matplotlib figure to plot on. + fig_params : + Passed to defdap.plotting.Plot. + ax : matplotlib.axes.Axes + Matplotlib axis to plot on. + ax_params : + Passed to defdap.plotting.Plot as ax_params. + plot : defdap.plotting.Plot + If none, use current plot. + make_interactive : + If true, make plot interactive + plot_colour_bar : bool + If true, plot a colour bar next to the map. + vmin : float + Minimum value for the colour scale. + vmax : float + Maximum value for the colour scale. + cmap : + Colour map. + clabel : str + Label for the colour bar. + plot_scale_bar : bool + If true, plot a scale bar in the map. + scale : float + Size of pizel in microns. + plot_slip_traces : bool + If true, plot slip traces with :func:`~defdap.plotting.GrainPlot.add_slip_traces` + plot_slip_bands : bool + If true, plot slip traces with :func:`~defdap.plotting.GrainPlot.add_slip_bands` + kwargs : + All other arguments passed to :func:`defdap.plotting.GrainPlot.add_map` + + Returns + ------- + defdap.plotting.GrainPlot + + """ + if plot is None: + plot = cls(calling_grain, fig=fig, ax=ax, ax_params=ax_params, + make_interactive=make_interactive, **fig_params) + plot.addMap(map_data, cmap=cmap, vmin=vmin, vmax=vmax, **kwargs) + + if plot_colour_bar: + plot.add_colour_bar(clabel) + + if plot_scale_bar: + plot.add_scale_bar(scale=scale) + + if plot_slip_traces: + plot.add_slip_traces() + + if plot_slip_bands: + plot.add_slip_bands(grain_map_data=map_data) + + return plot + + +class PolePlot(Plot): + """ Class for creating an inverse pole figure plot. + + """ + + def __init__(self, plot_type, crystal_sym, projection=None, + fig=None, ax=None, ax_params={}, make_interactive=False, + **kwargs): + super(PolePlot, self).__init__( + ax, ax_params=ax_params, fig=fig, make_interactive=make_interactive, + **kwargs) + + self.plot_type = plot_type + self.crystal_sym = crystal_sym + self.projection = self._validateProjection(projection) + + self.img_layers = [] + + self.add_axis() + + def add_axis(self): + """Draw axes on the IPF based on crystal symmetry. + + Raises + ------- + NotImplementedError + If a crystal type other than 'cubic' or 'hexagonal' are selected. + + """ + if self.plot_type == "IPF" and self.crystal_sym == "cubic": + # line between [001] and [111] + self.add_line([0, 0, 1], [1, 1, 1], c='k', lw=2) + + # line between [001] and [101] + self.add_line([0, 0, 1], [1, 0, 1], c='k', lw=2) + + # line between [101] and [111] + self.add_line([1, 0, 1], [1, 1, 1], c='k', lw=2) + + # label poles + self.label_point([0, 0, 1], '001', + pad_y=-0.005, va='top', ha='center', fontsize=12) + self.label_point([1, 0, 1], '101', + pad_y=-0.005, va='top', ha='center', fontsize=12) + self.label_point([1, 1, 1], '111', + pad_y=0.005, va='bottom', ha='center', fontsize=12) + + elif self.plot_type == "IPF" and self.crystal_sym == "hexagonal": + # line between [0001] and [10-10] ([001] and [210]) + # converted to cubic axes + self.add_line([0, 0, 1], [np.sqrt(3), 1, 0], c='k', lw=2) + + # line between [0001] and [2-1-10] ([001] and [100]) + self.add_line([0, 0, 1], [1, 0, 0], c='k', lw=2) + + # line between [2-1-10] and [10-10] ([100] and [210]) + self.add_line([1, 0, 0], [np.sqrt(3), 1, 0], c='k', lw=2) + + # label poles + self.label_point([0, 0, 1], '0001', + pad_y=-0.012, va='top', ha='center', fontsize=12) + self.label_point([1, 0, 0], r'$2\bar{1}\bar{1}0$', + pad_y=-0.012, va='top', ha='center', fontsize=12) + self.label_point([np.sqrt(3), 1, 0], r'$10\bar{1}0$', + pad_y=0.009, va='bottom', ha='center', fontsize=12) + + else: + raise NotImplementedError("Only works for cubic and hexagonal.") + + self.ax.axis('equal') + self.ax.axis('off') + + def add_line(self, start_point, end_point, plot_syms=False, res=100, **kwargs): + """Draw lines on the IPF plot. + + Parameters + ---------- + start_point : numpy.ndarray + Start point in crystal coordinates (i.e. [0,0,1]). + end_point : numpy.ndarray + End point in crystal coordinates, (i.e. [1,0,0]). + plot_syms : bool, optional + If true, plot all symmetrically equivelant points. + res : int + Number of points within each line to plot. + kwargs + All other arguments are passed to :func:`matplotlib.pyplot.plot`. + + """ + lines = [(start_point, end_point)] + if plot_syms: + for symm in quat.Quat.sym_eqv(self.crystal_sym)[1:]: + start_point_symm = symm.transform_vector(start_point).astype(int) + end_point_symm = symm.transform_vector(end_point).astype(int) + + if start_point_symm[2] < 0: + start_point_symm *= -1 + if end_point_symm[2] < 0: + end_point_symm *= -1 + + lines.append((start_point_symm, end_point_symm)) + + line_points = np.zeros((3, res), dtype=float) + for line in lines: + for i in range(3): + if line[0][i] == line[1][i]: + line_points[i] = np.full(res, line[0][i]) + else: + line_points[i] = np.linspace(line[0][i], line[1][i], res) + + xp, yp = self.projection(line_points[0], line_points[1], line_points[2]) + self.ax.plot(xp, yp, **kwargs) + + def label_point(self, point, label, pad_x=0, pad_y=0, **kwargs): + """Place a label near a coordinate in the pole plot. + + Parameters + ---------- + point : tuple + (x, y) coordinate to place text. + label : str + Text to use in label. + pad_x : int, optional + Pad added to x coordinate. + pad_y : int, optional + Pad added to y coordinate. + kwargs + Other arguments are passed to :func:`matplotlib.axes.Axes.text`. + + """ + xp, yp = self.projection(*point) + self.ax.text(xp + pad_x, yp + pad_y, label, **kwargs) + + def add_points(self, alpha_ang, beta_ang, marker_colour=None, marker_size=None, **kwargs): + """Add a point to the pole plot. + + Parameters + ---------- + alpha_ang + Inclination angle to plot. + beta_ang + Azimuthal angle (around z axis from x in anticlockwise as per ISO) to plot. + marker_colour : str or list(str), optional + Colour of marker. If two specified, then the point will have two + semicircles of different colour. + marker_size : float + Size of marker. + kwargs + Other arguments are passed to :func:`matplotlib.axes.Axes.scatter`. + + Raises + ------- + Exception + If more than two colours are specified + + """ + # project onto equatorial plane + xp, yp = self.projection(alpha_ang, beta_ang) + + # plot poles + # plot markers with 'half-and-half' colour + if type(marker_colour) is str: + marker_colour = [marker_colour] + + if marker_colour is None: + points = self.ax.scatter(xp, yp, **kwargs) + self.img_layers.append(points) + elif len(marker_colour) == 2: + pos = (xp, yp) + r1 = 0.5 + r2 = r1 + 0.5 + marker_size = np.sqrt(marker_size) + + x = [0] + np.cos(np.linspace(0, 2 * np.pi * r1, 10)).tolist() + y = [0] + np.sin(np.linspace(0, 2 * np.pi * r1, 10)).tolist() + xy1 = list(zip(x, y)) + + x = [0] + np.cos(np.linspace(2 * np.pi * r1, 2 * np.pi * r2, 10)).tolist() + y = [0] + np.sin(np.linspace(2 * np.pi * r1, 2 * np.pi * r2, 10)).tolist() + xy2 = list(zip(x, y)) + + points = self.ax.scatter( + pos[0], pos[1], marker=(xy1, 0), + s=marker_size, c=marker_colour[0], **kwargs + ) + self.img_layers.append(points) + points = self.ax.scatter( + pos[0], pos[1], marker=(xy2, 0), + s=marker_size, c=marker_colour[1], **kwargs + ) + self.img_layers.append(points) + else: + raise Exception("specify one colour for solid markers or list two for 'half and half'") + + def add_colour_bar(self, label, layer=0, **kwargs): + """Add a colour bar to the pole plot. + + Parameters + ---------- + label : str + Label to place next to colour bar. + layer : int + Layer number to add the colour bar to. + kwargs + Other argument are passed to :func:`matplotlib.pyplot.colorbar`. + + """ + img = self.img_layers[layer] + self.colour_bar = plt.colorbar(img, ax=self.ax, label=label, **kwargs) + + def add_legend(self, label='Grain area (μm$^2$)', number=6, layer=0, scaling=1, **kwargs): + """Add a marker size legend to the pole plot. + + Parameters + ---------- + label : str + Label to place next to legend. + number : + Number of markers to plot in legend. + layer : int + Layer number to add the colour bar to. + scaling : float + Scaling applied to the data. + kwargs + Other argument are passed to :func:`matplotlib.pyplot.legend`. + + """ + img = self.img_layers[layer] + self.legend = plt.legend(*img.legend_elements("sizes", num=number, + func=lambda s: s / scaling), title=label, **kwargs) + + @staticmethod + def _validateProjection(projection_in, validate_default=False): + if validate_default: + default_projection = None + else: + default_projection = PolePlot._validateProjection( + defaults['pole_projection'], validate_default=True + ) + + if projection_in is None: + projection = default_projection + + elif type(projection_in) is str: + projection_name = projection_in.replace(" ", "").lower() + if projection_name in ["lambert", "equalarea"]: + projection = PolePlot.lambert_project + elif projection_name in ["stereographic", "stereo", "equalangle"]: + projection = PolePlot.stereo_project + else: + print("Unknown projection name, using default") + projection = default_projection + + elif callable(projection_in): + projection = projection_in + + else: + print("Unknown projection, using default") + projection = default_projection + + if projection is None: + raise ValueError("Problem with default projection.") + + return projection + + @staticmethod + def stereo_project(*args): + """Stereographic projection of pole direction or pair of polar angles. + + Parameters + ---------- + args : numpy.ndarray, len 2 or 3 + 2 arguments for polar angles or 3 arguments for pole directions. + + Returns + ------- + float, float + x coordinate, y coordinate + + Raises + ------- + Exception + If input array has incorrect length + + """ + if len(args) == 3: + alpha, beta = quat.Quat.polar_angles(args[0], args[1], args[2]) + elif len(args) == 2: + alpha, beta = args + else: + raise Exception("3 arguments for pole directions and 2 for polar angles.") + + alpha_comp = np.tan(alpha / 2) + xp = alpha_comp * np.cos(beta) + yp = alpha_comp * np.sin(beta) + + return xp, yp + + @staticmethod + def lambert_project(*args): + """Lambert Projection of pole direction or pair of polar angles. + + Parameters + ---------- + args : numpy.ndarray, len 2 or 3 + 2 arguments for polar angles or 3 arguments for pole directions. + + Returns + ------- + float, float + x coordinate, y coordinate + + Raises + ------- + Exception + If input array has incorrect length + + """ + if len(args) == 3: + alpha, beta = quat.Quat.polar_angles(args[0], args[1], args[2]) + elif len(args) == 2: + alpha, beta = args + else: + raise Exception("3 arguments for pole directions and 2 for polar angles.") + + alpha_comp = np.sqrt(2 * (1 - np.cos(alpha))) + xp = alpha_comp * np.cos(beta) + yp = alpha_comp * np.sin(beta) + + return xp, yp + + +class HistPlot(Plot): + """ Class for creating a histogram. + + """ + + def __init__(self, plot_type="scatter", axes_type="linear", density=True, fig=None, + ax=None, ax_params={}, make_interactive=False, **kwargs): + """Initialise a histogram plot + + Parameters + ---------- + plot_type: str, {'scatter', 'bar', 'step'} + Type of plot to use + axes_type : str, {'linear', 'logx', 'logy', 'loglog', 'None'}, optional + If 'log' is specified, logarithmic scale is used. + density : + If true, histogram is normalised such that the integral sums to 1. + fig : matplotlib.figure.Figure + Matplotlib figure to plot on. + ax : matplotlib.axes.Axes + Matplotlib axis to plot on. + ax_params : + Passed to defdap.plotting.Plot as ax_params. + make_interactive : bool + If true, make the plot interactive. + kwargs + Other arguments are passed to :class:`defdap.plotting.Plot` + + """ + super(HistPlot, self).__init__( + ax, ax_params=ax_params, fig=fig, make_interactive=make_interactive, + **kwargs + ) + + axes_type = axes_type.lower() + if axes_type in ["linear", "logy", "logx", "loglog"]: + self.axes_type = axes_type + else: + raise ValueError("plotType must be linear or log.") + + if plot_type in ['scatter', 'bar', 'step']: + self.plot_type = plot_type + else: + raise ValueError("plotType must be scatter, bar or step.") + + self.density = bool(density) + + # set y-axis label + yLabel = "Normalised frequency" if self.density else "Frequency" + self.ax.set_ylabel(yLabel) + + # set axes to linear or log as appropriate and set to be numbers as opposed to scientific notation + if self.axes_type == 'logx' or self.axes_type == 'loglog': + self.ax.set_xscale("log") + self.ax.xaxis.set_major_formatter(FuncFormatter(lambda y, _: '{:.5g}'.format(y))) + if self.axes_type == 'logy' or self.axes_type == 'loglog': + self.ax.set_yscale("log") + self.ax.yaxis.set_major_formatter(FuncFormatter(lambda y, _: '{:.5g}'.format(y))) + + def add_hist(self, hist_data, bins=100, range=None, line='o', + label=None, **kwargs): + """Add a histogram to the current plot + + Parameters + ---------- + hist_data : numpy.ndarray + Data to be used in the histogram. + bins : int + Number of bins to use for histogram. + range : tuple or None, optional + The lower and upper range of the bins + line : str, optional + Marker or line type to be used. + label : str, optional + Label to use for data (used for legend). + kwargs + Other arguments are passed to :func:`numpy.histogram` + + """ + + # Generate the x bins with appropriate spaceing for linear or log + if self.axes_type == 'logx' or self.axes_type == 'loglog': + bin_list = np.logspace(np.log10(range[0]), np.log10(range[1]), bins) + else: + bin_list = np.linspace(range[0], range[1], bins) + + if self.plot_type == 'scatter': + # Generate the histogram data and plot as a scatter plot + hist = np.histogram(hist_data.flatten(), bins=bin_list, density=self.density) + y_vals = hist[0] + x_vals = 0.5 * (hist[1][1:] + hist[1][:-1]) + + self.ax.plot(x_vals, y_vals, line, label=label, **kwargs) + + else: + # Plot as a matplotlib histogram + self.ax.hist(hist_data.flatten(), bins=bin_list, histtype=self.plot_type, + density=self.density, label=label, **kwargs) + + def add_legend(self, **kwargs): + """Add legend to histogram. + + Parameters + ---------- + kwargs + All arguments passed to :func:`matplotlib.axes.Axes.legend`. + + """ + self.ax.legend(**kwargs) + + @classmethod + def create( + cls, hist_data, fig=None, fig_params={}, ax=None, ax_params={}, + plot=None, make_interactive=False, + plot_type="scatter", axes_type="linear", density=True, bins=10, range=None, + line='o', label=None, **kwargs + ): + """Create a histogram plot. + + Parameters + ---------- + hist_data : numpy.ndarray + Data to be used in the histogram. + fig : matplotlib.figure.Figure + Matplotlib figure to plot on. + fig_params : + Passed to defdap.plotting.Plot. + ax : matplotlib.axes.Axes + Matplotlib axis to plot on. + ax_params : + Passed to defdap.plotting.Plot as ax_params. + plot : defdap.plotting.HistPlot + Plot where histgram is created. If none, a new plot is created. + make_interactive : bool, optional + If true, make plot interactive. + plot_type: str, {'scatter', 'bar', 'barfilled', 'step'} + Type of plot to use + axes_type : str, {'linear', 'logx', 'logy', 'loglog', 'None'}, optional + If 'log' is specified, logarithmic scale is used. + density : + If true, histogram is normalised such that the integral sums to 1. + bins : int + Number of bins to use for histogram. + range : tuple or None, optional + The lower and upper range of the bins + line : str, optional + Marker or line type to be used. + label : str, optional + Label to use for data (is used for legend). + kwargs + Other arguments are passed to :func:`defdap.plotting.HistPlot.add_hist` + + Returns + ------- + defdap.plotting.HistPlot + + """ + if plot is None: + plot = cls(axesType=axes_type, plotType=plot_type, density=density, fig=fig, ax=ax, + ax_params=ax_params, make_interactive=make_interactive, + **fig_params) + plot.add_hist(hist_data, bins=bins, range=range, line=line, + label=label, **kwargs) + + return plot + + +class CrystalPlot(Plot): + """ Class for creating a 3D plot for plotting unit cells. + + """ + + def __init__(self, fig=None, ax=None, ax_params={}, + make_interactive=False, **kwargs): + """Initialise a 3D plot. + + Parameters + ---------- + fig : matplotlib.pyplot.Figure + Figure to plot to. + ax : matplotlib.pyplot.Axis + Axis to plot to. + ax_params + Passed to defdap.plotting.Plot as ax_params. + make_interactive : bool, optional + If true, make plot interactive. + kwargs + Other arguments are passed to :class:`defdap.plotting.Plot`. + + """ + # Set default plot parameters then update with input + fig_params = { + 'figsize': (6, 6) + } + fig_params.update(kwargs) + ax_params_default = { + 'projection': '3d', + 'proj_type': 'ortho' + } + ax_params_default.update(ax_params) + ax_params = ax_params_default + + super(CrystalPlot, self).__init__( + ax, ax_params=ax_params, fig=fig, make_interactive=make_interactive, + **fig_params + ) + + def add_verts(self, verts, **kwargs): + """Plots planes, defined by the vertices provided. + + Parameters + ---------- + verts : list + List of vertices. + kwargs + Other arguments are passed to :class:`matplotlib.collections.PolyCollection`. + + """ + # Set default plot parameters then update with any input + plot_params = { + 'alpha': 0.6, + 'facecolor': '0.8', + 'linewidths': 3, + 'edgecolor': 'k' + } + plot_params.update(kwargs) + + # Add list of planes defined by given vertices to the 3D plot + pc = Poly3DCollection(verts, **plot_params) + self.ax.add_collection3d(pc) diff --git a/build/lib/defdap/quat.py b/build/lib/defdap/quat.py new file mode 100644 index 0000000..9fda81d --- /dev/null +++ b/build/lib/defdap/quat.py @@ -0,0 +1,1098 @@ +# Copyright 2023 Mechanics of Microstructures Group +# at The University of Manchester +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np + +from defdap import plotting + +from typing import Union, Tuple, List, Optional + + +class Quat(object): + """Class used to define and perform operations on quaternions. These + are interpreted in the passive sense. + + """ + __slots__ = ['quat_coef'] + + def __init__(self, *args, allow_southern: Optional[bool] = False) -> None: + """ + Construct a Quat object from 4 quat coefficients or an array of + quat coefficients. + + Parameters + ---------- + *args + Variable length argument list. + allow_southern + if False, move quat to northern hemisphere. + + """ + # construct with array of quat coefficients + if len(args) == 1: + if len(args[0]) != 4: + raise TypeError("Arrays input must have 4 elements") + self.quat_coef = np.array(args[0], dtype=float) + + # construct with quat coefficients + elif len(args) == 4: + self.quat_coef = np.array(args, dtype=float) + + else: + raise TypeError("Incorrect argument length. Input should be " + "an array of quat coefficients or idividual " + "quat coefficients") + + # move to northern hemisphere + if not allow_southern and self.quat_coef[0] < 0: + self.quat_coef = self.quat_coef * -1 + + @classmethod + def from_euler_angles(cls, ph1: float, phi: float, ph2: float) -> 'Quat': + """Create a quat object from 3 Bunge euler angles. + + Parameters + ---------- + ph1 + First Euler angle, rotation around Z in radians. + phi + Second Euler angle, rotation around new X in radians. + ph2 + Third Euler angle, rotation around new Z in radians. + + Returns + ------- + defdap.quat.Quat + Initialised Quat object. + + """ + # calculate quat coefficients + quat_coef = np.array([ + np.cos(phi / 2.0) * np.cos((ph1 + ph2) / 2.0), + -np.sin(phi / 2.0) * np.cos((ph1 - ph2) / 2.0), + -np.sin(phi / 2.0) * np.sin((ph1 - ph2) / 2.0), + -np.cos(phi / 2.0) * np.sin((ph1 + ph2) / 2.0) + ], dtype=float) + + # call constructor + return cls(quat_coef) + + @classmethod + def from_axis_angle(cls, axis: np.ndarray, angle: float) -> 'Quat': + """Create a quat object from a rotation around an axis. This + creates a quaternion to represent the passive rotation (-ve axis). + + Parameters + ---------- + axis + Axis that the rotation is applied around. + angle + Magnitude of rotation in radians. + + Returns + ------- + defdap.quat.Quat + Initialised Quat object. + + """ + # normalise the axis vector + axis = np.array(axis) + axis = axis / np.sqrt(np.dot(axis, axis)) + # calculate quat coefficients + quat_coef = np.zeros(4, dtype=float) + quat_coef[0] = np.cos(angle / 2) + quat_coef[1:4] = -np.sin(angle / 2) * axis + + # call constructor + return cls(quat_coef) + + def euler_angles(self) -> np.ndarray: + """Calculate the Euler angle representation for this rotation. + + Returns + ------- + eulers : numpy.ndarray, shape 3 + Bunge euler angles (in radians). + + References + ---------- + Melcher A. et al., 'Conversion of EBSD data by a quaternion + based algorithm to be used for grain structure simulations', + Technische Mechanik, 30(4)401 – 413 + + Rowenhorst D. et al., 'Consistent representations of and + conversions between 3D rotations', + Model. Simul. Mater. Sci. Eng., 23(8) + + """ + eulers = np.empty(3, dtype=float) + + q = self.quat_coef + q03 = q[0]**2 + q[3]**2 + q12 = q[1]**2 + q[2]**2 + chi = np.sqrt(q03 * q12) + + if chi == 0 and q12 == 0: + eulers[0] = np.arctan2(-2 * q[0] * q[3], q[0]**2 - q[3]**2) + eulers[1] = 0 + eulers[2] = 0 + + elif chi == 0 and q03 == 0: + eulers[0] = np.arctan2(2 * q[1] * q[2], q[1]**2 - q[2]**2) + eulers[1] = np.pi + eulers[2] = 0 + + else: + cos_ph1 = (-q[0] * q[1] - q[2] * q[3]) / chi + sin_ph1 = (-q[0] * q[2] + q[1] * q[3]) / chi + + cos_phi = q[0]**2 + q[3]**2 - q[1]**2 - q[2]**2 + sin_phi = 2 * chi + + cos_ph2 = (-q[0] * q[1] + q[2] * q[3]) / chi + sin_ph2 = (q[1] * q[3] + q[0] * q[2]) / chi + + eulers[0] = np.arctan2(sin_ph1, cos_ph1) + eulers[1] = np.arctan2(sin_phi, cos_phi) + eulers[2] = np.arctan2(sin_ph2, cos_ph2) + + if eulers[0] < 0: + eulers[0] += 2 * np.pi + if eulers[2] < 0: + eulers[2] += 2 * np.pi + + return eulers + + def rot_matrix(self) -> np.ndarray: + """Calculate the rotation matrix representation for this rotation. + + Returns + ------- + rot_matrix : numpy.ndarray, shape (3, 3) + Rotation matrix. + + References + ---------- + Melcher A. et al., 'Conversion of EBSD data by a quaternion + based algorithm to be used for grain structure simulations', + Technische Mechanik, 30(4)401 – 413 + + Rowenhorst D. et al., 'Consistent representations of and + conversions between 3D rotations', + Model. Simul. Mater. Sci. Eng., 23(8) + + """ + rot_matrix = np.empty((3, 3), dtype=float) + + q = self.quat_coef + qbar = q[0]**2 - q[1]**2 - q[2]**2 - q[3]**2 + + rot_matrix[0, 0] = qbar + 2 * q[1]**2 + rot_matrix[0, 1] = 2 * (q[1] * q[2] - q[0] * q[3]) + rot_matrix[0, 2] = 2 * (q[1] * q[3] + q[0] * q[2]) + + rot_matrix[1, 0] = 2 * (q[1] * q[2] + q[0] * q[3]) + rot_matrix[1, 1] = qbar + 2 * q[2]**2 + rot_matrix[1, 2] = 2 * (q[2] * q[3] - q[0] * q[1]) + + rot_matrix[2, 0] = 2 * (q[1] * q[3] - q[0] * q[2]) + rot_matrix[2, 1] = 2 * (q[2] * q[3] + q[0] * q[1]) + rot_matrix[2, 2] = qbar + 2 * q[3]**2 + + return rot_matrix + + # show components when the quat is printed + def __repr__(self) -> str: + return "[{:.4f}, {:.4f}, {:.4f}, {:.4f}]".format(*self.quat_coef) + + def __str__(self) -> str: + return self.__repr__() + + def __eq__(self, right: 'Quat') -> bool: + return (isinstance(right, type(self)) and + self.quat_coef.tolist() == right.quat_coef.tolist()) + + def __hash__(self) -> int: + return hash(tuple(self.quat_coef.tolist())) + + def _plotIPF( + self, + direction: np.ndarray, + sym_group: str, + **kwargs + ) -> 'plotting.PolePlot': + Quat.plot_ipf([self], direction, sym_group, **kwargs) + + # overload * operator for quaternion product and vector product + def __mul__(self, right: 'Quat', allow_southern: bool = False) -> 'Quat': + if isinstance(right, type(self)): # another quat + new_quat_coef = np.zeros(4, dtype=float) + new_quat_coef[0] = ( + self.quat_coef[0] * right.quat_coef[0] - + np.dot(self.quat_coef[1:4], right.quat_coef[1:4]) + ) + new_quat_coef[1:4] = ( + self.quat_coef[0] * right.quat_coef[1:4] + + right.quat_coef[0] * self.quat_coef[1:4] + + np.cross(self.quat_coef[1:4], right.quat_coef[1:4]) + ) + return Quat(new_quat_coef, allow_southern=allow_southern) + + raise TypeError("{:} - {:}".format(type(self), type(right))) + + def dot(self, right: 'Quat') -> float: + """ Calculate dot product between two quaternions. + + Parameters + ---------- + right + Right hand quaternion. + + Returns + ------- + float + Dot product. + + """ + if isinstance(right, type(self)): + return np.dot(self.quat_coef, right.quat_coef) + raise TypeError() + + # overload + operator + def __add__(self, right: 'Quat') -> 'Quat': + if isinstance(right, type(self)): + return Quat(self.quat_coef + right.quat_coef) + raise TypeError() + + # overload += operator + def __iadd__(self, right: 'Quat') -> 'Quat': + if isinstance(right, type(self)): + self.quat_coef += right.quat_coef + return self + raise TypeError() + + # allow array like setting/getting of components + def __getitem__(self, key: int) -> float: + return self.quat_coef[key] + + def __setitem__(self, key: int, value: float) -> None: + self.quat_coef[key] = value + + def norm(self) -> float: + """Calculate the norm of the quaternion. + + Returns + ------- + float + Norm of the quaternion. + + """ + return np.sqrt(np.dot(self.quat_coef[0:4], self.quat_coef[0:4])) + + def normalise(self) -> 'Quat': + """ Normalise the quaternion (turn it into an unit quaternion). + + Returns + ------- + defdap.quat.Quat + Normalised quaternion. + + """ + self.quat_coef /= self.norm() + return + + # also the inverse if this is a unit quaternion + @property + def conjugate(self) -> 'Quat': + """Calculate the conjugate of the quaternion. + + Returns + ------- + defdap.quat.Quat + Conjugate of quaternion. + + """ + return Quat(self[0], -self[1], -self[2], -self[3]) + + def transform_vector( + self, + vector: Union[Tuple, List, np.ndarray] + ) -> np.ndarray: + """ + Transforms vector by the quaternion. For passive EBSD quaterions + this is a transformation from sample space to crystal space. + Perform on conjugate of quaternion for crystal to sample. For a + quaternion representing a passive rotation from CS1 to CS2 and a + fixed vector V defined in CS1, this gives the coordinates + of V in CS2. + + Parameters + ---------- + vector : numpy.ndarray, shape 3 or equivalent + Vector to transform. + + Returns + ------- + numpy.ndarray, shape 3 + Transformed vector. + + """ + if not isinstance(vector, (np.ndarray, list, tuple)): + raise TypeError("Vector must be a tuple, list or numpy array.") + if np.array(vector).shape != (3,): + raise TypeError("Vector must be length 3.") + + vector_quat = Quat(0, *vector) + vector_quat_transformed = self.__mul__( + vector_quat.__mul__(self.conjugate, allow_southern=True), + allow_southern=True + ) + return vector_quat_transformed.quat_coef[1:4] + + def mis_ori( + self, + right: 'Quat', + sym_group: str, + return_quat: Optional[int] = 0 + ) -> Tuple[float, 'Quat']: + """ + Calculate misorientation angle between 2 orientations taking + into account the symmetries of the crystal structure. + Angle is 2*arccos(output). + + Parameters + ---------- + right + Orientation to find misorientation to. + sym_group + Crystal type (cubic, hexagonal). + return_quat + What to return: 0 for minimum misorientation, 1 for + symmetric equivalent with minimum misorientation, 2 for both. + + Returns + ------- + float + Minimum misorientation. + defdap.quat.Quat + Symmetric equivalent orientation with minimum misorientation. + + """ + if isinstance(right, type(self)): + # looking for max of this as it is cos of misorientation angle + min_mis_ori = 0 + # loop over symmetrically equivalent orientations + for sym in Quat.sym_eqv(sym_group): + quat_sym = sym * right + current_mis_ori = abs(self.dot(quat_sym)) + if current_mis_ori > min_mis_ori: # keep if misorientation lower + min_mis_ori = current_mis_ori + min_quat_sym = quat_sym + + if return_quat == 1: + return min_quat_sym + elif return_quat == 2: + return min_mis_ori, min_quat_sym + else: + return min_mis_ori + raise TypeError("Input must be a quaternion.") + + def mis_ori_axis(self, right: 'Quat') -> np.ndarray: + """ + Calculate misorientation axis between 2 orientations. This + does not consider symmetries of the crystal structure. + + Parameters + ---------- + right : defdap.quat.Quat + Orientation to find misorientation axis to. + + Returns + ------- + numpy.ndarray, shape 3 + Axis of misorientation. + + """ + if isinstance(right, type(self)): + Dq = right * self.conjugate + Dq = Dq.quat_coef + mis_ori_axis = 2 * Dq[1:4] * np.arccos(Dq[0]) / np.sqrt(1 - Dq[0]**2) + return mis_ori_axis + raise TypeError("Input must be a quaternion.") + + def plot_ipf( + self, + direction: np.ndarray, + sym_group: str, + projection: Optional[str] = None, + plot: Optional['plotting.Plot'] = None, + fig: Optional['matplotlib.figure.Figure'] = None, + ax: Optional['matplotlib.axes.Axes'] = None, + plot_colour_bar: Optional[bool] = False, + clabel: Optional[str] = "", + make_interactive: Optional[bool] = False, + marker_colour: Optional[Union[List[str], str]] = None, + marker_size: Optional[float] = 40, + **kwargs + ) -> 'plotting.PolePlot': + """ + Plot IPF of orientation, with relation to specified sample direction. + + Parameters + ---------- + direction + Sample reference direction for IPF. + sym_group + Crystal type (cubic, hexagonal). + projection + Projection to use. Either string (stereographic or lambert) + or a function. + plot + Defdap plot to plot on. + fig + Figure to plot on, if not provided the current + active axis is used. + ax + Axis to plot on, if not provided the current + active axis is used. + make_interactive + If true, make the plot interactive. + plot_colour_bar : bool + If true, plot a colour bar next to the map. + clabel : str + Label for the colour bar. + marker_colour: str or list of str + Colour of markers (only used for half and half colouring, + otherwise use argument c). + marker_size + Size of markers (only used for half and half colouring, + otherwise use argument s). + kwargs + All other arguments are passed to :func:`defdap.plotting.PolePlot.add_points`. + + """ + plot_params = {'marker': '+'} + plot_params.update(kwargs) + + # Works as an instance or static method on a list of Quats + if isinstance(self, Quat): + quats = [self] + else: + quats = self + + alpha_fund, beta_fund = Quat.calc_fund_dirs(quats, direction, sym_group) + + if plot is None: + plot = plotting.PolePlot( + "IPF", sym_group, projection=projection, + ax=ax, fig=fig, make_interactive=make_interactive + ) + plot.add_points( + alpha_fund, beta_fund, + marker_colour=marker_colour, marker_size=marker_size, + **plot_params + ) + + if plot_colour_bar: + plot.add_colour_bar(clabel) + + return plot + + def plot_unit_cell( + self, + crystal_structure: 'defdap.crystal.CrystalStructure', + OI: Optional[bool] = True, + plot: Optional['plotting.CrystalPlot'] = None, + fig: Optional['matplotlib.figure.Figure'] = None, + ax: Optional['matplotlib.axes.Axes'] = None, + make_interactive: Optional[bool] = False, + **kwargs + ) -> 'plotting.CrystalPlot': + """Plots a unit cell. + + Parameters + ---------- + crystal_structure + Crystal structure. + OI + True if using oxford instruments system. + plot + Plot object to plot to. + fig + Figure to plot on, if not provided the current active axis is used. + ax + Axis to plot on, if not provided the current active axis is used. + make_interactive + True to make the plot interactive. + kwargs + All other arguments are passed to :func:`defdap.plotting.CrystalPlot.add_verts`. + + """ + # Set default plot parameters then update with any input + plot_params = {} + plot_params.update(kwargs) + + # TODO: most of this should be moved to either the crystal or + # plotting module + + vert = crystal_structure.vertices + faces = crystal_structure.faces + + if crystal_structure.name == 'hexagonal': + sz_fac = 0.18 + if OI: + # Add 30 degrees to phi2 for OI + eulerAngles = self.euler_angles() + eulerAngles[2] += np.pi / 6 + gg = Quat.from_euler_angles(*eulerAngles).rot_matrix().T + else: + gg = self.rot_matrix().T + + elif crystal_structure.name == 'cubic': + sz_fac = 0.25 + gg = self.rot_matrix().T + + # Rotate the lattice cell points + pts = np.matmul(gg, vert.T).T * sz_fac + + # Plot unit cell + planes = [] + for face in faces: + planes.append(pts[face, :]) + + if plot is None: + plot = plotting.CrystalPlot( + ax=ax, fig=fig, make_interactive=make_interactive + ) + + plot.ax.set_xlim3d(-0.15, 0.15) + plot.ax.set_ylim3d(-0.15, 0.15) + plot.ax.set_zlim3d(-0.15, 0.15) + plot.ax.view_init(azim=270, elev=90) + plot.ax._axis3don = False + + plot.add_verts(planes, **plot_params) + + return plot + +# Static methods + + @staticmethod + def create_many_quats(eulerArray: np.ndarray) -> np.ndarray: + """Create a an array of quats from an array of Euler angles. + + Parameters + ---------- + eulerArray + Array of Bunge Euler angles of shape 3 x n x ... x m. + + Returns + ------- + quats : numpy.ndarray(defdap.quat.Quat) + Array of quat objects of shape n x ... x m. + + """ + ph1 = eulerArray[0] + phi = eulerArray[1] + ph2 = eulerArray[2] + ori_shape = eulerArray.shape[1:] + + quat_comps = np.zeros((4,) + ori_shape, dtype=float) + + quat_comps[0] = np.cos(phi / 2.0) * np.cos((ph1 + ph2) / 2.0) + quat_comps[1] = -np.sin(phi / 2.0) * np.cos((ph1 - ph2) / 2.0) + quat_comps[2] = -np.sin(phi / 2.0) * np.sin((ph1 - ph2) / 2.0) + quat_comps[3] = -np.cos(phi / 2.0) * np.sin((ph1 + ph2) / 2.0) + + quats = np.empty(ori_shape, dtype=Quat) + + for i, idx in enumerate(np.ndindex(ori_shape)): + quats[idx] = Quat(quat_comps[(slice(None),) + idx]) + + return quats + + @staticmethod + def multiply_many_quats(quats: List['Quat'], right: 'Quat') -> List['Quat']: + """ Multiply all quats in a list of quats, by a single quat. + + Parameters + ---------- + quats + List of quats to be operated on. + right + Single quaternion to multiply with the list of quats. + + Returns + ------- + list(defdap.quat.Quat) + + """ + quat_array = np.array([q.quat_coef for q in quats]) + + temp_array = np.zeros((len(quat_array),4), dtype=float) + temp_array[...,0] = ((quat_array[...,0] * right.quat_coef[0]) - + np.dot(quat_array[...,1:4], right.quat_coef[1:4])) + + temp_array[...,1:4] = ((quat_array[...,0,None] * right.quat_coef[None, 1:4]) + + (right.quat_coef[0] * quat_array[..., 1:4]) + + np.cross(quat_array[...,1:4], right.quat_coef[1:4])) + + return [Quat(coefs) for coefs in temp_array] + + @staticmethod + def extract_quat_comps(quats: np.ndarray) -> np.ndarray: + """Return a NumPy array of the provided quaternion components + + Input quaternions may be given as a list of Quat objects or any iterable + whose items have 4 components which map to the quaternion. + + Parameters + ---------- + quats : numpy.ndarray(defdap.quat.Quat) + A list of Quat objects to return the components of + + Returns + ------- + numpy.ndarray + Array of quaternion components, shape (4, ..) + + """ + quats = np.array(quats) + quat_comps = np.empty((4,) + quats.shape) + for idx in np.ndindex(quats.shape): + quat_comps[(slice(None),) + idx] = quats[idx].quat_coef + + return quat_comps + + @staticmethod + def calc_sym_eqvs( + quats: np.ndarray, + sym_group: str, + dtype: Optional[type] = float + ) -> np.ndarray: + """Calculate all symmetrically equivalent quaternions of given quaternions. + + Parameters + ---------- + quats : numpy.ndarray(defdap.quat.Quat) + Array of quat objects. + sym_group + Crystal type (cubic, hexagonal). + dtype + Datatype used for calculation, defaults to `float`. + + Returns + ------- + quat_comps: numpy.ndarray, shape: (numSym x 4 x numQuats) + Array containing all symmetrically equivalent quaternion components of input quaternions. + + """ + syms = Quat.sym_eqv(sym_group) + quat_comps = np.empty((len(syms), 4, len(quats)), dtype=dtype) + + # store quat components in array + quat_comps[0] = Quat.extract_quat_comps(quats) + + # calculate symmetrical equivalents + for i, sym in enumerate(syms[1:], start=1): + # sym[i] * quat for all points (* is quaternion product) + quat_comps[i, 0, :] = ( + quat_comps[0, 0, :] * sym[0] - quat_comps[0, 1, :] * sym[1] - + quat_comps[0, 2, :] * sym[2] - quat_comps[0, 3, :] * sym[3]) + quat_comps[i, 1, :] = ( + quat_comps[0, 0, :] * sym[1] + quat_comps[0, 1, :] * sym[0] - + quat_comps[0, 2, :] * sym[3] + quat_comps[0, 3, :] * sym[2]) + quat_comps[i, 2, :] = ( + quat_comps[0, 0, :] * sym[2] + quat_comps[0, 2, :] * sym[0] - + quat_comps[0, 3, :] * sym[1] + quat_comps[0, 1, :] * sym[3]) + quat_comps[i, 3, :] = ( + quat_comps[0, 0, :] * sym[3] + quat_comps[0, 3, :] * sym[0] - + quat_comps[0, 1, :] * sym[2] + quat_comps[0, 2, :] * sym[1]) + + # swap into positive hemisphere if required + quat_comps[i, :, quat_comps[i, 0, :] < 0] *= -1 + + return quat_comps + + @staticmethod + def calc_average_ori( + quat_comps: np.ndarray + ) -> 'Quat': + """Calculate the average orientation of given quats. + + Parameters + ---------- + quat_comps : numpy.ndarray + Array containing all symmetrically equivalent quaternion components of given quaternions + (shape: numSym x 4 x numQuats), can be calculated with :func:`Quat.calc_sym_eqvs`. + + Returns + ------- + av_ori : defdap.quat.Quat + Average orientation of input quaternions. + + """ + av_ori = np.copy(quat_comps[0, :, 0]) + curr_mis_oris = np.empty(quat_comps.shape[0]) + + for i in range(1, quat_comps.shape[2]): + # calculate misorientation between current average and all + # symmetrical equivalents. Dot product of each symm quat in + # quatComps with refOri for point i + curr_mis_oris[:] = abs(np.einsum( + "ij,j->i", quat_comps[:, :, i], av_ori + )) + + # find min misorientation with current average then add to it + max_idx = np.argmax(curr_mis_oris[:]) + av_ori += quat_comps[max_idx, :, i] + + # Convert components back to a quat and normalise + av_ori = Quat(av_ori) + av_ori.normalise() + + return av_ori + + @staticmethod + def calcMisOri( + quat_comps: np.ndarray, + ref_ori: 'Quat' + ) -> Tuple[np.ndarray, 'Quat']: + """Calculate the misorientation between the quaternions and a reference quaternion. + + Parameters + ---------- + quat_comps + Array containing all symmetrically equivalent quaternion components of given quaternions + (shape: numSym x 4 x numQuats), can be calculated from quats with :func:`Quat.calc_sym_eqvs` . + ref_ori + Reference orientation. + + Returns + ------- + min_mis_oris : numpy.ndarray, len numQuats + Minimum misorientation between quats and reference orientation. + min_quat_comps : defdap.quat.Quat + Quaternion components describing minimum misorientation between quats and reference orientation. + + """ + mis_oris = np.empty((quat_comps.shape[0], quat_comps.shape[2])) + + # Dot product of each quat in quatComps with refOri + mis_oris[:, :] = abs(np.einsum("ijk,j->ik", quat_comps, ref_ori.quat_coef)) + + max_idxs0 = np.argmax(mis_oris, axis=0) + max_idxs1 = np.arange(mis_oris.shape[1]) + + min_mis_oris = mis_oris[max_idxs0, max_idxs1] + + min_quat_comps = quat_comps[max_idxs0, :, max_idxs1].transpose() + + min_mis_oris[min_mis_oris > 1] = 1 + + return min_mis_oris, min_quat_comps + + @staticmethod + def polar_angles(x: np.ndarray, y: np.ndarray, z: np.ndarray): + """Convert Cartesian coordinates to polar coordinates, for an + unit vector. + + Parameters + ---------- + x : numpy.ndarray(float) + x coordinate. + y : numpy.ndarray(float) + y coordinate. + z : numpy.ndarray(float) + z coordinate. + + Returns + ------- + float, float + inclination angle and azimuthal angle (around z axis from x + in anticlockwise as per ISO). + + """ + mod = np.sqrt(x**2 + y**2 + z**2) + x = x / mod + y = y / mod + z = z / mod + + alpha = np.arccos(z) + beta = np.arctan2(y, x) + + return alpha, beta + + @staticmethod + def calc_ipf_colours( + quats: np.ndarray, + direction: np.ndarray, + sym_group: str, + dtype: Optional[type] = np.float32 + ) -> np.ndarray: + """ + Calculate the RGB colours, based on the location of the given quats + on the fundamental region of the IPF for the sample direction specified. + + Parameters + ---------- + quats : numpy.ndarray(defdap.quat.Quat) + Array of quat objects. + direction + Direction in sample space. + sym_group + Crystal type (cubic, hexagonal). + dtype + Data type to use for calculation. + + Returns + ------- + numpy.ndarray, shape (3, num_quats) + Array of rgb colours for each quat. + + References + ------- + Stephen Cluff (BYU) - IPF_rgbcalc.m subroutine in OpenXY + https://github.com/BYU-MicrostructureOfMaterials/OpenXY/blob/master/Code/PlotIPF.m + + """ + num_quats = len(quats) + + alpha_fund, beta_fund = Quat.calc_fund_dirs( + quats, direction, sym_group, dtype=dtype + ) + + # revert to cartesians + dirvec = np.empty((3, num_quats), dtype=dtype) + dirvec[0, :] = np.sin(alpha_fund) * np.cos(beta_fund) + dirvec[1, :] = np.sin(alpha_fund) * np.sin(beta_fund) + dirvec[2, :] = np.cos(alpha_fund) + + if sym_group == 'cubic': + pole_directions = np.array([[0, 0, 1], + [1, 0, 1]/np.sqrt(2), + [1, 1, 1]/np.sqrt(3)], dtype=dtype) + if sym_group == 'hexagonal': + pole_directions = np.array([[0, 0, 1], + [np.sqrt(3), 1, 0]/np.sqrt(4), + [1, 0, 0]], dtype=dtype) + + rvect = np.broadcast_to(pole_directions[0].reshape((-1, 1)), (3, num_quats)) + gvect = np.broadcast_to(pole_directions[1].reshape((-1, 1)), (3, num_quats)) + bvect = np.broadcast_to(pole_directions[2].reshape((-1, 1)), (3, num_quats)) + + rgb = np.zeros((3, num_quats), dtype=dtype) + + # Red Component + r_dir_plane = np.cross(dirvec, rvect, axis=0) + gb_plane = np.cross(bvect, gvect, axis=0) + r_intersect = np.cross(r_dir_plane, gb_plane, axis=0) + r_norm = np.linalg.norm(r_intersect, axis=0, keepdims=True) + r_norm[r_norm == 0] = 1 #Prevent division by zero + r_intersect /= r_norm + + temp = np.arccos(np.clip(np.einsum("ij,ij->j", dirvec, r_intersect), -1, 1)) + r_intersect[:, temp > (np.pi / 2)] *= -1 + rgb[0, :] = np.divide( + np.arccos(np.clip(np.einsum("ij,ij->j", dirvec, r_intersect), -1, 1)), + np.arccos(np.clip(np.einsum("ij,ij->j", rvect, r_intersect), -1, 1)) + ) + + # Green Component + g_dir_plane = np.cross(dirvec, gvect, axis=0) + rb_plane = np.cross(rvect, bvect, axis=0) + g_intersect = np.cross(g_dir_plane, rb_plane, axis=0) + g_norm = np.linalg.norm(g_intersect, axis=0, keepdims=True) + g_norm[g_norm == 0] = 1 #Prevent division by zero + g_intersect /= g_norm + + temp = np.arccos(np.clip(np.einsum("ij,ij->j", dirvec, g_intersect), -1, 1)) + g_intersect[:, temp > (np.pi / 2)] *= -1 + rgb[1, :] = np.divide( + np.arccos(np.clip(np.einsum("ij,ij->j", dirvec, g_intersect), -1, 1)), + np.arccos(np.clip(np.einsum("ij,ij->j", gvect, g_intersect), -1, 1)) + ) + + # Blue Component + b_dir_plane = np.cross(dirvec, bvect, axis=0) + rg_plane = np.cross(gvect, rvect, axis=0) + b_intersect = np.cross(b_dir_plane, rg_plane, axis=0) + b_norm = np.linalg.norm(b_intersect, axis=0, keepdims=True) + b_norm[b_norm == 0] = 1 #Prevent division by zero + b_intersect /= b_norm + + temp = np.arccos(np.clip(np.einsum("ij,ij->j", dirvec, b_intersect), -1, 1)) + b_intersect[:, temp > (np.pi / 2)] *= -1 + rgb[2, :] = np.divide( + np.arccos(np.clip(np.einsum("ij,ij->j", dirvec, b_intersect), -1, 1)), + np.arccos(np.clip(np.einsum("ij,ij->j", bvect, b_intersect), -1, 1)) + ) + rgb /= np.amax(rgb, axis=0) + + return rgb + + @staticmethod + def calc_fund_dirs( + quats: np.ndarray, + direction: np.ndarray, + sym_group: str, + dtype: Optional[type] = float + ) -> Tuple[np.ndarray, np.ndarray]: + """ + Transform the sample direction to crystal coords based on the quats + and find the ones in the fundamental sector of the IPF. + + Parameters + ---------- + quats: array_like(defdap.quat.Quat) + Array of quat objects. + direction + Direction in sample space. + sym_group + Crystal type (cubic, hexagonal). + dtype + Data type to use for calculation. + + Returns + ------- + float, float + inclination angle and azimuthal angle (around z axis from x in anticlockwise). + + """ + # convert direction to float array + direction = np.array(direction, dtype=dtype) + + # get array of symmetry operations. shape - (numSym, 4, numQuats) + quat_comps_sym = Quat.calc_sym_eqvs(quats, sym_group, dtype=dtype) + + # array to store crystal directions for all orientations and symmetries + direction_crystal = np.empty( + (3, quat_comps_sym.shape[0], quat_comps_sym.shape[2]), dtype=dtype + ) + + # temp variables to use below + quat_dot_vec = (quat_comps_sym[:, 1, :] * direction[0] + + quat_comps_sym[:, 2, :] * direction[1] + + quat_comps_sym[:, 3, :] * direction[2]) + temp = (np.square(quat_comps_sym[:, 0, :]) - + np.square(quat_comps_sym[:, 1, :]) - + np.square(quat_comps_sym[:, 2, :]) - + np.square(quat_comps_sym[:, 3, :])) + + # transform the pole direction to crystal coords for all + # orientations and symmetries + # (quat_comps_sym * vectorQuat) * quat_comps_sym.conjugate + direction_crystal[0, :, :] = ( + 2 * quat_dot_vec * quat_comps_sym[:, 1, :] + + temp * direction[0] + + 2 * quat_comps_sym[:, 0, :] * ( + quat_comps_sym[:, 2, :] * direction[2] - + quat_comps_sym[:, 3, :] * direction[1] + ) + ) + direction_crystal[1, :, :] = ( + 2 * quat_dot_vec * quat_comps_sym[:, 2, :] + + temp * direction[1] + + 2 * quat_comps_sym[:, 0, :] * ( + quat_comps_sym[:, 3, :] * direction[0] - + quat_comps_sym[:, 1, :] * direction[2] + ) + ) + direction_crystal[2, :, :] = ( + 2 * quat_dot_vec * quat_comps_sym[:, 3, :] + + temp * direction[2] + + 2 * quat_comps_sym[:, 0, :] * ( + quat_comps_sym[:, 1, :] * direction[1] - + quat_comps_sym[:, 2, :] * direction[0] + ) + ) + + # normalise vectors + direction_crystal /= np.sqrt(np.einsum( + 'ijk,ijk->jk', direction_crystal, direction_crystal + )) + + # move all vectors into north hemisphere + direction_crystal[:, direction_crystal[2, :, :] < 0] *= -1 + + # convert to spherical coordinates + alpha, beta = Quat.polar_angles( + direction_crystal[0], direction_crystal[1], direction_crystal[2] + ) + + # find the poles in the fundamental triangle + if sym_group == "cubic": + # first beta should be between 0 and 45 deg leaving 3 + # symmetric equivalents per orientation + trial_poles = np.logical_and(beta >= 0, beta <= np.pi / 4) + + # if less than 3 left need to expand search slightly to + # catch edge cases + if np.any(np.sum(trial_poles, axis=0) < 3): + delta_beta = 1e-8 + trial_poles = np.logical_and(beta >= -delta_beta, + beta <= np.pi / 4 + delta_beta) + + # now of symmetric equivalents left we want the one with + # minimum alpha + min_alpha_idx = np.nanargmin(np.where(trial_poles==False, np.nan, alpha), axis=0) + beta_fund = beta[min_alpha_idx, np.arange(len(min_alpha_idx))] + alpha_fund = alpha[min_alpha_idx, np.arange(len(min_alpha_idx))] + + elif sym_group == "hexagonal": + # first beta should be between 0 and 30 deg leaving 1 + # symmetric equivalent per orientation + trial_poles = np.logical_and(beta >= 0, beta <= np.pi / 6) + # if less than 1 left need to expand search slightly to + # catch edge cases + if np.any(np.sum(trial_poles, axis=0) < 1): + delta_beta = 1e-8 + trial_poles = np.logical_and(beta >= -delta_beta, + beta <= np.pi / 6 + delta_beta) + + # non-indexed points cause more than 1 symmetric equivalent, use this + # to pick one and filter non-indexed points later + first_idx = (trial_poles==True).argmax(axis=0) + beta_fund = beta[first_idx, np.arange(len(first_idx))] + alpha_fund = alpha[first_idx, np.arange(len(first_idx))] + + else: + raise Exception("symGroup must be cubic or hexagonal") + + return alpha_fund, beta_fund + + @staticmethod + def sym_eqv(symGroup: str) -> List['Quat']: + """Returns all symmetric equivalents for a given crystal type. + LEGACY: move to use symmetries defined in crystal structures + + Parameters + ---------- + symGroup : str + Crystal type (cubic, hexagonal). + + Returns + ------- + list of defdap.quat.Quat + Symmetrically equivalent quats. + + """ + # Dirty fix to stop circular dependency + from defdap.crystal import crystalStructures + try: + return crystalStructures[symGroup].symmetries + except KeyError: + # return just identity if unknown structure + return [Quat(1.0, 0.0, 0.0, 0.0)] diff --git a/build/lib/defdap/slip_systems/cubic_bcc.txt b/build/lib/defdap/slip_systems/cubic_bcc.txt new file mode 100644 index 0000000..a852e6d --- /dev/null +++ b/build/lib/defdap/slip_systems/cubic_bcc.txt @@ -0,0 +1,50 @@ +# Plane normal then slip direction +green,green,green,green,red,red,red,red,blue,blue,blue,blue +0 1 -1 1 1 1 +-1 0 1 1 1 1 +1 -1 0 1 1 1 +0 1 1 1 1 -1 +-1 0 -1 1 1 -1 +1 -1 0 1 1 -1 +0 1 -1 -1 1 1 +1 0 1 -1 1 1 +-1 -1 0 -1 1 1 +0 -1 -1 1 -1 1 +-1 0 1 1 -1 1 +1 1 0 1 -1 1 +-2 1 1 1 1 1 +1 -2 1 1 1 1 +1 1 -2 1 1 1 +2 -1 1 -1 -1 1 +-1 2 1 -1 -1 1 +1 1 2 -1 -1 1 +2 1 -1 1 -1 1 +1 2 1 1 -1 1 +-1 1 2 1 -1 1 +2 1 1 -1 1 1 +1 2 -1 -1 1 1 +1 -1 2 -1 1 1 +1 2 3 1 1 -1 +2 1 3 1 1 -1 +3 2 1 1 1 -1 +2 3 1 1 1 -1 +-1 3 2 1 1 -1 +3 -1 2 1 1 -1 +1 3 2 1 -1 1 +2 3 1 1 -1 1 +3 1 2 1 -1 1 +2 1 3 1 -1 1 +-1 2 3 1 -1 1 +3 2 -1 1 -1 1 +3 1 2 -1 1 1 +3 2 1 -1 1 1 +1 2 3 -1 1 1 +1 3 2 -1 1 1 +2 -1 3 -1 1 1 +2 3 -1 -1 1 1 +1 2 3 1 1 1 +1 3 2 1 1 1 +3 1 2 1 1 1 +2 1 3 1 1 1 +2 3 1 1 1 1 +3 1 2 1 1 1 \ No newline at end of file diff --git a/build/lib/defdap/slip_systems/cubic_bcc_110only.txt b/build/lib/defdap/slip_systems/cubic_bcc_110only.txt new file mode 100644 index 0000000..d2e62b6 --- /dev/null +++ b/build/lib/defdap/slip_systems/cubic_bcc_110only.txt @@ -0,0 +1,14 @@ +# Plane normal then slip direction +green,green,green,green +0 1 -1 1 1 1 +-1 0 1 1 1 1 +1 -1 0 1 1 1 +0 1 1 1 1 -1 +-1 0 -1 1 1 -1 +1 -1 0 1 1 -1 +0 1 -1 -1 1 1 +1 0 1 -1 1 1 +-1 -1 0 -1 1 1 +0 -1 -1 1 -1 1 +-1 0 1 1 -1 1 +1 1 0 1 -1 1 \ No newline at end of file diff --git a/build/lib/defdap/slip_systems/cubic_fcc.txt b/build/lib/defdap/slip_systems/cubic_fcc.txt new file mode 100644 index 0000000..2476226 --- /dev/null +++ b/build/lib/defdap/slip_systems/cubic_fcc.txt @@ -0,0 +1,14 @@ +# Plane normal then slip direction +blue,green,red,white +1 1 1 0 1 -1 +1 1 1 -1 0 1 +1 1 1 1 -1 0 +1 1 -1 0 1 1 +1 1 -1 -1 0 -1 +1 1 -1 1 -1 0 +-1 1 1 0 1 -1 +-1 1 1 1 0 1 +-1 1 1 -1 -1 0 +1 -1 1 0 -1 -1 +1 -1 1 -1 0 1 +1 -1 1 1 1 0 \ No newline at end of file diff --git a/build/lib/defdap/slip_systems/cubic_fcc_damask.txt b/build/lib/defdap/slip_systems/cubic_fcc_damask.txt new file mode 100644 index 0000000..6794ea0 --- /dev/null +++ b/build/lib/defdap/slip_systems/cubic_fcc_damask.txt @@ -0,0 +1,14 @@ +# Plane normal then slip direction +blue,green,red,black + 1 1 1 0 1 -1 + 1 1 1 -1 0 1 + 1 1 1 1 -1 0 +-1 -1 1 0 -1 -1 +-1 -1 1 1 0 1 +-1 -1 1 -1 1 0 + 1 -1 -1 0 -1 1 + 1 -1 -1 -1 0 -1 + 1 -1 -1 1 1 0 +-1 1 -1 0 1 1 +-1 1 -1 1 0 -1 +-1 1 -1 -1 -1 0 diff --git a/build/lib/defdap/slip_systems/hexagonal_noca.txt b/build/lib/defdap/slip_systems/hexagonal_noca.txt new file mode 100644 index 0000000..1b9357c --- /dev/null +++ b/build/lib/defdap/slip_systems/hexagonal_noca.txt @@ -0,0 +1,14 @@ +# Plane normal then slip direction (3 x basal in red), (3 x prism in blue), (6 x py in green) +red,blue,blue,blue,green,green,green,green,green,green +0 0 0 1 1 1 -2 0 +0 0 0 1 1 -2 1 0 +0 0 0 1 -2 1 1 0 +1 0 -1 0 1 -2 1 0 +0 1 -1 0 -2 1 1 0 +1 -1 0 0 1 1 -2 0 +1 -1 0 1 1 1 -2 0 +1 -1 0 -1 1 1 -2 0 +1 0 -1 1 1 -2 1 0 +1 0 -1 -1 1 -2 1 0 +0 -1 1 1 -2 1 1 0 +0 -1 1 -1 -2 1 1 0 \ No newline at end of file diff --git a/build/lib/defdap/slip_systems/hexagonal_withca.txt b/build/lib/defdap/slip_systems/hexagonal_withca.txt new file mode 100644 index 0000000..0edffbe --- /dev/null +++ b/build/lib/defdap/slip_systems/hexagonal_withca.txt @@ -0,0 +1,26 @@ +# Plane normal then slip direction (3 x basal in red), (3 x prism in blue), (6 x py in green), (12 x py in green) +red,blue,blue,blue,green,green,green,green,green,green +0 0 0 1 1 1 -2 0 +0 0 0 1 1 -2 1 0 +0 0 0 1 -2 1 1 0 +1 0 -1 0 1 -2 1 0 +0 1 -1 0 -2 1 1 0 +1 -1 0 0 1 1 -2 0 +1 -1 0 1 1 1 -2 0 +1 -1 0 -1 1 1 -2 0 +1 0 -1 1 1 -2 1 0 +1 0 -1 -1 1 -2 1 0 +0 -1 1 1 -2 1 1 0 +0 -1 1 -1 -2 1 1 0 +1 0 -1 -1 1 1 -2 3 +1 0 -1 -1 2 -1 -1 3 +1 0 -1 1 -1 -1 2 3 +1 0 -1 1 -2 1 1 3 +1 -1 0 1 -2 1 1 3 +1 -1 0 1 -1 2 -1 3 +1 -1 0 -1 1 -2 1 3 +1 -1 0 -1 2 -1 -1 3 +0 -1 1 1 1 1 -2 3 +0 -1 1 1 -1 2 -1 3 +0 -1 1 -1 -1 -1 2 3 +0 -1 1 -1 1 -2 1 3 diff --git a/build/lib/defdap/utils.py b/build/lib/defdap/utils.py new file mode 100644 index 0000000..d1a3036 --- /dev/null +++ b/build/lib/defdap/utils.py @@ -0,0 +1,449 @@ +# Copyright 2023 Mechanics of Microstructures Group +# at The University of Manchester +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import functools +from datetime import datetime +from uuid import uuid4 + + +def report_progress(message: str = ""): + """Decorator for reporting progress of given function + + Parameters + ---------- + message + Message to display (prefixed by 'Starting ', progress percentage + and then 'Finished ' + + References + ---------- + Inspiration from : + https://gist.github.com/Garfounkel/20aa1f06234e1eedd419efe93137c004 + + """ + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + messageStart = f"\rStarting {message}.." + print(messageStart, end="") + # The yield statements in the function produces a generator + generator = func(*args, **kwargs) + progPrev = 0. + printFinal = True + ts = datetime.now() + try: + while True: + prog = next(generator) + if type(prog) is str: + printFinal = False + print("\r" + prog) + continue + # only report each percent + if prog - progPrev > 0.01: + messageProg = f"{messageStart} {prog*100:.0f} %" + print(messageProg, end="") + progPrev = prog + printFinal = True + + except StopIteration as e: + if printFinal: + te = str(datetime.now() - ts).split('.')[0] + messageEnd = f"\rFinished {message} ({te}) " + print(messageEnd) + # When generator finished pass the return value out + return e.value + + return wrapper + return decorator + + +class Datastore(object): + """Storage of data and metadata, with methods to allow derived data + to be calculated only when accessed. + + Attributes + ---------- + _store : dict of dict + Storage for data and metadata, keyed by data name. Each item is + a dict with at least a `data` key, all other items are metadata, + possibly including: + type : str + Type of data stored: + `map` - at least a 2-axis array, trailing axes are spatial + order : int + Tensor order of the data + unit : str + Measurement unit the data is stored in + plot_params : dict + Dictionary of the default parameters used to plot + _generators: dict + Methods to generate derived data, keyed by tuple of data names + that the method produces. + + """ + __slots__ = [ + '_store', + '_generators', + '_derivatives', + '_group_id', + '_crop_func', + ] + _been_to = None + + @staticmethod + def generate_id(): + return uuid4() + + def __init__(self, group_id=None, crop_func=None): + self._store = {} + self._generators = {} + self._derivatives = [] + self._group_id = self.generate_id() if group_id is None else group_id + self._crop_func = (lambda x, **kwargs: x) if crop_func is None else crop_func + + def __len__(self): + """Number of data in the store, including data not yet generated.""" + return len(self.keys()) + + def __str__(self): + text = 'Datastore' + for key, val in self._store.items(): + # text += f'\n {key}: {val["data"].__repr__()}' + text += f'\n {key}' + text2 = '' + for derivative in self._derivatives: + for key in self.lookup_derivative_keys(derivative): + text2 += f'\n {key}' + if text2 != '': + text += '\n Derived data:' + text2 + + return text + + def __contains__(self, key): + return key in self.keys() + + def __getitem__(self, key): + """Get data or metadata + + Parameters + ---------- + key : str or tuple of str + Either the data name or tuple of data name and metadata name. + + Returns + ------- + data or metadata + + """ + if isinstance(key, tuple): + attr = key[1] + key = key[0] + else: + attr = 'data' + + # Avoid looking up all keys over derivatives + if key not in self._store: + if key not in self: + raise KeyError(f'Data with name `{key}` does not exist.') + return self._get_derived_item(key, attr) + if attr not in self._store[key]: + raise KeyError(f'Metadata `{attr}` does not exist for `{key}`.') + + val = self._store[key][attr] + + # Generate data if needed + if attr == 'data' and val is None: + try: + val = self.generate(key, return_val=True) + except DataGenerationError: + # No generator found + pass + + if (attr == 'data' and self.get_metadata(key, 'type') == 'map' and + not self.get_metadata(key, 'cropped', False)): + binning = self.get_metadata(key, 'binning', 1) + val = self._crop_func(val, binning=binning) + + return val + + def __setitem__(self, key, val): + """Set data or metadata of item that already exists. + + Parameters + ---------- + key : str or tuple of str + Either the data name or tuple of data name and metadata name. + val : any + Value to set + + """ + if isinstance(key, tuple): + attr = key[1] + key = key[0] + else: + attr = 'data' + + if key not in self: + raise ValueError(f'Data with name `{key}` does not exist.') + + ## TODO: fix derived data + self._store[key][attr] = val + + def __getattr__(self, key): + """Get data + + """ + return self[key] + + def __setattr__(self, key, val): + """Set data of item that already exists. + + """ + if key in self.__slots__: + super().__setattr__(key, val) + else: + self[key] = val + + def __iter__(self): + """Iterate through the data names. Allows use of `*datastore` to + get all keys in the store, imitating functionality of a dictionary. + + """ + for key in self.keys(): + yield key + + def keys(self): + """Get the names of all data items. Allows use of `**datastore` + to get key-value pairs, imitating functionality of a dictionary. + + """ + keys = list(self._store.keys()) + for derivative in self._derivatives: + keys += self.lookup_derivative_keys(derivative) + return keys + + def lookup_derivative_keys(self, derivative): + root_call = False + if Datastore._been_to is None: + root_call = True + Datastore._been_to = set() + Datastore._been_to.add(self._group_id) + + source = derivative['source'] + matched_keys = [] + if source._group_id in Datastore._been_to: + return matched_keys + for key in source: + for meta_key in derivative['in_props']: + if source.get_metadata(key, meta_key) != derivative['in_props'][meta_key]: + break + else: + matched_keys.append(key) + + if root_call: + Datastore._been_to = None + + return matched_keys + + def _get_derived_item(self, key, attr): + for derivative in self._derivatives: + if key in self.lookup_derivative_keys(derivative): + break + else: + raise KeyError(f'Data with name `{key}` does not exist.') + source = derivative['source'] + + ## TODO: fix derived metadata + # if attr not in source._store[key]: + # raise KeyError(f'Metadata `{attr}` does not exist for `{key}`.') + + if attr in derivative['out_props']: + return derivative['out_props'][attr] + + if derivative['pass_ref'] and attr == 'data': + return derivative['func'](key) + + val = derivative['source'][(key, attr)] + if attr == 'data': + val = derivative['func'](val) + + return val + + # def values(self): + # return self._store.values() + + # def items(self): + # return dict(**self) + + def add(self, key, data, **kwargs): + """Add an item to the datastore. + + Parameters + ---------- + key : str + Name of the data. + data : any + Data to store. + kwargs : dict + Key-value pairs stored as the items metadata. + + """ + if key in self: + raise ValueError(f'Data with name `{key}` already exists.') + if 'data' in kwargs: + raise ValueError(f'Metadata name `data` is not allowed.') + + self._store[key] = { + 'data': data, + **kwargs + } + + def add_generator(self, keys, func, metadatas=None, **kwargs): + """Add a data generator method that produces one or more data. + + Parameters + ---------- + keys: str or tuple of str + Name(s) of data that the generator produces. + func: callable + Method that produces the data. Should return the same number + of values as there are `keys`. + metadatas : list of dict + Metadata dicts for each of data items produced. + kwargs : dict + Key-value pairs stored as the items metadata for every data + item produced. + + """ + if isinstance(keys, str): + keys = (keys, ) + if isinstance(metadatas, dict): + metadatas = (metadatas, ) + for i, key in enumerate(keys): + if metadatas is None: + metadata = {} + else: + metadata = metadatas[i] + metadata.update(kwargs) + self.add(key, None, **metadata) + self._generators[keys] = func + + def add_derivative(self, datastore, derive_func, in_props=None, + out_props=None, pass_ref=False): + if in_props is None: + in_props = {} + if out_props is None: + out_props = {} + new_derivative = { + 'source': datastore, + 'func': derive_func, + 'in_props': in_props, + 'out_props': out_props, + 'pass_ref': pass_ref, + } + # check if exists and update + for derivative in self._derivatives: + if derivative['func'] == derive_func: + derivative.update(new_derivative) + break + # or add new + else: + self._derivatives.append(new_derivative) + + def generate(self, key, return_val=False, **kwargs): + """Generate data from the associated data generation method and + store if metadata `save` is not set to False. + + Parameters + ---------- + key : str + Name of the data to generate. + + Returns + ------- + Requested data after generating. + + """ + for (keys, generator) in self._generators.items(): + if key not in keys: + continue + + datas = generator(**kwargs) + if len(keys) == 1: + if self.get_metadata(key, 'save', True): + self[key] = datas + return datas if return_val else None + + if len(keys) != len(datas): + raise ValueError( + 'Data generator method did not return the expected ' + 'number of values.' + ) + for key_i, data in zip(keys, datas): + if self.get_metadata(key_i, 'save', True): + self[key_i] = data + if key_i == key: + rtn_val = data + return rtn_val if return_val else None + + else: + raise DataGenerationError(f'Generator not found for data `{key}`') + + def update(self, other, priority=None): + """Update with data items stored in `other`. + + Parameters + ---------- + other : defdap.utils.Datastore + priority : str + Which datastore to keep an item from if the same name exists + in both. Default is to prioritise `other`. + + """ + if priority == 'self': + other._store.update(self._store) + self._store = other._store + else: + self._store.update(other._store) + + def get_metadata(self, key, attr, value=None): + """Get metadata value with a default returned if it does not + exist. Imitating the `get()` method of a dictionary. + + Parameters + ---------- + key : str + Name of the data item. + attr : str + Metadata to get. + value : any + Default value to return if metadata does not exist. + + Returns + ------- + Metadata value or the default value. + + """ + if key in self._store: + return self._store[key].get(attr, value) + + try: + return self._get_derived_item(key, attr) + except KeyError: + return value + + +class DataGenerationError(Exception): + pass diff --git a/defdap/dev-Optical.ipynb b/defdap/dev-Optical.ipynb index b24ee9a..bc023eb 100644 --- a/defdap/dev-Optical.ipynb +++ b/defdap/dev-Optical.ipynb @@ -2,29 +2,15 @@ "cells": [ { "cell_type": "code", - "execution_count": 3, + "execution_count": 1, "id": "a167425e-fc5d-4f0d-ad30-96ab225104a9", "metadata": {}, - "outputs": [ - { - "ename": "ModuleNotFoundError", - "evalue": "No module named 'numba'", - "output_type": "error", - "traceback": [ - "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[1;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[1;32mIn[3], line 7\u001b[0m\n\u001b[0;32m 5\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mdefdap\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mfile_readers\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m OpticalDataLoader, MatplotlibLoader\n\u001b[0;32m 6\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mdefdap\u001b[39;00m\n\u001b[1;32m----> 7\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mdefdap\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m ebsd\n\u001b[0;32m 8\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01moptical\u001b[39;00m \n", - "File \u001b[1;32mC:\\\\Users\\\\mbgm5pc3\\\\dev-optical\\\\DefDAP\\defdap\\ebsd.py:28\u001b[0m\n\u001b[0;32m 26\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mdefdap\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mquat\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m Quat\n\u001b[0;32m 27\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mdefdap\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m base\n\u001b[1;32m---> 28\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mdefdap\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01m_accelerated\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m flood_fill\n\u001b[0;32m 30\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mdefdap\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m defaults\n\u001b[0;32m 31\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mdefdap\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mplotting\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m MapPlot\n", - "File \u001b[1;32mC:\\\\Users\\\\mbgm5pc3\\\\dev-optical\\\\DefDAP\\defdap\\_accelerated.py:1\u001b[0m\n\u001b[1;32m----> 1\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mnumba\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m njit\n\u001b[0;32m 2\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mnumpy\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m \u001b[38;5;21;01mnp\u001b[39;00m\n\u001b[0;32m 5\u001b[0m \u001b[38;5;129m@njit\u001b[39m\n\u001b[0;32m 6\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mfind_first\u001b[39m(arr):\n", - "\u001b[1;31mModuleNotFoundError\u001b[0m: No module named 'numba'" - ] - } - ], + "outputs": [], "source": [ "import numpy as np\n", "import matplotlib.pyplot as plt\n", "import matplotlib.image as mpimg\n", - "#%matplotlib qt5\n", + "%matplotlib qt5\n", "from defdap.file_readers import OpticalDataLoader, MatplotlibLoader\n", "import defdap\n", "from defdap import ebsd\n", @@ -34,28 +20,28 @@ { "cell_type": "code", "execution_count": 2, - "id": "23fafbcf-4cb1-4540-b85e-10906c14cfe0", - "metadata": {}, - "outputs": [], - "source": [ - "import sys\n", - "sys.path.append(r'C:\\\\Users\\\\mbgm5pc3\\\\dev-optical\\\\DefDAP')" - ] - }, - { - "cell_type": "code", - "execution_count": null, "id": "6ae24a9b-6842-4540-b36b-3795c1d85f05", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Starting loading Optical data..Image loaded from test_data\\F5-pol-Cy50.png\n", + "(dimensions: 1170 x 935 pixels) \n" + ] + } + ], "source": [ - "fname = \"./test_data/F5-pol-Cy50.png\"\n", - "Opt = optical.Map(fname)" + "path = \"./test_data/\"\n", + "fname = \"F5-pol-Cy50.png\"\n", + "metadata = \"test-meta-data.xlsx\"\n", + "Opt = optical.Map(file_name = path+fname)#,meta_data = path + metadata)\n" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "8bcb125c-55be-4745-a22c-3578cef3f7ee", "metadata": {}, "outputs": [], @@ -65,7 +51,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "4387f479-785f-4639-b778-b60148f849cb", "metadata": {}, "outputs": [], @@ -75,29 +61,136 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "de922a4a-44ff-45a9-8f1d-2fb99297b698", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "Opt.plot_optical_image()" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "a0eaeccb-5e58-428e-b448-903ed9bb1942", "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loaded EBSD data (dimensions: 852 x 734 pixels, step size: 3.0 um)\n" + ] + } + ], + "source": [ + "EbsdMap = ebsd.Map(\"./test_data/f-5-test-region\")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "254019f4-7e23-487a-9efb-7a026a4ce0b8", + "metadata": {}, "outputs": [], "source": [ - "EbsdMap = ebsd.Map(path+'../EBSD_data/post_deformed/f-5-test-region')" + "#EbsdMap.plot_ipf_map([1,0,0])" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "id": "6a0c8059-c551-40b6-8091-cbaacbd798e7", "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Finished building quaternion array (0:00:01) \n", + "Finished finding grain boundaries (0:00:04) \n", + "Finished finding grain boundaries (0:00:02) \n", + "Finished finding grains (0:00:04) \n" + ] + }, + { + "data": { + "text/plain": [ + "array([[ 1, 1, 1, ..., 11, 11, 11],\n", + " [ 1, 1, 1, ..., 11, 11, 11],\n", + " [ 1, 1, 1, ..., 11, 11, 11],\n", + " ...,\n", + " [-2, -2, -2, ..., -2, -2, -2],\n", + " [-2, -2, -2, ..., -2, -2, -2],\n", + " [-2, -2, -2, ..., -2, -2, -2]])" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "#EbsdMap.calc_quat_array()\n", + "EbsdMap.find_boundaries(misori_tol = 10) #degrees\n", + "#EbsdMap.build_neighbour_network()\n", + "EbsdMap.find_grains(min_grain_size = 200) #pixels\n", + "#EbsdMap.calc_grain_mis_ori(calc_axis = False)\n", + "#EbsdMap.calc_average_grain_schmid_factors(load_vector=[1,0,0])" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "bb9b6f7c-a22a-4ca5-83be-604b40ae67f8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Starting loading Optical data.." + ] + }, + { + "ename": "TypeError", + "evalue": "Map.load_data() missing 1 required positional argument: 'file_name'", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[9], line 2\u001b[0m\n\u001b[0;32m 1\u001b[0m \u001b[38;5;66;03m#EbsdMap.set_homog_point()\u001b[39;00m\n\u001b[1;32m----> 2\u001b[0m \u001b[43mOpt\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mset_homog_point\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32m~\\dev-optical\\DefDAP\\defdap\\base.py:123\u001b[0m, in \u001b[0;36mMap.set_homog_point\u001b[1;34m(self, **kwargs)\u001b[0m\n\u001b[0;32m 122\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mset_homog_point\u001b[39m(\u001b[38;5;28mself\u001b[39m, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs):\n\u001b[1;32m--> 123\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mframe\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mset_homog_point\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32m~\\dev-optical\\DefDAP\\defdap\\experiment.py:191\u001b[0m, in \u001b[0;36mFrame.set_homog_point\u001b[1;34m(self, map_obj, map_name, **kwargs)\u001b[0m\n\u001b[0;32m 188\u001b[0m map_name \u001b[38;5;241m=\u001b[39m map_obj\u001b[38;5;241m.\u001b[39mhomog_map_name\n\u001b[0;32m 190\u001b[0m binning \u001b[38;5;241m=\u001b[39m map_obj\u001b[38;5;241m.\u001b[39mdata\u001b[38;5;241m.\u001b[39mget_metadata(map_name, \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mbinning\u001b[39m\u001b[38;5;124m'\u001b[39m, \u001b[38;5;241m1\u001b[39m)\n\u001b[1;32m--> 191\u001b[0m plot \u001b[38;5;241m=\u001b[39m \u001b[43mmap_obj\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mplot_map\u001b[49m\u001b[43m(\u001b[49m\u001b[43mmap_name\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmake_interactive\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mTrue\u001b[39;49;00m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 193\u001b[0m \u001b[38;5;66;03m# Plot stored homog points if there are any\u001b[39;00m\n\u001b[0;32m 194\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mhomog_points) \u001b[38;5;241m>\u001b[39m \u001b[38;5;241m0\u001b[39m:\n", + "File \u001b[1;32m~\\dev-optical\\DefDAP\\defdap\\base.py:583\u001b[0m, in \u001b[0;36mMap.plot_map\u001b[1;34m(self, map_name, component, **kwargs)\u001b[0m\n\u001b[0;32m 579\u001b[0m plot_params[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mscale\u001b[39m\u001b[38;5;124m'\u001b[39m] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mscale \u001b[38;5;241m/\u001b[39m binning\n\u001b[0;32m 581\u001b[0m plot_params\u001b[38;5;241m.\u001b[39mupdate(kwargs)\n\u001b[1;32m--> 583\u001b[0m map_data \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_extract_component(\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdata\u001b[49m\u001b[43m[\u001b[49m\u001b[43mmap_name\u001b[49m\u001b[43m]\u001b[49m, comp)\n\u001b[0;32m 585\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m MapPlot\u001b[38;5;241m.\u001b[39mcreate(\u001b[38;5;28mself\u001b[39m, map_data, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mplot_params)\n", + "File \u001b[1;32m~\\dev-optical\\DefDAP\\defdap\\utils.py:169\u001b[0m, in \u001b[0;36mDatastore.__getitem__\u001b[1;34m(self, key)\u001b[0m\n\u001b[0;32m 167\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m attr \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mdata\u001b[39m\u001b[38;5;124m'\u001b[39m \u001b[38;5;129;01mand\u001b[39;00m val \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[0;32m 168\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m--> 169\u001b[0m val \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mgenerate\u001b[49m\u001b[43m(\u001b[49m\u001b[43mkey\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mreturn_val\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mTrue\u001b[39;49;00m\u001b[43m)\u001b[49m\n\u001b[0;32m 170\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m DataGenerationError:\n\u001b[0;32m 171\u001b[0m \u001b[38;5;66;03m# No generator found\u001b[39;00m\n\u001b[0;32m 172\u001b[0m \u001b[38;5;28;01mpass\u001b[39;00m\n", + "File \u001b[1;32m~\\dev-optical\\DefDAP\\defdap\\utils.py:383\u001b[0m, in \u001b[0;36mDatastore.generate\u001b[1;34m(self, key, return_val, **kwargs)\u001b[0m\n\u001b[0;32m 380\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m key \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;129;01min\u001b[39;00m keys:\n\u001b[0;32m 381\u001b[0m \u001b[38;5;28;01mcontinue\u001b[39;00m\n\u001b[1;32m--> 383\u001b[0m datas \u001b[38;5;241m=\u001b[39m \u001b[43mgenerator\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 384\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(keys) \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m1\u001b[39m:\n\u001b[0;32m 385\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mget_metadata(key, \u001b[38;5;124m'\u001b[39m\u001b[38;5;124msave\u001b[39m\u001b[38;5;124m'\u001b[39m, \u001b[38;5;28;01mTrue\u001b[39;00m):\n", + "File \u001b[1;32m~\\dev-optical\\DefDAP\\defdap\\utils.py:42\u001b[0m, in \u001b[0;36mreport_progress..decorator..wrapper\u001b[1;34m(*args, **kwargs)\u001b[0m\n\u001b[0;32m 40\u001b[0m \u001b[38;5;28mprint\u001b[39m(messageStart, end\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m 41\u001b[0m \u001b[38;5;66;03m# The yield statements in the function produces a generator\u001b[39;00m\n\u001b[1;32m---> 42\u001b[0m generator \u001b[38;5;241m=\u001b[39m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 43\u001b[0m progPrev \u001b[38;5;241m=\u001b[39m \u001b[38;5;241m0.\u001b[39m\n\u001b[0;32m 44\u001b[0m printFinal \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mTrue\u001b[39;00m\n", + "\u001b[1;31mTypeError\u001b[0m: Map.load_data() missing 1 required positional argument: 'file_name'" + ] + } + ], + "source": [ + "#EbsdMap.set_homog_point()\n", + "Opt.set_homog_point()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e8032bfb-1f04-4ffc-9ad4-20a47b3bb9e9", + "metadata": {}, "outputs": [], "source": [] } diff --git a/defdap/main-Optical.ipynb b/defdap/main-Optical.ipynb new file mode 100644 index 0000000..e6e1f71 --- /dev/null +++ b/defdap/main-Optical.ipynb @@ -0,0 +1,2937 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "9f25ff76-d4c4-4212-87a3-d8f6ac4d4af3", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "%matplotlib qt5\n", + "from matplotlib.pyplot import imread\n", + "import inspect\n", + "from skimage import transform as tf\n", + "from skimage import morphology as mph\n", + "from scipy.stats import mode#\n", + "from scipy.ndimage import binary_dilation\n", + "import peakutils\n", + "from defdap import base\n", + "from defdap import defaults\n", + "from defdap import quat\n", + "from defdap import ebsd\n", + "from defdap import hrdic\n", + "from defdap import plotting\n", + "import optical as Optical" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "6b0de770-12ba-42b0-867e-49b7f5cdace8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "path = 'C:/Users/mbgm5pc3/The University of Manchester Dropbox/Patrick Curran/PhD Patrick Curran/4) Experiments folder/year 2 4-point bending/analysis/'\n", + "fname = 'F5-pol-Cy50'\n", + "extension = 'png'\n", + "optical = Optical.Map(path,fname,extension)\n", + "print(optical)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "e8889462-1fbc-48d8-8bf9-48432445ff65", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "optical.setScale(micrometrePerPixel=2.105)\n", + "#optical.setCrop(xMin=10, xMax=10, yMin=10, yMax=10)\n", + "optical.plotMap(plotScaleBar=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "33cbe401-2d6b-488b-9f29-4ed983ccbd82", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loaded EBSD data (dimensions: 852 x 734 pixels, step size: 3.0 um)\n" + ] + } + ], + "source": [ + "EbsdMap = ebsd.Map(path+'../EBSD_data/post_deformed/f-5-test-region')" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "a4ea5725-7656-4b7b-914a-7dab674fdf34", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Finished building quaternion array (0:00:02) \n", + "Finished finding grain boundaries (0:00:04) \n", + "Starting finding grains.. 99 %[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "[[2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " ...\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]\n", + " [2 2 2 ... 2 2 2]]\n", + "Finished finding grains (0:00:02) \n", + "Finished calculating grain misorientations (0:00:07) \n", + "Finished calculating grain average Schmid factors (0:00:00) \n" + ] + } + ], + "source": [ + "EbsdMap.buildQuatArray()\n", + "EbsdMap.findBoundaries(boundDef = 10) #degrees\n", + "EbsdMap.findGrains(minGrainSize = 200) #pixels\n", + "EbsdMap.calcGrainMisOri(calcAxis = False)\n", + "EbsdMap.calcAverageGrainSchmidFactors(loadVector=[1,0,0])\n", + "#EbsdMap.rotateData()" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "03910387-b153-4f13-8a22-f0e6b519c3f9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "EbsdMap.plotIPFMap([1,0,0], plotScaleBar=True)#,plotGBs= True)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "cb09bfcd-27bd-4b20-bcd5-217b201eac34", + "metadata": {}, + "outputs": [], + "source": [ + "#EbsdMap.plotHomog=EbsdMap.plotBandContrastMap\n", + "#EbsdMap.setHomogPoint()\n", + "#optical.setHomogPoint(display='optical')" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "c5c2e5fd-c42d-4462-9183-e1f154624d51", + "metadata": {}, + "outputs": [], + "source": [ + "optical.homogPoints = [(np.int64(223), np.int64(487)), (np.int64(922), np.int64(17)), (np.int64(1023), np.int64(786)), (np.int64(86), np.int64(715))]\n", + "EbsdMap.homogPoints = [(np.int64(136), np.int64(417)), (np.int64(622), np.int64(87)), (np.int64(699), np.int64(628)), (np.int64(41), np.int64(582))]" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "d75e187b-e5cc-4085-a952-7aad1e360320", + "metadata": {}, + "outputs": [], + "source": [ + "#print(optical.homogPoints)\n", + "#print(EbsdMap.homogPoints)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "aab97d07-ada3-4fbf-a74f-b51532a6ad39", + "metadata": {}, + "outputs": [], + "source": [ + "optical.linkEbsdMap(EbsdMap, transformType='affine')" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "07837163-5971-4f77-a438-ff3a6d997085", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "optical.plotMap(plotGBs=True, dilateBoundaries=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "27db4b04-f59a-4c92-bf89-6f27422c5b04", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Finished finding grains (0:00:00) \n" + ] + } + ], + "source": [ + "optical.findGrains(algorithm='warp')" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "7f0f5198-aeac-46b2-8994-d236817dff34", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "EbsdMap.locateGrainID()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24130817-76a6-4678-b64f-bd3e9309acf5", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "98d34f20-c368-4574-8988-ac83de80e594", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "66557c8e-e61b-451d-a428-4f7f312ebae7", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "931eef33-04a5-4fd8-a5e4-f576b2632d20", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[0.0142, 0.0492, 0.9033, 0.4260]\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "grainID = 182\n", + "grain = EbsdMap[grainID]\n", + "grain.calcAverageOri() # stored as a quaternion named grain.refOri\n", + "print(grain.refOri)\n", + "grain.plotRefOri(direction=[1, 0, 0])" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "09ad2510-44da-4a58-b106-04abaa6bde32", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Grain ID: 182\n", + "Coordinate List: []\n", + "Optical Data: []\n", + "Points List: []\n", + "Groups List: []\n", + "EBSD Grain: None\n", + "EBSD Map: None\n" + ] + } + ], + "source": [ + "# Example usage:\n", + "#grainID = 1 # Replace with your actual grain ID\n", + "opticalMap = Optical.Map(path, fname, extension) # Replace with your actual map creation\n", + "\n", + "# Create an instance of Grain\n", + "opticalGrain = Optical.Grain(grainID, opticalMap)\n", + "\n", + "# Optionally, add points and optical data\n", + "# opticalGrain.addPoint((x, y), optical_data)\n", + "\n", + "# Print the stored data\n", + "opticalGrain.printData() # Make sure to have the printData method defined\n" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "33422fef-c5f4-4b49-a0c7-94958a2c0c83", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Grain(ID=182)\n" + ] + } + ], + "source": [ + "print(opticalGrain)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "f50c14db-5685-4873-998e-9bf8a2b5d4c5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Grain(ID=182)\n", + "[]\n" + ] + } + ], + "source": [ + "mapData = np.array(opticalGrain)\n", + "print(opticalGrain.grainData(mapData))" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "1bdaecc9-5926-4564-8c7e-275bc316f780", + "metadata": {}, + "outputs": [], + "source": [ + "#print(opticalGrain.grainData(opticalGrain.optical_map)) # Check if grainData contains valid data" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "ae9a0206-d71f-4ca9-95aa-0947886992b6", + "metadata": {}, + "outputs": [ + { + "ename": "ValueError", + "evalue": "zero-size array to reduction operation minimum which has no identity", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mValueError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[19], line 1\u001b[0m\n\u001b[1;32m----> 1\u001b[0m plot \u001b[38;5;241m=\u001b[39m \u001b[43mopticalGrain\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mplotDarkfield\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 2\u001b[0m \u001b[43m \u001b[49m\u001b[43mplotScaleBar\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mTrue\u001b[39;49;00m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mplotSlipTraces\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mTrue\u001b[39;49;00m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mplotSlipBands\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mTrue\u001b[39;49;00m\n\u001b[0;32m 3\u001b[0m \u001b[43m)\u001b[49m\n", + "File \u001b[1;32m~\\DefDAP\\defdap\\optical.py:784\u001b[0m, in \u001b[0;36mGrain.plotDarkfield\u001b[1;34m(self, **kwargs)\u001b[0m\n\u001b[0;32m 778\u001b[0m plotParams \u001b[38;5;241m=\u001b[39m {\n\u001b[0;32m 779\u001b[0m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mplotColourBar\u001b[39m\u001b[38;5;124m'\u001b[39m: \u001b[38;5;28;01mFalse\u001b[39;00m,\n\u001b[0;32m 780\u001b[0m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mclabel\u001b[39m\u001b[38;5;124m'\u001b[39m: \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mEffective shear strain\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[0;32m 781\u001b[0m }\n\u001b[0;32m 782\u001b[0m plotParams\u001b[38;5;241m.\u001b[39mupdate(kwargs)\n\u001b[1;32m--> 784\u001b[0m plot \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mplotGrainData\u001b[49m\u001b[43m(\u001b[49m\u001b[43mgrainData\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43moptical_map\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mplotParams\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 786\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m plot\n", + "File \u001b[1;32m~\\DefDAP\\defdap\\base.py:1057\u001b[0m, in \u001b[0;36mGrain.plotGrainData\u001b[1;34m(self, mapData, grainData, **kwargs)\u001b[0m\n\u001b[0;32m 1054\u001b[0m plotParams \u001b[38;5;241m=\u001b[39m {}\n\u001b[0;32m 1055\u001b[0m plotParams\u001b[38;5;241m.\u001b[39mupdate(kwargs)\n\u001b[1;32m-> 1057\u001b[0m grainMapData \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mgrainMapData\u001b[49m\u001b[43m(\u001b[49m\u001b[43mmapData\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmapData\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mgrainData\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mgrainData\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 1059\u001b[0m plot \u001b[38;5;241m=\u001b[39m GrainPlot\u001b[38;5;241m.\u001b[39mcreate(\u001b[38;5;28mself\u001b[39m, grainMapData, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mplotParams)\n\u001b[0;32m 1061\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m plot\n", + "File \u001b[1;32m~\\DefDAP\\defdap\\base.py:959\u001b[0m, in \u001b[0;36mGrain.grainMapData\u001b[1;34m(self, mapData, grainData, bg)\u001b[0m\n\u001b[0;32m 957\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m 958\u001b[0m grainData \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mgrainData(mapData)\n\u001b[1;32m--> 959\u001b[0m x0, y0, xmax, ymax \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mextremeCoords\u001b[49m\n\u001b[0;32m 961\u001b[0m grainMapData \u001b[38;5;241m=\u001b[39m np\u001b[38;5;241m.\u001b[39mfull((ymax \u001b[38;5;241m-\u001b[39m y0 \u001b[38;5;241m+\u001b[39m \u001b[38;5;241m1\u001b[39m, xmax \u001b[38;5;241m-\u001b[39m x0 \u001b[38;5;241m+\u001b[39m \u001b[38;5;241m1\u001b[39m), bg,\n\u001b[0;32m 962\u001b[0m dtype\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mtype\u001b[39m(grainData[\u001b[38;5;241m0\u001b[39m]))\n\u001b[0;32m 964\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m coord, data \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mzip\u001b[39m(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mcoordList, grainData):\n", + "File \u001b[1;32m~\\DefDAP\\defdap\\base.py:820\u001b[0m, in \u001b[0;36mGrain.extremeCoords\u001b[1;34m(self)\u001b[0m\n\u001b[0;32m 810\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"Coordinates of the bounding box for a grain.\u001b[39;00m\n\u001b[0;32m 811\u001b[0m \n\u001b[0;32m 812\u001b[0m \u001b[38;5;124;03mReturns\u001b[39;00m\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 816\u001b[0m \n\u001b[0;32m 817\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[0;32m 818\u001b[0m coords \u001b[38;5;241m=\u001b[39m np\u001b[38;5;241m.\u001b[39marray(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mcoordList, dtype\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mint\u001b[39m)\n\u001b[1;32m--> 820\u001b[0m x0, y0 \u001b[38;5;241m=\u001b[39m \u001b[43mcoords\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mmin\u001b[49m\u001b[43m(\u001b[49m\u001b[43maxis\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m0\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[0;32m 821\u001b[0m xmax, ymax \u001b[38;5;241m=\u001b[39m coords\u001b[38;5;241m.\u001b[39mmax(axis\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m0\u001b[39m)\n\u001b[0;32m 823\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m x0, y0, xmax, ymax\n", + "File \u001b[1;32m~\\optical\\Lib\\site-packages\\numpy\\core\\_methods.py:45\u001b[0m, in \u001b[0;36m_amin\u001b[1;34m(a, axis, out, keepdims, initial, where)\u001b[0m\n\u001b[0;32m 43\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m_amin\u001b[39m(a, axis\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m, out\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m, keepdims\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mFalse\u001b[39;00m,\n\u001b[0;32m 44\u001b[0m initial\u001b[38;5;241m=\u001b[39m_NoValue, where\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mTrue\u001b[39;00m):\n\u001b[1;32m---> 45\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mumr_minimum\u001b[49m\u001b[43m(\u001b[49m\u001b[43ma\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43maxis\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mNone\u001b[39;49;00m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mout\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mkeepdims\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43minitial\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mwhere\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[1;31mValueError\u001b[0m: zero-size array to reduction operation minimum which has no identity" + ] + } + ], + "source": [ + "plot = opticalGrain.plotDarkfield(\n", + " plotScaleBar=True, plotSlipTraces=True, plotSlipBands=True\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fbf23886-5999-4329-bf3e-fe1e82384fe0", + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " opticalGrain.addPoint((10, 20), [0.5]) # Add point (10, 20) with optical data [0.5]\n", + " opticalGrain.addPoint((15, 25), [0.75]) # Add point (15, 25) with optical data [0.75]\n", + " opticalGrain.addPoint((20, 30), [1.0]) # Add point (20, 30) with optical data [1.0]\n", + "\n", + " # Optional: Print the stored data after adding points\n", + " opticalGrain.printData()\n", + "except ValueError as e:\n", + " print(f\"Error adding point: {e}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4caa440b-df9c-41cc-bf80-81167fcc4d9a", + "metadata": {}, + "outputs": [], + "source": [ + "# Assuming you have created or loaded an EBSD grain and map\n", + "ebsd_grain_id = 1 # Replace with your actual EBSD grain ID\n", + "ebsd_map = EbsdMap # Replace with your actual EBSD map instance\n", + "\n", + "# Set the ebsdGrain and ebsdMap attributes\n", + "opticalGrain.ebsdGrain = ebsd_grain_id # Set the EBSD grain ID\n", + "opticalGrain.ebsdMap = ebsd_map # Set the EBSD map instance\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "391d205f-195c-44f6-b843-d401a640f221", + "metadata": {}, + "outputs": [], + "source": [ + "opticalGrain.printData() " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2192eab2-c007-4f93-a107-d575dda97c49", + "metadata": {}, + "outputs": [], + "source": [ + "opticalGrain.plotDarkfield()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "94ca7127-c4cf-40c7-87a6-2ae98cb69e87", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "aac91237-0e69-4c36-8f86-2cf7ac98e8e8", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "477a60c7-667f-4735-8417-999c42916782", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9c1fb1d7-6dc7-45a4-80a2-0e305676e2b3", + "metadata": {}, + "outputs": [], + "source": [ + "int(a)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7ebfc3c2-2417-4f96-8940-c4349dfa7222", + "metadata": {}, + "outputs": [], + "source": [ + "#optical.runGrainInspector(corrAngle=0)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "181e805e-33e3-4993-adfe-e785c695c126", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "59ec1911-ab4f-482a-835e-5b8dd5851bc7", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d089ca9d-5ce1-4561-9884-5c5e525f62b2", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21bf5116-cd61-4bf9-a79e-8ba6ee5b11da", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "90be0fcc-a7e4-41a7-b138-7ebb2c38f5ff", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5236a754-29cb-4125-be01-b49b98faa981", + "metadata": {}, + "outputs": [], + "source": [ + "int('a')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9855c4dc-0d25-4fee-84c6-d0763a5f269c", + "metadata": { + "jupyter": { + "source_hidden": true + } + }, + "outputs": [], + "source": [ + "class Map(base.Map):\n", + " def __init__(self, path, fname, file_extension):\n", + " \"\"\"Initialise class and import DIC data from file.\n", + "\n", + " Parameters\n", + " ----------\n", + " path : str\n", + " Path to file.\n", + " fname : str\n", + " Name of file including extension.\n", + " dataType : str\n", + " Type of data file.\n", + "\n", + " \"\"\"\n", + " # Call base class constructor\n", + " super(Map, self).__init__()\n", + "\n", + " # Initialise variables\n", + " self.format = None # Software name\n", + " self.version = None # Software version\n", + " self.extension = file_extension #file extension\n", + " self.xc = None # x coordinates\n", + " self.yc = None # y coordinates\n", + "\n", + " self.ebsdMap = None # EBSD map linked to DIC map\n", + " self.ebsdTransform = None # Transform from EBSD to Optical coordinates\n", + " self.ebsdTransformInv = None # Transform from Optical to EBSD coordinates\n", + " self.ebsdGrainIds = None\n", + " self.plotHomog = self.plotMap # Use max shear map for defining homologous points\n", + " self.highlightAlpha = 0.6\n", + " #self.bseScale = None # size of a pixel in the correlated images\n", + " #self.patScale = None # size of pixel in loaded\n", + " self.opticalScale = None\n", + " # pattern relative to pixel size of dic data i.e 1 means they\n", + " # are the same size and 2 means the pixels in the pattern are\n", + " # half the size of the dic data.\n", + " self.path = path # file path\n", + " self.fname = fname # file name\n", + " self.patternImPath = self.path + f\"{self.fname}.{self.extension}\"\n", + " #self.loadData(path, fname, dataType=dataType\n", + " \n", + " # crop distances (default all zeros)\n", + " self.cropDists = np.array(((0, 0), (0, 0)), dtype=int)\n", + " self.optical = plt.imread(self.patternImPath)\n", + " self.xdim = np.shape(self.optical)[0] # size of map along x (from header)\n", + " self.ydim = np.shape(self.optical)[1] # size of map along y (from header)\n", + " #self.plotDefault = lambda *args, **kwargs: self.plotMaxShear(plotGBs=True, *args, **kwargs)# come back to --------------------------------------\n", + " \n", + " # *dim are full size of data. *Dim are size after cropping\n", + " self.xDim = self.xdim\n", + " self.yDim = self.ydim\n", + "#########################################################################################################################################################\n", + " def setScale(self, micrometrePerPixel):\n", + " \"\"\"Sets the scale of the map.\n", + "\n", + " Parameters\n", + " ----------\n", + " micrometrePerPixel : float\n", + " Length of pixel in original BSE image in micrometres.\n", + "\n", + " \"\"\"\n", + " self.opticalScale = micrometrePerPixel\n", + "\n", + " @property\n", + " def scale(self):\n", + " \"\"\"Returns the number of micrometers per pixel in the DIC map.\n", + "\n", + " \"\"\"\n", + " if self.opticalScale is None:\n", + " raise ValueError(\"Map scale not set. Set with setScale()\")\n", + "\n", + " return self.opticalScale \n", + "\n", + " def setCrop(self, xMin=None, xMax=None, yMin=None, yMax=None, updateHomogPoints=False):\n", + " \"\"\"Set a crop for the DIC map.\n", + "\n", + " Parameters\n", + " ----------\n", + " xMin : int\n", + " Distance to crop from left in pixels.\n", + " xMax : int\n", + " Distance to crop from right in pixels.\n", + " yMin : int\n", + " Distance to crop from top in pixels.\n", + " yMax : int\n", + " Distance to crop from bottom in pixels.\n", + " updateHomogPoints : bool, optional\n", + " If true, change homologous points to reflect crop.\n", + "\n", + " \"\"\"\n", + " # changes in homog points\n", + " dx = 0\n", + " dy = 0\n", + "\n", + " # update crop distances\n", + " if xMin is not None:\n", + " if updateHomogPoints:\n", + " dx = self.cropDists[0, 0] - int(xMin)\n", + " self.cropDists[0, 0] = int(xMin)\n", + " if xMax is not None:\n", + " self.cropDists[0, 1] = int(xMax)\n", + " if yMin is not None:\n", + " if updateHomogPoints:\n", + " dy = self.cropDists[1, 0] - int(yMin)\n", + " self.cropDists[1, 0] = int(yMin)\n", + " if yMax is not None:\n", + " self.cropDists[1, 1] = int(yMax)\n", + "\n", + " # update homogo points if required\n", + " if updateHomogPoints and (dx != 0 or dy != 0):\n", + " self.updateHomogPoint(homogID=-1, delta=(dx, dy))\n", + "\n", + " # set new cropped dimensions\n", + " self.xDim = self.xdim - self.cropDists[0, 0] - self.cropDists[0, 1]\n", + " self.yDim = self.ydim - self.cropDists[1, 0] - self.cropDists[1, 1]\n", + "\n", + " def crop(self, mapData, binned=True):\n", + " \"\"\" Crop given data using crop parameters stored in map\n", + " i.e. cropped_data = DicMap.crop(DicMap.data_to_crop).\n", + "\n", + " Parameters\n", + " ----------\n", + " mapData : numpy.ndarray\n", + " Bap data to crop.\n", + " binned : bool\n", + " True if mapData is binned i.e. binned BSE pattern.\n", + " \"\"\"\n", + " if binned:\n", + " multiplier = 1\n", + " else:\n", + " multiplier = self.opticalScale\n", + "\n", + " minY = int(self.cropDists[1, 0] * multiplier)\n", + " maxY = int((self.ydim - self.cropDists[1, 1]) * multiplier)\n", + "\n", + " minX = int(self.cropDists[0, 0] * multiplier)\n", + " maxX = int((self.xdim - self.cropDists[0, 1]) * multiplier)\n", + "\n", + " return mapData[minY:maxY, minX:maxX]\n", + " \n", + "\n", + " def plotMap(self, component=None, **kwargs):\n", + " \"\"\"Plot a map from the DIC/EBSD data or an optical image.\n", + " \n", + " Parameters\n", + " ----------\n", + " component : str, optional\n", + " Map component to plot, e.g., 'e11', 'f11', 'eMaxShear'. If None, the optical image is plotted.\n", + " kwargs : dict\n", + " Additional arguments passed to :func:`defdap.plotting.MapPlot.create`.\n", + " \n", + " Returns\n", + " -------\n", + " defdap.plotting.MapPlot or None\n", + " Returns the plot if a map component is plotted, otherwise None for optical images.\n", + " \"\"\"\n", + " \n", + " # If a map component is provided, plot the DIC/EBSD map component\n", + " if component is not None:\n", + " plotParams = {\n", + " 'plotColourBar': True,\n", + " 'clabel': component\n", + " }\n", + " plotParams.update(kwargs)\n", + " \n", + " # Assuming self.component[component] holds the map data (e.g., strain)\n", + " mapData = self.crop(self.component[component])\n", + " \n", + " # Use the create method for map plotting\n", + " plot = MapPlot.create(self, mapData, **plotParams)\n", + " \n", + " return plot\n", + " \n", + " else:\n", + " # Default figure/axis setup for optical image\n", + " mapData = self.crop(self.optical)\n", + " plotParams = {'plotColourBar': False, 'clabel': None}\n", + " plotParams.update(kwargs)\n", + " plot = MapPlot.create(self, mapData, **plotParams)\n", + " return plot\n", + " \n", + " def plotOptical(self, **kwargs):\n", + " \"\"\"\n", + " Plot optical image of Map. For use with setting with homogPoints. \n", + " Parameters\n", + " ----------\n", + " kwargs\n", + " All arguments are passed to :func:`defdap.plotting.MapPlot.create`.\n", + "\n", + " Returns\n", + " -------\n", + " defdap.plotting.MapPlot\n", + " \"\"\"\n", + " # Set default plot parameters then update with any input\n", + " plotParams = {\n", + " 'cmap': 'gray'\n", + " }\n", + " #scale = 1\n", + " #try:\n", + " # plotParams['scale'] = self.scale / self.opticalScale * 1e-6\n", + " #except(ValueError):\n", + " # pass\n", + " #plotParams.update(kwargs)\n", + "\n", + " # Check image path is set\n", + " if self.patternImPath is None:\n", + " raise Exception(\"First set path to pattern image.\")\n", + "\n", + " polarised = imread(self.patternImPath)\n", + " #polarised = self.crop(polarised, binned=False)\n", + "\n", + " plot = MapPlot.create(self, polarised,makeInteractive=True, **plotParams)\n", + "\n", + " return plot \n", + " \n", + " def setHomogPoint(self, points=None, display=None, **kwargs):#1\n", + " \"\"\"Set homologous points. Uses interactive GUI if points is None.\n", + " \n", + " Parameters\n", + " ----------\n", + " points : list, optional\n", + " Homologous points to set.\n", + " display : string, optional\n", + " Use max shear map if set to 'maxshear', pattern if set to 'pattern', \n", + " or optical image if set to 'optical'.\n", + " \"\"\"\n", + " \n", + " if points is not None:\n", + " self.homogPoints = points\n", + " \n", + " if points is None:\n", + " # Default display setting\n", + " if display is None:\n", + " display = \"optical\"\n", + " \n", + " # Normalize the display string for comparison\n", + " display = display.lower().replace(\" \", \"\")\n", + " \n", + " # Handle different display modes\n", + " if display == \"optical\":\n", + " # Set to optical image display (new case for optical data)\n", + " self.plotHomog = self.plotOptical # This should be a method for handling optical data\n", + " self.opticalScale = 1\n", + " binSize = self.opticalScale # Adjust to whatever scaling factor is appropriate for optical data\n", + " \n", + " # Call setHomogPoint from the base class, passing binSize and points\n", + " super(type(self), self).setHomogPoint(binSize=binSize, points=points, **kwargs)\n", + "\n", + "\n", + " def linkEbsdMap(self, ebsdMap, transformType=\"affine\", **kwargs):\n", + " \"\"\"Calculates the transformation required to align EBSD dataset to DIC.\n", + "\n", + " Parameters\n", + " ----------\n", + " ebsdMap : defdap.ebsd.Map\n", + " EBSD map object to link.\n", + " transformType : str, optional\n", + " affine, piecewiseAffine or polynomial.\n", + " kwargs\n", + " All arguments are passed to `estimate` method of the transform.\n", + "\n", + " \"\"\"\n", + " self.ebsdMap = ebsdMap\n", + " calc_inv = False\n", + " if transformType.lower() == \"piecewiseaffine\":\n", + " self.ebsdTransform = tf.PiecewiseAffineTransform()\n", + " elif transformType.lower() == \"projective\":\n", + " self.ebsdTransform = tf.ProjectiveTransform()\n", + " elif transformType.lower() == \"polynomial\":\n", + " calc_inv = True\n", + " self.ebsdTransform = tf.PolynomialTransform()\n", + " self.ebsdTransformInv = tf.PolynomialTransform()\n", + " else:\n", + " # default to using affine\n", + " self.ebsdTransform = tf.AffineTransform()\n", + "\n", + " # calculate transform from EBSD to DIC frame\n", + " self.ebsdTransform.estimate(\n", + " np.array(self.homogPoints),\n", + " np.array(self.ebsdMap.homogPoints),\n", + " **kwargs\n", + " )\n", + " # Calculate inverse if required\n", + " if calc_inv:\n", + " self.ebsdTransformInv.estimate(\n", + " np.array(self.ebsdMap.homogPoints),\n", + " np.array(self.homogPoints),\n", + " **kwargs\n", + " )\n", + " else:\n", + " self.ebsdTransformInv = self.ebsdTransform.inverse\n", + "\n", + " def warp_lines_to_optical_frame(self, lines):\n", + " \"\"\"Warp a set of lines to the DIC reference frame.\n", + "\n", + " Parameters\n", + " ----------\n", + " lines : list of tuples\n", + " Lines to warp. Each line is represented as a tuple of start\n", + " and end coordinates (x, y).\n", + "\n", + " Returns\n", + " -------\n", + " list of tuples\n", + " List of warped lines with same representation as input.\n", + "\n", + " \"\"\"\n", + " # Flatten to coord list\n", + " lines = np.array(lines).reshape(-1, 2)\n", + " # Transform & reshape back\n", + " lines = self.ebsdTransformInv(lines).reshape(-1, 2, 2)\n", + " # Round to nearest\n", + " lines = np.round(lines - 0.5) + 0.5\n", + " lines = [(tuple(l[0]), tuple(l[1])) for l in lines]\n", + "\n", + " return lines\n", + " \n", + " @property\n", + " def boundaries(self):\n", + " \"\"\"Returns EBSD map grain boundaries warped to DIC frame.\n", + "\n", + " \"\"\"\n", + " # Check a EBSD map is linked\n", + " self.checkEbsdLinked()\n", + "\n", + " # image is returned cropped if a piecewise transform is being used\n", + " boundaries = self.ebsdMap.boundaries\n", + " boundaries = self.warpToOpticalFrame( \n", + " -boundaries.astype(float), cropImage=False\n", + " )\n", + " boundaries = boundaries > 0.1\n", + " boundaries = mph.skeletonize(boundaries)\n", + " boundaries = mph.remove_small_objects(\n", + " boundaries, min_size=10, connectivity=2\n", + " )\n", + "\n", + " # crop image if it is a simple affine transform\n", + " if type(self.ebsdTransform) is tf.AffineTransform:\n", + " # need to apply the translation of ebsd transform and\n", + " # remove 5% border\n", + " crop = np.copy(self.ebsdTransform.params[0:2, 2])\n", + " crop += 0.05 * np.array(self.ebsdMap.boundaries.shape)\n", + " # the crop is defined in EBSD coords so need to transform it\n", + " transformMatrix = np.copy(self.ebsdTransform.params[0:2, 0:2])\n", + " crop = np.matmul(np.linalg.inv(transformMatrix), crop)\n", + " crop = crop.round().astype(int)\n", + "\n", + " boundaries = boundaries[crop[1]:crop[1] + self.yDim,\n", + " crop[0]:crop[0] + self.xDim]\n", + "\n", + " return -boundaries.astype(int)\n", + "\n", + " \n", + " @property\n", + " def boundaryLines(self):\n", + " return self.warp_lines_to_optical_frame(self.ebsdMap.boundaryLines)\n", + "\n", + " @property\n", + " def phaseBoundaryLines(self):\n", + " return self.warp_lines_to_optical_frame(self.ebsdMap.phaseBoundaryLines)\n", + "\n", + " def checkEbsdLinked(self):\n", + " \"\"\"Check if an EBSD map has been linked.\n", + "\n", + " Returns\n", + " ----------\n", + " bool\n", + " Returns True if EBSD map linked.\n", + "\n", + " Raises\n", + " ----------\n", + " Exception\n", + " If EBSD map not linked.\n", + "\n", + " \"\"\"\n", + " if self.ebsdMap is None:\n", + " raise Exception(\"No EBSD map linked.\")\n", + " return True\n", + "\n", + " def warpToOpticalFrame(self, mapData, cropImage=True, order=1, preserve_range=False):\n", + " \"\"\"Warps a map to the DIC frame.\n", + "\n", + " Parameters\n", + " ----------\n", + " mapData : numpy.ndarray\n", + " Data to warp.\n", + " cropImage : bool, optional\n", + " Crop to size of DIC map if true.\n", + " order : int, optional\n", + " Order of interpolation (0: Nearest-neighbor, 1: Bi-linear...).\n", + " preserve_range: bool, optional\n", + " Keep the original range of values.\n", + "\n", + " Returns\n", + " ----------\n", + " numpy.ndarray\n", + " Map (i.e. EBSD map data) warped to the DIC frame.\n", + "\n", + " \"\"\"\n", + " # Check a EBSD map is linked\n", + " self.checkEbsdLinked()\n", + "\n", + " if (cropImage or type(self.ebsdTransform) is not tf.AffineTransform):\n", + " # crop to size of DIC map\n", + " outputShape = (self.yDim, self.xDim)\n", + " # warp the map\n", + " warpedMap = tf.warp(\n", + " mapData, self.ebsdTransform,\n", + " output_shape=outputShape,\n", + " order=order, preserve_range=preserve_range\n", + " )\n", + " else:\n", + " # copy ebsd transform and change translation to give an extra\n", + " # 5% border to show the entire image after rotation/shearing\n", + " tempEbsdTransform = tf.AffineTransform(matrix=np.copy(self.ebsdTransform.params))\n", + " tempEbsdTransform.params[0:2, 2] = -0.05 * np.array(mapData.shape)\n", + "\n", + " # output the entire warped image with 5% border (add some\n", + " # extra to fix a bug)\n", + " outputShape = np.array(mapData.shape) * 1.4 / tempEbsdTransform.scale\n", + "\n", + " # warp the map\n", + " warpedMap = tf.warp(\n", + " mapData, tempEbsdTransform,\n", + " output_shape=outputShape.astype(int),\n", + " order=order, preserve_range=preserve_range\n", + " )\n", + "\n", + " return warpedMap\n", + "\n", + "\n", + " @reportProgress(\"finding grains\")\n", + " def findGrains(self, algorithm=None, minGrainSize=10):\n", + " \"\"\"Finds grains in the DIC map.\n", + "\n", + " Parameters\n", + " ----------\n", + " algorithm : str {'warp', 'floodfill'}\n", + " Use floodfill or warp algorithm.\n", + " minGrainSize : int\n", + " Minimum grain area in pixels for floodfill algorithm.\n", + " \"\"\"\n", + " # Check a EBSD map is linked\n", + " self.checkEbsdLinked()\n", + "\n", + " if algorithm is None:\n", + " algorithm = defaults['hrdic_grain_finding_method']\n", + "\n", + " if algorithm == 'warp':\n", + " # Warp EBSD grain map to DIC frame\n", + " self.grains = self.warpToOpticalFrame(self.ebsdMap.grains, cropImage=True,\n", + " order=0, preserve_range=True)\n", + "\n", + " # Find all unique values (these are the EBSD grain IDs in the DIC area, sorted)\n", + " self.ebsdGrainIds = np.array([int(i) for i in np.unique(self.grains) if i>0])\n", + "\n", + " # Make a new list of sequential IDs of same length as number of grains\n", + " dicGrainIds = np.arange(1, len(self.ebsdGrainIds)+1)\n", + "\n", + " # Map the EBSD IDs to the DIC IDs (keep the same mapping for values <= 0)\n", + " negVals = np.array([i for i in np.unique(self.grains) if i<=0])\n", + " old = np.concatenate((negVals, self.ebsdGrainIds))\n", + " new = np.concatenate((negVals, dicGrainIds))\n", + " index = np.digitize(self.grains.ravel(), old, right=True)\n", + " self.grains = new[index].reshape(self.grains.shape)\n", + "\n", + " self.grainList = []\n", + " for i, (dicGrainId, ebsdGrainId) in enumerate(zip(dicGrainIds, self.ebsdGrainIds)):\n", + " yield i / len(dicGrainIds) # Report progress\n", + "\n", + " # Make grain object\n", + " currentGrain = Grain(grainID=dicGrainId, dicMap=self)\n", + "\n", + " # Find (x,y) coordinates and corresponding max shears of grain\n", + " coords = np.argwhere(self.grains == dicGrainId) # (y,x)\n", + " currentGrain.coordList = np.flip(coords, axis=1) # (x,y)\n", + " currentGrain.maxShearList = self.optical[coords[:,0]+ self.cropDists[1, 0], \n", + " coords[:,1]+ self.cropDists[0, 0]]\n", + "\n", + " # Assign EBSD grain ID to DIC grain and increment grain list\n", + " currentGrain.ebsdGrainId = ebsdGrainId - 1\n", + " currentGrain.ebsdGrain = self.ebsdMap.grainList[ebsdGrainId - 1]\n", + " currentGrain.ebsdMap = self.ebsdMap\n", + " self.grainList.append(currentGrain)\n", + "\n", + " elif algorithm == 'floodfill':\n", + " # Initialise the grain map\n", + " self.grains = np.copy(self.boundaries)\n", + "\n", + " self.grainList = []\n", + "\n", + " # List of points where no grain has been set yet\n", + " points_left = self.grains == 0\n", + " total_points = points_left.sum()\n", + " found_point = 0\n", + " next_point = points_left.tobytes().find(b'\\x01')\n", + "\n", + " # Start counter for grains\n", + " grainIndex = 1\n", + "\n", + " # Loop until all points (except boundaries) have been assigned\n", + " # to a grain or ignored\n", + " i = 0\n", + " while found_point >= 0:\n", + " # Flood fill first unknown point and return grain object\n", + " idx = np.unravel_index(next_point, self.grains.shape)\n", + " currentGrain = self.floodFill(idx[1], idx[0], grainIndex,\n", + " points_left)\n", + "\n", + " if len(currentGrain) < minGrainSize:\n", + " # if grain size less than minimum, ignore grain and set\n", + " # values in grain map to -2\n", + " for coord in currentGrain.coordList:\n", + " self.grains[coord[1], coord[0]] = -2\n", + " else:\n", + " # add grain to list and increment grain index\n", + " self.grainList.append(currentGrain)\n", + " grainIndex += 1\n", + "\n", + " # find next search point\n", + " points_left_sub = points_left.reshape(-1)[next_point + 1:]\n", + " found_point = points_left_sub.tobytes().find(b'\\x01')\n", + " next_point += found_point + 1\n", + "\n", + " # report progress\n", + " i += 1\n", + " if i == defaults['find_grain_report_freq']:\n", + " yield 1. - points_left_sub.sum() / total_points\n", + " i = 0\n", + "\n", + " # Now link grains to those in ebsd Map\n", + " # Warp DIC grain map to EBSD frame\n", + " dicGrains = self.grains\n", + " warpedDicGrains = tf.warp(\n", + " np.ascontiguousarray(dicGrains.astype(float)),\n", + " self.ebsdTransformInv,\n", + " output_shape=(self.ebsdMap.yDim, self.ebsdMap.xDim),\n", + " order=0\n", + " ).astype(int)\n", + "\n", + " # Initialise list to store ID of corresponding grain in EBSD map.\n", + " # Also stored in grain objects\n", + " self.ebsdGrainIds = []\n", + "\n", + " for i in range(len(self)):\n", + " # Find grain by masking the native ebsd grain image with\n", + " # selected grain from the warped dic grain image. The modal\n", + " # value is the EBSD grain label.\n", + " modeId, _ = mode(self.ebsdMap.grains[warpedDicGrains == i + 1])\n", + " ebsd_grain_idx = modeId[0] - 1\n", + " self.ebsdGrainIds.append(ebsd_grain_idx)\n", + " self[i].ebsdGrainId = ebsd_grain_idx\n", + " self[i].ebsdGrain = self.ebsdMap[ebsd_grain_idx]\n", + " self[i].ebsdMap = self.ebsdMap\n", + "\n", + " else:\n", + " raise ValueError(f\"Unknown grain finding algorithm '{algorithm}'.\")\n", + "\n", + "\n", + " def floodFill(self, x, y, grainIndex, points_left):\n", + " \"\"\"Flood fill algorithm that uses the combined x and y boundary array \n", + " to fill a connected area around the seed point. The points are inserted\n", + " into a grain object and the grain map array is updated.\n", + "\n", + " Parameters\n", + " ----------\n", + " x : int\n", + " Seed point x for flood fill\n", + " y : int\n", + " Seed point y for flood fill\n", + " grainIndex : int\n", + " Value to fill in grain map\n", + " points_left : numpy.ndarray\n", + " Boolean map of the points that have not been assigned a grain yet\n", + "\n", + " Returns\n", + " -------\n", + " currentGrain : defdap.hrdic.Grain\n", + " New grain object with points added\n", + "\n", + " \"\"\"\n", + " # create new grain\n", + " currentGrain = Grain(grainIndex - 1, self)\n", + "\n", + " # add first point to the grain\n", + " currentGrain.addPoint((x, y), self.optical[y + self.cropDists[1, 0],\n", + " x + self.cropDists[0, 0]])\n", + " self.grains[y, x] = grainIndex\n", + " points_left[y, x] = False\n", + " edge = [(x, y)]\n", + "\n", + " while edge:\n", + " x, y = edge.pop(0)\n", + "\n", + " moves = [(x+1, y), (x-1, y), (x, y+1), (x, y-1)]\n", + " # get rid of any that go out of the map area\n", + " if x <= 0:\n", + " moves.pop(1)\n", + " elif x >= self.xDim - 1:\n", + " moves.pop(0)\n", + " if y <= 0:\n", + " moves.pop(-1)\n", + " elif y >= self.yDim - 1:\n", + " moves.pop(-2)\n", + "\n", + " for (s, t) in moves:\n", + " addPoint = False\n", + "\n", + " if self.grains[t, s] == 0:\n", + " addPoint = True\n", + " edge.append((s, t))\n", + "\n", + " elif self.grains[t, s] == -1 and (s > x or t > y):\n", + " addPoint = True\n", + "\n", + " if addPoint:\n", + " currentGrain.addPoint(\n", + " (s, t),\n", + " self.optical[t + self.cropDists[1, 0],\n", + " s + self.cropDists[0, 0]]\n", + " )\n", + " self.grains[t, s] = grainIndex\n", + " points_left[t, s] = False\n", + "\n", + " return currentGrain\n", + "\n", + " def runGrainInspector(self, vmax=0.1, corrAngle=0):\n", + " \"\"\"Run the grain inspector interactive tool.\n", + "\n", + " Parameters\n", + " ----------\n", + " vmax : float\n", + " Maximum value of the colour map.\n", + " corrAngle: float\n", + " Correction angle in degrees to subtract from measured angles to account\n", + " for small rotation between DIC and EBSD frames. Approximately the rotation\n", + " component of affine transform.\n", + "\n", + " \"\"\"\n", + " GrainInspector(currMap=self, vmax=vmax, corrAngle=corrAngle)\n", + "\n", + "\n", + "\n", + "\n", + "#---------------------------------------------------------------------------------------------------------------------------------------------------------\n", + "\n", + "\n", + "class Grain(base.Grain):\n", + " \"\"\"\n", + " Class to encapsulate DIC grain data and useful analysis and plotting\n", + " methods.\n", + "\n", + " Attributes\n", + " ----------\n", + " dicMap : defdap.hrdic.Map\n", + " DIC map this grain is a member of\n", + " ownerMap : defdap.hrdic.Map\n", + " DIC map this grain is a member of\n", + " maxShearList : list\n", + " List of maximum shear values for grain.\n", + " ebsdGrain : defdap.ebsd.Grain\n", + " EBSD grain ID that this DIC grain corresponds to.\n", + " ebsdMap : defdap.ebsd.Map\n", + " EBSD map that this DIC grain belongs to.\n", + " pointsList : numpy.ndarray\n", + " Start and end points for lines drawn using defdap.inspector.GrainInspector.\n", + " groupsList :\n", + " Groups, angles and slip systems detected for\n", + " lines drawn using defdap.inspector.GrainInspector.\n", + "\n", + " \"\"\"\n", + " def __init__(self, grainID, dicMap):\n", + " # Call base class constructor\n", + " super(Grain, self).__init__(grainID, dicMap)\n", + "\n", + " self.dicMap = self.ownerMap # DIC map this grain is a member of\n", + " self.maxShearList = []\n", + " self.ebsdGrain = None\n", + " self.ebsdMap = None\n", + "\n", + " self.pointsList = [] # Lines drawn for STA\n", + " self.groupsList = [] # Unique angles drawn for STA\n", + "\n", + " @property\n", + " def plotDefault(self):\n", + " return lambda *args, **kwargs: self.plotMaxShear(\n", + " plotColourBar=True, plotScaleBar=True, plotSlipTraces=True,\n", + " plotSlipBands=True, *args, **kwargs\n", + " )\n", + "\n", + " # coord is a tuple (x, y)\n", + " def addPoint(self, coord, maxShear):\n", + " self.coordList.append(coord)\n", + " self.maxShearList.append(maxShear)\n", + "\n", + " def plotMaxShear(self, **kwargs):\n", + " \"\"\"Plot a maximum shear map for a grain.\n", + "\n", + " Parameters\n", + " ----------\n", + " kwargs\n", + " All arguments are passed to :func:`defdap.base.plotGrainData`.\n", + "\n", + " Returns\n", + " -------\n", + " defdap.plotting.GrainPlot\n", + "\n", + " \"\"\"\n", + " # Set default plot parameters then update with any input\n", + " plotParams = {\n", + " 'plotColourBar': True,\n", + " 'clabel': \"Effective shear strain\"\n", + " }\n", + " plotParams.update(kwargs)\n", + "\n", + " plot = self.plotGrainData(grainData=self.maxShearList, **plotParams)\n", + "\n", + " return plot\n", + "\n", + " @property\n", + " def refOri(self):\n", + " \"\"\"Returns average grain orientation.\n", + "\n", + " Returns\n", + " -------\n", + " defdap.quat.Quat\n", + "\n", + " \"\"\"\n", + " return self.ebsdGrain.refOri\n", + "\n", + " @property\n", + " def slipTraces(self):\n", + " \"\"\"Returns list of slip trace angles based on EBSD grain orientation.\n", + "\n", + " Returns\n", + " -------\n", + " list\n", + "\n", + " \"\"\"\n", + " return self.ebsdGrain.slipTraces\n", + "\n", + " def calcSlipTraces(self, slipSystems=None):\n", + " \"\"\"Calculates list of slip trace angles based on EBSD grain orientation.\n", + "\n", + " Parameters\n", + " -------\n", + " slipSystems : defdap.crystal.SlipSystem, optional\n", + "\n", + " \"\"\"\n", + " self.ebsdGrain.calcSlipTraces(slipSystems=slipSystems)\n", + "\n", + " def calcSlipBands(self, grainMapData, thres=None, min_dist=None):\n", + " \"\"\"Use Radon transform to detect slip band angles.\n", + "\n", + " Parameters\n", + " ----------\n", + " grainMapData : numpy.ndarray\n", + " Data to find bands in.\n", + " thres : float, optional\n", + " Normalised threshold for peaks.\n", + " min_dist : int, optional\n", + " Minimum angle between bands.\n", + "\n", + " Returns\n", + " ----------\n", + " list(float)\n", + " Detected slip band angles\n", + "\n", + " \"\"\"\n", + " if thres is None:\n", + " thres = 0.3\n", + " if min_dist is None:\n", + " min_dist = 30\n", + " grainMapData = np.nan_to_num(grainMapData)\n", + "\n", + " if grainMapData.min() < 0:\n", + " print(\"Negative values in data, taking absolute value.\")\n", + " # grainMapData = grainMapData**2\n", + " grainMapData = np.abs(grainMapData)\n", + " suppGMD = np.zeros(grainMapData.shape) #array to hold shape / support of grain\n", + " suppGMD[grainMapData!=0]=1\n", + " sin_map = tf.radon(grainMapData, circle=False)\n", + " #profile = np.max(sin_map, axis=0) # old method\n", + " supp_map = tf.radon(suppGMD, circle=False)\n", + " supp_1 = np.zeros(supp_map.shape)\n", + " supp_1[supp_map>0]=1\n", + " mindiam = np.min(np.sum(supp_1, axis=0), axis=0) # minimum diameter of grain\n", + " crop_map = np.zeros(sin_map.shape)\n", + " # only consider radon rays that cut grain with mindiam*2/3 or more, and scale by length of the cut\n", + " crop_map[supp_map>mindiam*2/3] = sin_map[supp_map>mindiam*2/3]/supp_map[supp_map>mindiam*2/3] \n", + " supp_crop = np.zeros(crop_map.shape)\n", + " supp_crop[crop_map>0] = 1\n", + " profile = np.sum(crop_map**4, axis=0) / np.sum(supp_crop, axis=0) # raise to power to accentuate local peaks\n", + "\n", + " x = np.arange(180)\n", + "\n", + " # indexes = peakutils.indexes(profile, thres=thres, min_dist=min_dist, thres_abs=False)\n", + " indexes = peakutils.indexes(profile, thres=thres, min_dist=min_dist)\n", + " peaks = x[indexes]\n", + " # peaks = peakutils.interpolate(x, profile, ind=indexes)\n", + " print(\"Number of bands detected: {:}\".format(len(peaks)))\n", + "\n", + " slipBandAngles = peaks\n", + " slipBandAngles = slipBandAngles * np.pi / 180\n", + " return slipBandAngles\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4d9727c7-41d2-43f6-838e-94728799c09d", + "metadata": {}, + "outputs": [], + "source": [ + "dirct = path + fname + extension\n", + "dirct" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c9242930-6e88-44fb-b246-8b1b8f99108d", + "metadata": {}, + "outputs": [], + "source": [ + "x= plt.imread(path+fname+'.'+extension)\n", + "plt.imshow(x)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "662b587a-e00d-4b15-8e1f-09e80e72d555", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/defdap/optical.py b/defdap/optical.py index 79f51d1..6b26425 100644 --- a/defdap/optical.py +++ b/defdap/optical.py @@ -7,6 +7,7 @@ from defdap.utils import report_progress import numpy as np + class Map(base.Map): ''' This class is for import and analysiing optical image data @@ -61,17 +62,36 @@ def __init__(self, *args, **kwargs): self.crop_dists = np.array(((0, 0), (0, 0)), dtype=int) self.file_name = None self.shape= None + self.binning = 1 + + self.plot_default = lambda *args, **kwargs: self.plot_map(map_name='optical', + plot_gbs=True, *args, **kwargs) + + self.homog_map_name = 'optical' + + + self.data.add_generator( + 'grains', self.find_grains, unit='', type='map', order=0, + cropped=True + ) + + self.data.add_generator( + 'optical', self.load_data, # This should point to your load_data method + unit='', type='map', order=0, + save=False, + plot_params={'cmap': 'gray'} + ) + @report_progress("loading Optical data") def load_data(self, file_name, data_type=None): - """Load DIC data from file. + """Load optical data from file. Parameters ---------- file_name : pathlib.Path Name of file including extension. - data_type : str, {'Davis', 'OpenPIV'} - Type of data file. + data_type : str, not sure is relavent? """ loader = MatplotlibLoader(file_name) @@ -88,6 +108,14 @@ def load_data(self, file_name, data_type=None): f"(dimensions: {self.xdim} x {self.ydim} pixels) " ) + def load_metadata_from_excel(self, file_path): + """Load metadata from an Excel file and convert it into a list of dictionaries.""" + # Read the Excel file into a DataFrame + df = pd.read_excel(file_path) + + # Convert each row in the DataFrame to a dictionary and store in a list + self.metadata = df.to_dict(orient='records') + def set_scale(self, scale): """Sets the scale of the map. @@ -188,12 +216,139 @@ def plot_optical_image(self, **kwargs): ) return plot_instance - ''' - def plot_optical_image(self,map_data, **kwargs): - """Uses the Plot class to display the optical image.""" - plot_params = {} - plot_params.update(kwargs) - return MapPlot.create(self, **plot_params) - ''' - - \ No newline at end of file + @report_progress("finding grains") + def find_grains(self, algorithm=None, min_grain_size=10): + """Finds grains in the DIC map. + + Parameters + ---------- + algorithm : str {'warp', 'floodfill'} + Use floodfill or warp algorithm. + min_grain_size : int + Minimum grain area in pixels for floodfill algorithm. + """ + # Check a EBSD map is linked + self.check_ebsd_linked() + + if algorithm is None: + algorithm = defaults['hrdic_grain_finding_method'] + algorithm = algorithm.lower() + + grain_list = [] + group_id = Datastore.generate_id() + + if algorithm == 'warp': + # Warp EBSD grain map to DIC frame + grains = self.warp_to_dic_frame( + self.ebsd_map.data.grains, order=0, preserve_range=True + ) + + # Find all unique values (these are the EBSD grain IDs in the DIC area, sorted) + ebsd_grain_ids = np.unique(grains) + neg_vals = ebsd_grain_ids[ebsd_grain_ids <= 0] + ebsd_grain_ids = ebsd_grain_ids[ebsd_grain_ids > 0] + + # Map the EBSD IDs to the DIC IDs (keep the same mapping for values <= 0) + old = np.concatenate((neg_vals, ebsd_grain_ids)) + 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) + + # Make grain object + grain = Grain(dic_grain_id, self, group_id) + + # Find (x,y) coordinates and corresponding max shears of grain + 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 + grain.ebsd_grain = self.ebsd_map[ebsd_grain_id - 1] + grain.ebsd_map = self.ebsd_map + grain_list.append(grain) + + elif algorithm == 'floodfill': + # Initialise the grain map + grains = -np.copy(self.data.grain_boundaries.image.astype(int)) + + # List of points where no grain has been set yet + points_left = grains == 0 + 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') + + # Start counter for grains + grain_index = 1 + # Loop until all points (except boundaries) have been assigned + # to a grain or ignored + i = 0 + 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_dic( + (seed[1], seed[0]), grain_index, points_left, + 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 + # values in grain map to -2 + for point in grain.data.point: + grains[point[1], point[0]] = -2 + else: + # add grain to list and increment grain index + grain_list.append(grain) + grain_index += 1 + + # find next search point + points_left_sub = points_left.reshape(-1)[next_point + 1:] + found_point = points_left_sub.tobytes().find(b'\x01') + next_point += found_point + 1 + + # report progress + i += 1 + if i == defaults['find_grain_report_freq']: + yield 1. - points_left_sub.sum() / total_points + i = 0 + + # Now link grains to those in ebsd Map + # Warp DIC grain map to EBSD frame + warped_dic_grains = self.experiment.warp_image( + grains.astype(float), self.frame, self.ebsd_map.frame, + output_shape=self.ebsd_map.shape, order=0 + ).astype(int) + for i, grain in enumerate(grain_list): + # Find grain by masking the native ebsd grain image with + # selected grain from the warped dic grain image. The modal + # value is the EBSD grain label. + mode_id, _ = mode( + self.ebsd_map.data.grains[warped_dic_grains == i+1], + keepdims=False + ) + grain.ebsd_grain = self.ebsd_map[mode_id - 1] + grain.ebsd_map = self.ebsd_map + + else: + raise ValueError(f"Unknown grain finding algorithm '{algorithm}'.") + + ## TODO: this will get duplicated if find grains called again + self.data.add_derivative( + grain_list[0].data, self.grain_data_to_map, pass_ref=True, + in_props={ + 'type': 'list' + }, + out_props={ + 'type': 'map' + } + ) + + self._grains = grain_list + return grains diff --git a/defdap/test_data/test-meta-data.xlsx b/defdap/test_data/test-meta-data.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..d565f9a5d2978444f57e0a25aa489fb04dd66065 GIT binary patch literal 9412 zcmeHN1y>wdx9#8rf)lLqG(m&AOK=N>kU((P#$6lN1cJMTAPvC@?jC5|U6TMIxHqpe zZ|2QRX5KIOzT0cnUA?OISylI*efFtyN?i#Vg$RHKzyJUMv;gDpnU?zZ0D!kB001EX z14&mJZ0}-b?_%)W)4|MH?}>+kyEyQkp3C1zZ?Y*a zR!Ly+?nC+rn4gKYcfac`G11MkvbxKPxW<#pCAQ|TB!De?a~%#_R@vLtLBe~RG_a!r z``fkjDfroY`u0F=BxLbUImDit2`s^Z9#5}n?+HY{yEuYJ7Q7-OSO z)UO)(h%F7+bh9J^X(8jY0Ck(Fnuldv_AuXWapHXCp&y+zOV^+^LSXj_g=u5fo42N_ zq;z(ytMB`=WYh%tnIV23`UgGz2MmIh8{w$AKNejfiz z$Nyjs{^il3@hWOv9N3`;@;71q=Tl2>@uZa8CFPrFpZf(UEaKM22O32{BuroDUKW~s<>-pe%;1zH>j+)z#&?@KojOaEQ}kqX zYk9+5`l%>aX<&t3cH&T`5)bx7n;etyJuvj4XlkHNuga@s^E5EEZ9kStZzH_* zVN=cM*P!DexZr}x^?`Cu{dH=14u>*5yZJG@O8b8dK%%INmJtf|Pm=J#M0%o;0f04x zl;I*)#>1A~9qeRd3jhMTHV#r@RCq(d+02A1ZGW#&=zRC*=c zPEaneyf%K4jLB1_?+VNAl>Oa`f77DVVa@i90&+kj(RKI8Yp`<4DJ<=ZFURaZRZu+c^y+Ne1b*&QL`-N9(Jg z12MjuUXX@9FQC#K^p0J$s9ej+qqew#W|yFI*+_c<^TO*#7X$XR1A%QOP^j%`qb)a>THGY4us}r*vBA1>??VacMe>z| za>VmI+C59^m)eyPoVHl|ri-=qvE;PSDScAk?1b!4wkjc^G-dnnsY+7P4iio*P z{FLxq`~=k{ZYQXCFx|1B`R>lyI-mgq-Emnlx8tt#g8aUVTvqODn#jG;VQqC>Tk+{) zjKd9^&FieKUT&VQF>JxQt^9Ug6QZ8kGf`nuBiJQQ2L`*c;8#;ebqIuN7boG?bT@P_ zl+reoa%kDIIV}uV<5;b$;#?0E`n}m(wthd|d{SdUv_fcOp)$5Y<7%jHt8T@|FqM_2 zKh7j@X!#7?EFFo|q?%s}%xG?M%lku*TYe94dB!85%9WjO`OT}JI&jXF(kA#eEAB8> z!@mis)^GY-EB*J$n5Z>!ZRc!LJ;Gce>DOkT*w=rMVmuDPQP(^W=q%*bpmKTqQFjhT zBZhWTVWR5myh+1dskezjkAyjx>`Detkb) ztYgSp@;JPPrpQ;K{akde{{h_uTI2{278fMP5J?A zN=?1JBcuk-^YCiDbE!;$*DAw-u^e#DTv-DL-ZL&*A*C4%chL#QSq#ipa!&DG?;0YM}GD*vF(ed;QJ#ch6V@+B?avhNyp@;Qq;;_7B6U5;)V6TMI&^YizB$l0wSw5` zf4YpP)CbQTM68#-4*-xNuJ}FHJ6oEWxj3``HgWz8`WXoW<>?$mq34NLIQ`cg(hfvU zQ_A18__g|KO)|Rg_fEPoX!)K~ac}A~rQ=}=pmVJFGjRJhgpUhyzHfj!5q%PKdim6> zqf{@}5B~BE*QTX(2_&J)Sg?^RM&F!TTp6Z8d6LDs0mvN}oTt!K6H%KGC6sBZGAC}H zQv`(v>ee2+l#w!}y^;AE(wglB`EkECIKnHKwq93tPGdaZFwu%OGO=6Y#tUY%G=bua z@;F=wxQm~dht?gMy!e`e(FrB9`8#uQe_?PlyQFfF^D~EF2iX?o;~P~4hmwLM-3_EU z8DG$IP+0y; zQwQmXxr;;jbsVI+H>P{C(#Q8&7>FuFL2et5$Ti;8m<0H?GHf7DepC<;p}g?r`x>w> zt$`iMg39RR+!UlTZ?DK!IgcqoV5XzQ*tRM`a06u7aJ)vrc|BOLAS?l(X5 znb^gF&=8^j&~_9Fz;LV1)irbTzAtO<`z(ESe_y)n1o+a=kkD@<`WQWz-E+C1DlVd^ zG)%iMm{85Ytk|S8DLOUVUUA7qllYYuNfjqcw|7;<^Pyvw;o3nZZjy_&ghx`f-q3j! zA@hedsV$EKbIxVdPw{v4Qw|@K5>eB^{Q04XWb6ji+1FrXt)c*@ejp0* zTuhWhw%n>T<%&P&?dt(#0jYo|dt=Y2DA#%BD`7!6u-ym$_v}?n^2%zbDy)jI&nz$c z6;xO%wYWyPwKW(Am0@EfaLEY0l9*nd0zM(91A;WrfZ#BJ#P zcM_lVrayo2HWD2Dz<>7LYb{Z4Q(hq>HWU}6HP%dH$<*X$bP2!uIvs4h`M!=!9G61#R&Dd%cds~&uadRQ797!A zlv(j8e!33$CaCqsuQIWGFuhD(Jn(IM6~n0_lCVlTDW=2vB|0bK#n+_;MrzJaDhZRw z+zcZ%*2W5?dp9DZ7!66t45O&#dbr!E$3vJ68dSzFnJ;p8FSlW(%T2mH=nZ(9-lj@U zCdbJ`TS&*pqo0|QCiV;9jJ7jDWq!zp)+GGayLV+`$nDi+B38Zcq1=`{w{9sWN;!Mu zxlI%It@&CD+UmLlzwlfeEscXEQsGzMxnqU%jk-E1)I-60tKNRI2a~ML_slM~aazT` z)po{(@aKe&ZTgTb+14%$16x8^1%zEXg@+4Q$hUtWp^jC=JQ3}IJhnD~_FRyWm;fwk zx*{3m%QUOnxx?4>OFqd+Gi!#^3Bl4z*_K8y|{b zsk_Bto1L#&k-x@O!r=C zJ1$^{P4JdBw0qm-np3xrPlM6x`?zPYAUl27jSt?MfU8rX!t-7~W*Dsur_k0Qs`&QV z{ju{^^o%{%b4^87_ck$nkw6sf!sjUAa5E6FwOQ>`3)PHmUW88L%u(6H+E6?=r-RmTg41H!d>sNN{}OPNq~6QsO?$>m$fLZ zD7iP6hitX+zcQ(KlD*`j2f=Qr#C~+fUpI`xZ3Ce*WJt-Q*KS0`AG!?*U)GM99b~Gn zAhBCRV~mD+JuV#Bjm#!wMQOY7w9dwTar%k4O{t}t{UcDn)ah)^FQj%D?_<0s(!zlK zdLx^iy%NR2U2Dx;eG(WeZ2_`fm*dz1@qG&^EHcVzX3`NYJS05Wzh-4Lv9KQQkK-w$ z#{9^8%!!?rm0+c8e;r_jl6>W9h=f&7G}}+pF$FG)9HGmQ@@M4G`~3d17CT6>A-FD% zjPLfjlS;jnSlPOX7;HYXux-k$Qh95Gy+MGa5$@5VAx+BS9u=G-eUfPS@(14^C z%!qMQzKFCS85`~ka)L(ZGryNEK-PJk+g>fW)$3~} z=hNP%dBKCJiRqKA0{&WFhv1(dAcNB ze-|fq;6HW!;86TjE#0`9{62ZvS=f(SRk|4`WTnAW2*} ztw(|1d8aLA7&|wpiujJN7(}e`pfZ{(DjoXn@^N7#Mz`hS>B%v;D-gsGVY(?);o@Js zDb=#-XSLbBTbDdwENNb{cRn6o=i-LNB&PIm#31HkA*l9|L0}Us?-G<&mFN*o+f803 z;q9&p0#c)-mhKRZbk(Io!`p0xHM4HZq|523O_F2X{BTJ6rBFu_G-=TlAzR>r1qs*!?oAQzHK}uN{Ms2 z$9YSeOj*k3yh5^rZKKSZ<7esf-cSt1CN<|bOLghN69nL<)1bxEqAB6-!U~>1FwV%3 z#e&Zjc6=Y6D!w+W@V+_bV(K`TC~tq6jr=U1J{;OEy3-u zK2P3u0*UlAk{7ctMqlmELArqa5swVc+MJfKH7CvjtL;NnY{s7I>e;DIx|Z3S zPvZ<_%@-J%FnyY@rK=f04ILL0{ygl&zM0CBs6Fox@&e-4vT_xp?~BKWM%=bUs`ah5 z(ZGt|t5)&jSP^Qz>?-c{+Kz@unzx1aJ#l$-^Iiq6)gZ!GUnHFB)xhy!E$ zVk*Xgfb#F>#YAuVkT~Kteu?^d!>u3t4sMvgQC}%VjxvY&!Lq5M7F) zDWw)Mu}&rSK}rwFWgl*_t8mA6zGy)dyg_r1jn~}t-ANP-#iA1rG%GZ0=!=7jUlA^l z%ChY8D@d+$00|WuIz>B`lkSk{iW^q=B#X_YDGeMumdd+2<&&z@YsW*&QPl17@!6hH z#Hd8;I13a+wF<}`ja!!5xTAfNnc-!hQ)<-Y0@aAn-aA@&n-?5>SUs5w(8BN(OFqJ`6qk_W$Uey%89(#d{ln4S3H3{zD7UL3|d5w_Y>+1vUYia`0;nHleTJJD5t6LSvz@Why#0`qYa&a`A$fi1 zGy_0Kb=x&+Y;H6C_+aU&BiF1%Bl5A!dll3(F}*?Bgb$D@oN)5i<%^FK4{pk9oDBm- z$^`Umo`$f$M;FA&$%w~v+|gb2zQt}Ti<=*mYD{PD8dZi9;7Bu?itim>Mf6eUoz5c9 z;=i8WHF~B?eYjFdF;sfeptGZrc?8L=!6f^vVUmXl5g*$AUwtrV!Xn6^~VV3#XC zarvSAchdn&eU8UJU=;xF$yYo8$JVKV6W%U`~ zwedMBZ!(C~I1oV%qTgv@ zY2;*Ps_EinWpD8tJAC3-6uUTxdJj-nXg96t^n}c$Wf%0GM>gSwj-X@gzoJl9HC7Xv zuIJCp8MW8n9&-2F0apo<4abhgSEU^Fh-_n5)FgHaZBbv-d96n`cPm;)y*;7|2Ujhm z2lrRwMI{{dde=XM45CFBG`I$=?HKCP`+C&X(#$}hMfZNlMuiFJW6~2ly_m=G?QdF# zN}o0s@Fh#cllPDEBhW6V8AY0$`1Ra%<#DaMN8SEhbvwkW<0HyXreG6wC$NJvy9wCI?Dvl6 zf2&T2rHhX5u<4>9?Kud##qIFUvl@H}QZS+vEmMEl*K40=plijG=VoPSnH{QVJ6fya6C8%gXd$&WD`=0UD6?aWQK)KMai^!4z~ag`@w;eJ>y3< zsGVKA7v_`ukMf3lAOsXVL0n`b&;19wZgWf;uVyun6$Vr11%t)p}K~) zXmpJSz(KVI9-!X@;KZ?aRy zn8@%y25|N9Qy1L)YhTYu0TV9*;d=Op3IpC>UC+qD;lG}TaJk<{M!Y0=fdf0_0Qrg< zdx=7DNgX>7YAmmPQekv2zRp6W&?L9&Q8+YiZ{|(i*yNnox0p?Dk1H94C7_VO$=k6W z^@y!JB2*L#83FAfI?e#zbt)P9Q`dm>H z<49Kf&1x#>eF~O~zlWW6-h~|=T882YgD_!uCECJdbgi2FEAgJ2Xf{A@ylqOTiQxH& z&G}PyIb@QZURpyWqcx!8PCqi-nvY>ZxO+(Ou%F{lRbU|yEaW`Gub--R_1K%T$rSMl zuX)-PV&Nm-GYolED*)f0xfc@c_UH5CHgx mnEnd?dkFk198CQe`0oKxT?rMjy8ys_#4iA$*H7qv{`((woqrer literal 0 HcmV?d00001