From a74b583f69c4c02459efaa8db74981c86a897947 Mon Sep 17 00:00:00 2001 From: Stephanie Ribet Date: Thu, 7 Sep 2023 20:50:11 -0700 Subject: [PATCH 01/10] reorganization and bug fix --- py4DSTEM/__init__.py | 2 +- py4DSTEM/process/__init__.py | 2 +- py4DSTEM/process/latticevectors/__init__.py | 1 - py4DSTEM/process/latticevectors/fit.py | 129 ------ py4DSTEM/process/latticevectors/index.py | 131 ------ py4DSTEM/process/latticevectors/strain.py | 231 ---------- py4DSTEM/process/strain/__init__.py | 2 + py4DSTEM/process/strain/latticevectors.py | 448 ++++++++++++++++++++ py4DSTEM/process/{ => strain}/strain.py | 152 ++++--- 9 files changed, 537 insertions(+), 561 deletions(-) delete mode 100644 py4DSTEM/process/latticevectors/strain.py create mode 100644 py4DSTEM/process/strain/__init__.py create mode 100644 py4DSTEM/process/strain/latticevectors.py rename py4DSTEM/process/{ => strain}/strain.py (86%) diff --git a/py4DSTEM/__init__.py b/py4DSTEM/__init__.py index dcb6a861d..adf757d1b 100644 --- a/py4DSTEM/__init__.py +++ b/py4DSTEM/__init__.py @@ -53,7 +53,7 @@ ) # strain -from py4DSTEM.process import StrainMap +from py4DSTEM.process.strain.strain import StrainMap # TODO - crystal # TODO - ptycho diff --git a/py4DSTEM/process/__init__.py b/py4DSTEM/process/__init__.py index e711e907d..0df11ef01 100644 --- a/py4DSTEM/process/__init__.py +++ b/py4DSTEM/process/__init__.py @@ -1,5 +1,5 @@ from py4DSTEM.process.polar import PolarDatacube -from py4DSTEM.process.strain import StrainMap +from py4DSTEM.process.strain.strain import StrainMap from py4DSTEM.process import latticevectors from py4DSTEM.process import phase diff --git a/py4DSTEM/process/latticevectors/__init__.py b/py4DSTEM/process/latticevectors/__init__.py index 560a3b7e6..cda4f91e5 100644 --- a/py4DSTEM/process/latticevectors/__init__.py +++ b/py4DSTEM/process/latticevectors/__init__.py @@ -1,4 +1,3 @@ from py4DSTEM.process.latticevectors.initialguess import * from py4DSTEM.process.latticevectors.index import * from py4DSTEM.process.latticevectors.fit import * -from py4DSTEM.process.latticevectors.strain import * diff --git a/py4DSTEM/process/latticevectors/fit.py b/py4DSTEM/process/latticevectors/fit.py index 659bc8940..d36b10bca 100644 --- a/py4DSTEM/process/latticevectors/fit.py +++ b/py4DSTEM/process/latticevectors/fit.py @@ -7,135 +7,6 @@ from py4DSTEM.data import RealSlice -def fit_lattice_vectors(braggpeaks, x0=0, y0=0, minNumPeaks=5): - """ - Fits lattice vectors g1,g2 to braggpeaks given some known (h,k) indexing. - - Args: - braggpeaks (PointList): A 6 coordinate PointList containing the data to fit. - Coords are 'qx','qy' (the bragg peak positions), 'intensity' (used as a - weighting factor when fitting), 'h','k' (indexing). May optionally also - contain 'index_mask' (bool), indicating which peaks have been successfully - indixed and should be used. - x0 (float): x-coord of the origin - y0 (float): y-coord of the origin - minNumPeaks (int): if there are fewer than minNumPeaks peaks found in braggpeaks - which can be indexed, return None for all return parameters - - Returns: - (7-tuple) A 7-tuple containing: - - * **x0**: *(float)* the x-coord of the origin of the best-fit lattice. - * **y0**: *(float)* the y-coord of the origin - * **g1x**: *(float)* x-coord of the first lattice vector - * **g1y**: *(float)* y-coord of the first lattice vector - * **g2x**: *(float)* x-coord of the second lattice vector - * **g2y**: *(float)* y-coord of the second lattice vector - * **error**: *(float)* the fit error - """ - assert isinstance(braggpeaks, PointList) - assert np.all( - [name in braggpeaks.dtype.names for name in ("qx", "qy", "intensity", "h", "k")] - ) - braggpeaks = braggpeaks.copy() - - # Remove unindexed peaks - if "index_mask" in braggpeaks.dtype.names: - deletemask = braggpeaks.data["index_mask"] == False - braggpeaks.remove(deletemask) - - # Check to ensure enough peaks are present - if braggpeaks.length < minNumPeaks: - return None, None, None, None, None, None, None - - # Get M, the matrix of (h,k) indices - h, k = braggpeaks.data["h"], braggpeaks.data["k"] - M = np.vstack((np.ones_like(h, dtype=int), h, k)).T - - # Get alpha, the matrix of measured Bragg peak positions - alpha = np.vstack((braggpeaks.data["qx"] - x0, braggpeaks.data["qy"] - y0)).T - - # Get weighted matrices - weights = braggpeaks.data["intensity"] - weighted_M = M * weights[:, np.newaxis] - weighted_alpha = alpha * weights[:, np.newaxis] - - # Solve for lattice vectors - beta = lstsq(weighted_M, weighted_alpha, rcond=None)[0] - x0, y0 = beta[0, 0], beta[0, 1] - g1x, g1y = beta[1, 0], beta[1, 1] - g2x, g2y = beta[2, 0], beta[2, 1] - - # Calculate the error - alpha_calculated = np.matmul(M, beta) - error = np.sqrt(np.sum((alpha - alpha_calculated) ** 2, axis=1)) - error = np.sum(error * weights) / np.sum(weights) - - return x0, y0, g1x, g1y, g2x, g2y, error - - -def fit_lattice_vectors_all_DPs(braggpeaks, x0=0, y0=0, minNumPeaks=5): - """ - Fits lattice vectors g1,g2 to each diffraction pattern in braggpeaks, given some - known (h,k) indexing. - - Args: - braggpeaks (PointList): A 6 coordinate PointList containing the data to fit. - Coords are 'qx','qy' (the bragg peak positions), 'intensity' (used as a - weighting factor when fitting), 'h','k' (indexing). May optionally also - contain 'index_mask' (bool), indicating which peaks have been successfully - indixed and should be used. - x0 (float): x-coord of the origin - y0 (float): y-coord of the origin - minNumPeaks (int): if there are fewer than minNumPeaks peaks found in braggpeaks - which can be indexed, return None for all return parameters - - Returns: - (RealSlice): A RealSlice ``g1g2map`` containing the following 8 arrays: - - * ``g1g2_map.get_slice('x0')`` x-coord of the origin of the best fit lattice - * ``g1g2_map.get_slice('y0')`` y-coord of the origin - * ``g1g2_map.get_slice('g1x')`` x-coord of the first lattice vector - * ``g1g2_map.get_slice('g1y')`` y-coord of the first lattice vector - * ``g1g2_map.get_slice('g2x')`` x-coord of the second lattice vector - * ``g1g2_map.get_slice('g2y')`` y-coord of the second lattice vector - * ``g1g2_map.get_slice('error')`` the fit error - * ``g1g2_map.get_slice('mask')`` 1 for successful fits, 0 for unsuccessful - fits - """ - assert isinstance(braggpeaks, PointListArray) - assert np.all( - [name in braggpeaks.dtype.names for name in ("qx", "qy", "intensity", "h", "k")] - ) - - # Make RealSlice to contain outputs - slicelabels = ("x0", "y0", "g1x", "g1y", "g2x", "g2y", "error", "mask") - g1g2_map = RealSlice( - data=np.zeros((8, braggpeaks.shape[0], braggpeaks.shape[1])), - slicelabels=slicelabels, - name="g1g2_map", - ) - - # Fit lattice vectors - for Rx, Ry in tqdmnd(braggpeaks.shape[0], braggpeaks.shape[1]): - braggpeaks_curr = braggpeaks.get_pointlist(Rx, Ry) - qx0, qy0, g1x, g1y, g2x, g2y, error = fit_lattice_vectors( - braggpeaks_curr, x0, y0, minNumPeaks - ) - # Store data - if g1x is not None: - g1g2_map.get_slice("x0").data[Rx, Ry] = qx0 - g1g2_map.get_slice("y0").data[Rx, Ry] = qx0 - g1g2_map.get_slice("g1x").data[Rx, Ry] = g1x - g1g2_map.get_slice("g1y").data[Rx, Ry] = g1y - g1g2_map.get_slice("g2x").data[Rx, Ry] = g2x - g1g2_map.get_slice("g2y").data[Rx, Ry] = g2y - g1g2_map.get_slice("error").data[Rx, Ry] = error - g1g2_map.get_slice("mask").data[Rx, Ry] = 1 - - return g1g2_map - - def fit_lattice_vectors_masked(braggpeaks, mask, x0=0, y0=0, minNumPeaks=5): """ Fits lattice vectors g1,g2 to each diffraction pattern in braggpeaks corresponding diff --git a/py4DSTEM/process/latticevectors/index.py b/py4DSTEM/process/latticevectors/index.py index 4ac7939e7..2d243cd0c 100644 --- a/py4DSTEM/process/latticevectors/index.py +++ b/py4DSTEM/process/latticevectors/index.py @@ -34,61 +34,6 @@ def get_selected_lattice_vectors(gx, gy, i0, i1, i2): return (g1x, g1y), (g2x, g2y) -def index_bragg_directions(x0, y0, gx, gy, g1, g2): - """ - From an origin (x0,y0), a set of reciprocal lattice vectors gx,gy, and an pair of - lattice vectors g1=(g1x,g1y), g2=(g2x,g2y), find the indices (h,k) of all the - reciprocal lattice directions. - - The approach is to solve the matrix equation - ``alpha = beta * M`` - where alpha is the 2xN array of the (x,y) coordinates of N measured bragg directions, - beta is the 2x2 array of the two lattice vectors u,v, and M is the 2xN array of the - h,k indices. - - Args: - x0 (float): x-coord of origin - y0 (float): y-coord of origin - gx (1d array): x-coord of the reciprocal lattice vectors - gy (1d array): y-coord of the reciprocal lattice vectors - g1 (2-tuple of floats): g1x,g1y - g2 (2-tuple of floats): g2x,g2y - - Returns: - (3-tuple) A 3-tuple containing: - - * **h**: *(ndarray of ints)* first index of the bragg directions - * **k**: *(ndarray of ints)* second index of the bragg directions - * **bragg_directions**: *(PointList)* a 4-coordinate PointList with the - indexed bragg directions; coords 'qx' and 'qy' contain bragg_x and bragg_y - coords 'h' and 'k' contain h and k. - """ - # Get beta, the matrix of lattice vectors - beta = np.array([[g1[0], g2[0]], [g1[1], g2[1]]]) - - # Get alpha, the matrix of measured bragg angles - alpha = np.vstack([gx - x0, gy - y0]) - - # Calculate M, the matrix of peak positions - M = lstsq(beta, alpha, rcond=None)[0].T - M = np.round(M).astype(int) - - # Get h,k - h = M[:, 0] - k = M[:, 1] - - # Store in a PointList - coords = [("qx", float), ("qy", float), ("h", int), ("k", int)] - temp_array = np.zeros([], dtype=coords) - bragg_directions = PointList(data=temp_array) - bragg_directions.add_data_by_field((gx, gy, h, k)) - mask = np.zeros(bragg_directions["qx"].shape[0]) - mask[0] = 1 - bragg_directions.remove(mask) - - return h, k, bragg_directions - - def generate_lattice(ux, uy, vx, vy, x0, y0, Q_Nx, Q_Ny, h_max=None, k_max=None): """ Returns a full reciprocal lattice stretching to the limits of the diffraction pattern @@ -163,82 +108,6 @@ def generate_lattice(ux, uy, vx, vy, x0, y0, Q_Nx, Q_Ny, h_max=None, k_max=None) return ideal_lattice -def add_indices_to_braggvectors( - braggpeaks, lattice, maxPeakSpacing, qx_shift=0, qy_shift=0, mask=None -): - """ - Using the peak positions (qx,qy) and indices (h,k) in the PointList lattice, - identify the indices for each peak in the PointListArray braggpeaks. - Return a new braggpeaks_indexed PointListArray, containing a copy of braggpeaks plus - three additional data columns -- 'h','k', and 'index_mask' -- specifying the peak - indices with the ints (h,k) and indicating whether the peak was successfully indexed - or not with the bool index_mask. If `mask` is specified, only the locations where - mask is True are indexed. - - Args: - braggpeaks (PointListArray): the braggpeaks to index. Must contain - the coordinates 'qx', 'qy', and 'intensity' - lattice (PointList): the positions (qx,qy) of the (h,k) lattice points. - Must contain the coordinates 'qx', 'qy', 'h', and 'k' - maxPeakSpacing (float): Maximum distance from the ideal lattice points - to include a peak for indexing - qx_shift,qy_shift (number): the shift of the origin in the `lattice` PointList - relative to the `braggpeaks` PointListArray - mask (bool): Boolean mask, same shape as the pointlistarray, indicating which - locations should be indexed. This can be used to index different regions of - the scan with different lattices - - Returns: - (PointListArray): The original braggpeaks pointlistarray, with new coordinates - 'h', 'k', containing the indices of each indexable peak. - """ - - # assert isinstance(braggpeaks,BraggVectors) - # assert isinstance(lattice, PointList) - # assert np.all([name in lattice.dtype.names for name in ('qx','qy','h','k')]) - - if mask is None: - mask = np.ones(braggpeaks.Rshape, dtype=bool) - - assert ( - mask.shape == braggpeaks.Rshape - ), "mask must have same shape as pointlistarray" - assert mask.dtype == bool, "mask must be boolean" - - coords = [ - ("qx", float), - ("qy", float), - ("intensity", float), - ("h", int), - ("k", int), - ] - - indexed_braggpeaks = PointListArray( - dtype=coords, - shape=braggpeaks.Rshape, - ) - - # loop over all the scan positions - for Rx, Ry in tqdmnd(mask.shape[0], mask.shape[1]): - if mask[Rx, Ry]: - pl = braggpeaks.cal[Rx, Ry] - for i in range(pl.data.shape[0]): - r2 = (pl.data["qx"][i] - lattice.data["qx"] + qx_shift) ** 2 + ( - pl.data["qy"][i] - lattice.data["qy"] + qy_shift - ) ** 2 - ind = np.argmin(r2) - if r2[ind] <= maxPeakSpacing**2: - indexed_braggpeaks[Rx, Ry].add_data_by_field( - ( - pl.data["qx"][i], - pl.data["qy"][i], - pl.data["intensity"][i], - lattice.data["h"][ind], - lattice.data["k"][ind], - ) - ) - - return indexed_braggpeaks def bragg_vector_intensity_map_by_index(braggpeaks, h, k, symmetric=False): diff --git a/py4DSTEM/process/latticevectors/strain.py b/py4DSTEM/process/latticevectors/strain.py deleted file mode 100644 index 6f4000449..000000000 --- a/py4DSTEM/process/latticevectors/strain.py +++ /dev/null @@ -1,231 +0,0 @@ -# Functions for calculating strain from lattice vector maps - -import numpy as np -from numpy.linalg import lstsq - -from py4DSTEM.data import RealSlice - - -def get_reference_g1g2(g1g2_map, mask): - """ - Gets a pair of reference lattice vectors from a region of real space specified by - mask. Takes the median of the lattice vectors in g1g2_map within the specified - region. - - Args: - g1g2_map (RealSlice): the lattice vector map; contains 2D arrays in g1g2_map.data - under the keys 'g1x', 'g1y', 'g2x', and 'g2y'. See documentation for - fit_lattice_vectors_all_DPs() for more information. - mask (ndarray of bools): use lattice vectors from g1g2_map scan positions wherever - mask==True - - Returns: - (2-tuple of 2-tuples) A 2-tuple containing: - - * **g1**: *(2-tuple)* first reference lattice vector (x,y) - * **g2**: *(2-tuple)* second reference lattice vector (x,y) - """ - assert isinstance(g1g2_map, RealSlice) - assert np.all( - [name in g1g2_map.slicelabels for name in ("g1x", "g1y", "g2x", "g2y")] - ) - assert mask.dtype == bool - g1x = np.median(g1g2_map.get_slice("g1x").data[mask]) - g1y = np.median(g1g2_map.get_slice("g1y").data[mask]) - g2x = np.median(g1g2_map.get_slice("g2x").data[mask]) - g2y = np.median(g1g2_map.get_slice("g2y").data[mask]) - return (g1x, g1y), (g2x, g2y) - - -def get_strain_from_reference_g1g2(g1g2_map, g1, g2): - """ - Gets a strain map from the reference lattice vectors g1,g2 and lattice vector map - g1g2_map. - - Note that this function will return the strain map oriented with respect to the x/y - axes of diffraction space - to rotate the coordinate system, use - get_rotated_strain_map(). Calibration of the rotational misalignment between real and - diffraction space may also be necessary. - - Args: - g1g2_map (RealSlice): the lattice vector map; contains 2D arrays in g1g2_map.data - under the keys 'g1x', 'g1y', 'g2x', and 'g2y'. See documentation for - fit_lattice_vectors_all_DPs() for more information. - g1 (2-tuple): first reference lattice vector (x,y) - g2 (2-tuple): second reference lattice vector (x,y) - - Returns: - (RealSlice) the strain map; contains the elements of the infinitessimal strain - matrix, in the following 5 arrays: - - * ``strain_map.get_slice('e_xx')``: change in lattice x-components with respect - to x - * ``strain_map.get_slice('e_yy')``: change in lattice y-components with respect - to y - * ``strain_map.get_slice('e_xy')``: change in lattice x-components with respect - to y - * ``strain_map.get_slice('theta')``: rotation of lattice with respect to - reference - * ``strain_map.get_slice('mask')``: 0/False indicates unknown values - - Note 1: the strain matrix has been symmetrized, so e_xy and e_yx are identical - """ - assert isinstance(g1g2_map, RealSlice) - assert np.all( - [name in g1g2_map.slicelabels for name in ("g1x", "g1y", "g2x", "g2y", "mask")] - ) - - # Get RealSlice for output storage - R_Nx, R_Ny = g1g2_map.get_slice("g1x").shape - strain_map = RealSlice( - data=np.zeros((5, R_Nx, R_Ny)), - slicelabels=("e_xx", "e_yy", "e_xy", "theta", "mask"), - name="strain_map", - ) - - # Get reference lattice matrix - g1x, g1y = g1 - g2x, g2y = g2 - M = np.array([[g1x, g1y], [g2x, g2y]]) - - for Rx in range(R_Nx): - for Ry in range(R_Ny): - # Get lattice vectors for DP at Rx,Ry - alpha = np.array( - [ - [ - g1g2_map.get_slice("g1x").data[Rx, Ry], - g1g2_map.get_slice("g1y").data[Rx, Ry], - ], - [ - g1g2_map.get_slice("g2x").data[Rx, Ry], - g1g2_map.get_slice("g2y").data[Rx, Ry], - ], - ] - ) - # Get transformation matrix - beta = lstsq(M, alpha, rcond=None)[0].T - - # Get the infinitesimal strain matrix - strain_map.get_slice("e_xx").data[Rx, Ry] = 1 - beta[0, 0] - strain_map.get_slice("e_yy").data[Rx, Ry] = 1 - beta[1, 1] - strain_map.get_slice("e_xy").data[Rx, Ry] = -(beta[0, 1] + beta[1, 0]) / 2.0 - strain_map.get_slice("theta").data[Rx, Ry] = (beta[0, 1] - beta[1, 0]) / 2.0 - strain_map.get_slice("mask").data[Rx, Ry] = g1g2_map.get_slice("mask").data[ - Rx, Ry - ] - return strain_map - - -def get_strain_from_reference_region(g1g2_map, mask): - """ - Gets a strain map from the reference region of real space specified by mask and the - lattice vector map g1g2_map. - - Note that this function will return the strain map oriented with respect to the x/y - axes of diffraction space - to rotate the coordinate system, use - get_rotated_strain_map(). Calibration of the rotational misalignment between real - and diffraction space may also be necessary. - - Args: - g1g2_map (RealSlice): the lattice vector map; contains 2D arrays in g1g2_map.data - under the keys 'g1x', 'g1y', 'g2x', and 'g2y'. See documentation for - fit_lattice_vectors_all_DPs() for more information. - mask (ndarray of bools): use lattice vectors from g1g2_map scan positions - wherever mask==True - - Returns: - (RealSlice) the strain map; contains the elements of the infinitessimal strain - matrix, in the following 5 arrays: - - * ``strain_map.get_slice('e_xx')``: change in lattice x-components with respect - to x - * ``strain_map.get_slice('e_yy')``: change in lattice y-components with respect - to y - * ``strain_map.get_slice('e_xy')``: change in lattice x-components with respect - to y - * ``strain_map.get_slice('theta')``: rotation of lattice with respect to - reference - * ``strain_map.get_slice('mask')``: 0/False indicates unknown values - - Note 1: the strain matrix has been symmetrized, so e_xy and e_yx are identical - """ - assert isinstance(g1g2_map, RealSlice) - assert np.all( - [name in g1g2_map.slicelabels for name in ("g1x", "g1y", "g2x", "g2y", "mask")] - ) - assert mask.dtype == bool - - g1, g2 = get_reference_g1g2(g1g2_map, mask) - strain_map = get_strain_from_reference_g1g2(g1g2_map, g1, g2) - return strain_map - - -def get_rotated_strain_map(unrotated_strain_map, xaxis_x, xaxis_y, flip_theta): - """ - Starting from a strain map defined with respect to the xy coordinate system of - diffraction space, i.e. where exx and eyy are the compression/tension along the Qx - and Qy directions, respectively, get a strain map defined with respect to some other - right-handed coordinate system, in which the x-axis is oriented along (xaxis_x, - xaxis_y). - - Args: - xaxis_x,xaxis_y (float): diffraction space (x,y) coordinates of a vector - along the new x-axis - unrotated_strain_map (RealSlice): a RealSlice object containing 2D arrays of the - infinitessimal strain matrix elements, stored at - * unrotated_strain_map.get_slice('e_xx') - * unrotated_strain_map.get_slice('e_xy') - * unrotated_strain_map.get_slice('e_yy') - * unrotated_strain_map.get_slice('theta') - - Returns: - (RealSlice) the rotated counterpart to unrotated_strain_map, with the - rotated_strain_map.get_slice('e_xx') element oriented along the new coordinate - system - """ - assert isinstance(unrotated_strain_map, RealSlice) - assert np.all( - [ - key in ["e_xx", "e_xy", "e_yy", "theta", "mask"] - for key in unrotated_strain_map.slicelabels - ] - ) - theta = -np.arctan2(xaxis_y, xaxis_x) - cost = np.cos(theta) - sint = np.sin(theta) - cost2 = cost**2 - sint2 = sint**2 - - Rx, Ry = unrotated_strain_map.get_slice("e_xx").data.shape - rotated_strain_map = RealSlice( - data=np.zeros((5, Rx, Ry)), - slicelabels=["e_xx", "e_xy", "e_yy", "theta", "mask"], - name=unrotated_strain_map.name + "_rotated".format(np.degrees(theta)), - ) - - rotated_strain_map.data[0, :, :] = ( - cost2 * unrotated_strain_map.get_slice("e_xx").data - - 2 * cost * sint * unrotated_strain_map.get_slice("e_xy").data - + sint2 * unrotated_strain_map.get_slice("e_yy").data - ) - rotated_strain_map.data[1, :, :] = ( - cost - * sint - * ( - unrotated_strain_map.get_slice("e_xx").data - - unrotated_strain_map.get_slice("e_yy").data - ) - + (cost2 - sint2) * unrotated_strain_map.get_slice("e_xy").data - ) - rotated_strain_map.data[2, :, :] = ( - sint2 * unrotated_strain_map.get_slice("e_xx").data - + 2 * cost * sint * unrotated_strain_map.get_slice("e_xy").data - + cost2 * unrotated_strain_map.get_slice("e_yy").data - ) - if flip_theta == True: - rotated_strain_map.data[3, :, :] = -unrotated_strain_map.get_slice("theta").data - else: - rotated_strain_map.data[3, :, :] = unrotated_strain_map.get_slice("theta").data - rotated_strain_map.data[4, :, :] = unrotated_strain_map.get_slice("mask").data - return rotated_strain_map diff --git a/py4DSTEM/process/strain/__init__.py b/py4DSTEM/process/strain/__init__.py new file mode 100644 index 000000000..b47682aa4 --- /dev/null +++ b/py4DSTEM/process/strain/__init__.py @@ -0,0 +1,2 @@ +from py4DSTEM.process.strain.strain import StrainMap +from py4DSTEM.process.strain.latticevectors import * diff --git a/py4DSTEM/process/strain/latticevectors.py b/py4DSTEM/process/strain/latticevectors.py new file mode 100644 index 000000000..90f7f938d --- /dev/null +++ b/py4DSTEM/process/strain/latticevectors.py @@ -0,0 +1,448 @@ +# Functions for indexing the Bragg directions + +import numpy as np +from emdfile import PointList, PointListArray, tqdmnd +from numpy.linalg import lstsq +from py4DSTEM.data import RealSlice + + +def index_bragg_directions(x0, y0, gx, gy, g1, g2): + """ + From an origin (x0,y0), a set of reciprocal lattice vectors gx,gy, and an pair of + lattice vectors g1=(g1x,g1y), g2=(g2x,g2y), find the indices (h,k) of all the + reciprocal lattice directions. + + The approach is to solve the matrix equation + ``alpha = beta * M`` + where alpha is the 2xN array of the (x,y) coordinates of N measured bragg directions, + beta is the 2x2 array of the two lattice vectors u,v, and M is the 2xN array of the + h,k indices. + + Args: + x0 (float): x-coord of origin + y0 (float): y-coord of origin + gx (1d array): x-coord of the reciprocal lattice vectors + gy (1d array): y-coord of the reciprocal lattice vectors + g1 (2-tuple of floats): g1x,g1y + g2 (2-tuple of floats): g2x,g2y + + Returns: + (3-tuple) A 3-tuple containing: + + * **h**: *(ndarray of ints)* first index of the bragg directions + * **k**: *(ndarray of ints)* second index of the bragg directions + * **bragg_directions**: *(PointList)* a 4-coordinate PointList with the + indexed bragg directions; coords 'qx' and 'qy' contain bragg_x and bragg_y + coords 'h' and 'k' contain h and k. + """ + # Get beta, the matrix of lattice vectors + beta = np.array([[g1[0], g2[0]], [g1[1], g2[1]]]) + + # Get alpha, the matrix of measured bragg angles + alpha = np.vstack([gx - x0, gy - y0]) + + # Calculate M, the matrix of peak positions + M = lstsq(beta, alpha, rcond=None)[0].T + M = np.round(M).astype(int) + + # Get h,k + h = M[:, 0] + k = M[:, 1] + + # Store in a PointList + coords = [("qx", float), ("qy", float), ("h", int), ("k", int)] + temp_array = np.zeros([], dtype=coords) + bragg_directions = PointList(data=temp_array) + bragg_directions.add_data_by_field((gx, gy, h, k)) + mask = np.zeros(bragg_directions["qx"].shape[0]) + mask[0] = 1 + bragg_directions.remove(mask) + + return h, k, bragg_directions + + +def add_indices_to_braggvectors( + braggpeaks, lattice, maxPeakSpacing, qx_shift=0, qy_shift=0, mask=None +): + """ + Using the peak positions (qx,qy) and indices (h,k) in the PointList lattice, + identify the indices for each peak in the PointListArray braggpeaks. + Return a new braggpeaks_indexed PointListArray, containing a copy of braggpeaks plus + three additional data columns -- 'h','k', and 'index_mask' -- specifying the peak + indices with the ints (h,k) and indicating whether the peak was successfully indexed + or not with the bool index_mask. If `mask` is specified, only the locations where + mask is True are indexed. + + Args: + braggpeaks (PointListArray): the braggpeaks to index. Must contain + the coordinates 'qx', 'qy', and 'intensity' + lattice (PointList): the positions (qx,qy) of the (h,k) lattice points. + Must contain the coordinates 'qx', 'qy', 'h', and 'k' + maxPeakSpacing (float): Maximum distance from the ideal lattice points + to include a peak for indexing + qx_shift,qy_shift (number): the shift of the origin in the `lattice` PointList + relative to the `braggpeaks` PointListArray + mask (bool): Boolean mask, same shape as the pointlistarray, indicating which + locations should be indexed. This can be used to index different regions of + the scan with different lattices + + Returns: + (PointListArray): The original braggpeaks pointlistarray, with new coordinates + 'h', 'k', containing the indices of each indexable peak. + """ + + # assert isinstance(braggpeaks,BraggVectors) + # assert isinstance(lattice, PointList) + # assert np.all([name in lattice.dtype.names for name in ('qx','qy','h','k')]) + + if mask is None: + mask = np.ones(braggpeaks.Rshape, dtype=bool) + + assert ( + mask.shape == braggpeaks.Rshape + ), "mask must have same shape as pointlistarray" + assert mask.dtype == bool, "mask must be boolean" + + coords = [ + ("qx", float), + ("qy", float), + ("intensity", float), + ("h", int), + ("k", int), + ] + + indexed_braggpeaks = PointListArray( + dtype=coords, + shape=braggpeaks.Rshape, + ) + + # loop over all the scan positions + for Rx, Ry in tqdmnd(mask.shape[0], mask.shape[1]): + if mask[Rx, Ry]: + pl = braggpeaks.cal[Rx, Ry] + for i in range(pl.data.shape[0]): + r2 = (pl.data["qx"][i] - lattice.data["qx"] + qx_shift) ** 2 + ( + pl.data["qy"][i] - lattice.data["qy"] + qy_shift + ) ** 2 + ind = np.argmin(r2) + if r2[ind] <= maxPeakSpacing**2: + indexed_braggpeaks[Rx, Ry].add_data_by_field( + ( + pl.data["qx"][i], + pl.data["qy"][i], + pl.data["intensity"][i], + lattice.data["h"][ind], + lattice.data["k"][ind], + ) + ) + + return indexed_braggpeaks + + +def fit_lattice_vectors(braggpeaks, x0=0, y0=0, minNumPeaks=5): + """ + Fits lattice vectors g1,g2 to braggpeaks given some known (h,k) indexing. + + Args: + braggpeaks (PointList): A 6 coordinate PointList containing the data to fit. + Coords are 'qx','qy' (the bragg peak positions), 'intensity' (used as a + weighting factor when fitting), 'h','k' (indexing). May optionally also + contain 'index_mask' (bool), indicating which peaks have been successfully + indixed and should be used. + x0 (float): x-coord of the origin + y0 (float): y-coord of the origin + minNumPeaks (int): if there are fewer than minNumPeaks peaks found in braggpeaks + which can be indexed, return None for all return parameters + + Returns: + (7-tuple) A 7-tuple containing: + + * **x0**: *(float)* the x-coord of the origin of the best-fit lattice. + * **y0**: *(float)* the y-coord of the origin + * **g1x**: *(float)* x-coord of the first lattice vector + * **g1y**: *(float)* y-coord of the first lattice vector + * **g2x**: *(float)* x-coord of the second lattice vector + * **g2y**: *(float)* y-coord of the second lattice vector + * **error**: *(float)* the fit error + """ + assert isinstance(braggpeaks, PointList) + assert np.all( + [name in braggpeaks.dtype.names for name in ("qx", "qy", "intensity", "h", "k")] + ) + braggpeaks = braggpeaks.copy() + + # Remove unindexed peaks + if "index_mask" in braggpeaks.dtype.names: + deletemask = braggpeaks.data["index_mask"] == False + braggpeaks.remove(deletemask) + + # Check to ensure enough peaks are present + if braggpeaks.length < minNumPeaks: + return None, None, None, None, None, None, None + + # Get M, the matrix of (h,k) indices + h, k = braggpeaks.data["h"], braggpeaks.data["k"] + M = np.vstack((np.ones_like(h, dtype=int), h, k)).T + + # Get alpha, the matrix of measured Bragg peak positions + alpha = np.vstack((braggpeaks.data["qx"] - x0, braggpeaks.data["qy"] - y0)).T + + # Get weighted matrices + weights = braggpeaks.data["intensity"] + weighted_M = M * weights[:, np.newaxis] + weighted_alpha = alpha * weights[:, np.newaxis] + + # Solve for lattice vectors + beta = lstsq(weighted_M, weighted_alpha, rcond=None)[0] + x0, y0 = beta[0, 0], beta[0, 1] + g1x, g1y = beta[1, 0], beta[1, 1] + g2x, g2y = beta[2, 0], beta[2, 1] + + # Calculate the error + alpha_calculated = np.matmul(M, beta) + error = np.sqrt(np.sum((alpha - alpha_calculated) ** 2, axis=1)) + error = np.sum(error * weights) / np.sum(weights) + + return x0, y0, g1x, g1y, g2x, g2y, error + + +def fit_lattice_vectors_all_DPs(braggpeaks, x0=0, y0=0, minNumPeaks=5): + """ + Fits lattice vectors g1,g2 to each diffraction pattern in braggpeaks, given some + known (h,k) indexing. + + Args: + braggpeaks (PointList): A 6 coordinate PointList containing the data to fit. + Coords are 'qx','qy' (the bragg peak positions), 'intensity' (used as a + weighting factor when fitting), 'h','k' (indexing). May optionally also + contain 'index_mask' (bool), indicating which peaks have been successfully + indixed and should be used. + x0 (float): x-coord of the origin + y0 (float): y-coord of the origin + minNumPeaks (int): if there are fewer than minNumPeaks peaks found in braggpeaks + which can be indexed, return None for all return parameters + + Returns: + (RealSlice): A RealSlice ``g1g2map`` containing the following 8 arrays: + + * ``g1g2_map.get_slice('x0')`` x-coord of the origin of the best fit lattice + * ``g1g2_map.get_slice('y0')`` y-coord of the origin + * ``g1g2_map.get_slice('g1x')`` x-coord of the first lattice vector + * ``g1g2_map.get_slice('g1y')`` y-coord of the first lattice vector + * ``g1g2_map.get_slice('g2x')`` x-coord of the second lattice vector + * ``g1g2_map.get_slice('g2y')`` y-coord of the second lattice vector + * ``g1g2_map.get_slice('error')`` the fit error + * ``g1g2_map.get_slice('mask')`` 1 for successful fits, 0 for unsuccessful + fits + """ + assert isinstance(braggpeaks, PointListArray) + assert np.all( + [name in braggpeaks.dtype.names for name in ("qx", "qy", "intensity", "h", "k")] + ) + + # Make RealSlice to contain outputs + slicelabels = ("x0", "y0", "g1x", "g1y", "g2x", "g2y", "error", "mask") + g1g2_map = RealSlice( + data=np.zeros((8, braggpeaks.shape[0], braggpeaks.shape[1])), + slicelabels=slicelabels, + name="g1g2_map", + ) + + # Fit lattice vectors + for Rx, Ry in tqdmnd(braggpeaks.shape[0], braggpeaks.shape[1]): + braggpeaks_curr = braggpeaks.get_pointlist(Rx, Ry) + qx0, qy0, g1x, g1y, g2x, g2y, error = fit_lattice_vectors( + braggpeaks_curr, x0, y0, minNumPeaks + ) + # Store data + if g1x is not None: + g1g2_map.get_slice("x0").data[Rx, Ry] = qx0 + g1g2_map.get_slice("y0").data[Rx, Ry] = qx0 + g1g2_map.get_slice("g1x").data[Rx, Ry] = g1x + g1g2_map.get_slice("g1y").data[Rx, Ry] = g1y + g1g2_map.get_slice("g2x").data[Rx, Ry] = g2x + g1g2_map.get_slice("g2y").data[Rx, Ry] = g2y + g1g2_map.get_slice("error").data[Rx, Ry] = error + g1g2_map.get_slice("mask").data[Rx, Ry] = 1 + + return g1g2_map + + +def get_reference_g1g2(g1g2_map, mask): + """ + Gets a pair of reference lattice vectors from a region of real space specified by + mask. Takes the median of the lattice vectors in g1g2_map within the specified + region. + + Args: + g1g2_map (RealSlice): the lattice vector map; contains 2D arrays in g1g2_map.data + under the keys 'g1x', 'g1y', 'g2x', and 'g2y'. See documentation for + fit_lattice_vectors_all_DPs() for more information. + mask (ndarray of bools): use lattice vectors from g1g2_map scan positions wherever + mask==True + + Returns: + (2-tuple of 2-tuples) A 2-tuple containing: + + * **g1**: *(2-tuple)* first reference lattice vector (x,y) + * **g2**: *(2-tuple)* second reference lattice vector (x,y) + """ + assert isinstance(g1g2_map, RealSlice) + assert np.all( + [name in g1g2_map.slicelabels for name in ("g1x", "g1y", "g2x", "g2y")] + ) + assert mask.dtype == bool + g1x = np.median(g1g2_map.get_slice("g1x").data[mask]) + g1y = np.median(g1g2_map.get_slice("g1y").data[mask]) + g2x = np.median(g1g2_map.get_slice("g2x").data[mask]) + g2y = np.median(g1g2_map.get_slice("g2y").data[mask]) + return (g1x, g1y), (g2x, g2y) + + +def get_strain_from_reference_g1g2(g1g2_map, g1, g2): + """ + Gets a strain map from the reference lattice vectors g1,g2 and lattice vector map + g1g2_map. + + Note that this function will return the strain map oriented with respect to the x/y + axes of diffraction space - to rotate the coordinate system, use + get_rotated_strain_map(). Calibration of the rotational misalignment between real and + diffraction space may also be necessary. + + Args: + g1g2_map (RealSlice): the lattice vector map; contains 2D arrays in g1g2_map.data + under the keys 'g1x', 'g1y', 'g2x', and 'g2y'. See documentation for + fit_lattice_vectors_all_DPs() for more information. + g1 (2-tuple): first reference lattice vector (x,y) + g2 (2-tuple): second reference lattice vector (x,y) + + Returns: + (RealSlice) the strain map; contains the elements of the infinitessimal strain + matrix, in the following 5 arrays: + + * ``strain_map.get_slice('e_xx')``: change in lattice x-components with respect + to x + * ``strain_map.get_slice('e_yy')``: change in lattice y-components with respect + to y + * ``strain_map.get_slice('e_xy')``: change in lattice x-components with respect + to y + * ``strain_map.get_slice('theta')``: rotation of lattice with respect to + reference + * ``strain_map.get_slice('mask')``: 0/False indicates unknown values + + Note 1: the strain matrix has been symmetrized, so e_xy and e_yx are identical + """ + assert isinstance(g1g2_map, RealSlice) + assert np.all( + [name in g1g2_map.slicelabels for name in ("g1x", "g1y", "g2x", "g2y", "mask")] + ) + + # Get RealSlice for output storage + R_Nx, R_Ny = g1g2_map.get_slice("g1x").shape + strain_map = RealSlice( + data=np.zeros((5, R_Nx, R_Ny)), + slicelabels=("e_xx", "e_yy", "e_xy", "theta", "mask"), + name="strain_map", + ) + + # Get reference lattice matrix + g1x, g1y = g1 + g2x, g2y = g2 + M = np.array([[g1x, g1y], [g2x, g2y]]) + + for Rx in range(R_Nx): + for Ry in range(R_Ny): + # Get lattice vectors for DP at Rx,Ry + alpha = np.array( + [ + [ + g1g2_map.get_slice("g1x").data[Rx, Ry], + g1g2_map.get_slice("g1y").data[Rx, Ry], + ], + [ + g1g2_map.get_slice("g2x").data[Rx, Ry], + g1g2_map.get_slice("g2y").data[Rx, Ry], + ], + ] + ) + # Get transformation matrix + beta = lstsq(M, alpha, rcond=None)[0].T + + # Get the infinitesimal strain matrix + strain_map.get_slice("e_xx").data[Rx, Ry] = 1 - beta[0, 0] + strain_map.get_slice("e_yy").data[Rx, Ry] = 1 - beta[1, 1] + strain_map.get_slice("e_xy").data[Rx, Ry] = -(beta[0, 1] + beta[1, 0]) / 2.0 + strain_map.get_slice("theta").data[Rx, Ry] = (beta[0, 1] - beta[1, 0]) / 2.0 + strain_map.get_slice("mask").data[Rx, Ry] = g1g2_map.get_slice("mask").data[ + Rx, Ry + ] + return strain_map + +def get_rotated_strain_map(unrotated_strain_map, xaxis_x, xaxis_y, flip_theta): + """ + Starting from a strain map defined with respect to the xy coordinate system of + diffraction space, i.e. where exx and eyy are the compression/tension along the Qx + and Qy directions, respectively, get a strain map defined with respect to some other + right-handed coordinate system, in which the x-axis is oriented along (xaxis_x, + xaxis_y). + + Args: + xaxis_x,xaxis_y (float): diffraction space (x,y) coordinates of a vector + along the new x-axis + unrotated_strain_map (RealSlice): a RealSlice object containing 2D arrays of the + infinitessimal strain matrix elements, stored at + * unrotated_strain_map.get_slice('e_xx') + * unrotated_strain_map.get_slice('e_xy') + * unrotated_strain_map.get_slice('e_yy') + * unrotated_strain_map.get_slice('theta') + + Returns: + (RealSlice) the rotated counterpart to unrotated_strain_map, with the + rotated_strain_map.get_slice('e_xx') element oriented along the new coordinate + system + """ + assert isinstance(unrotated_strain_map, RealSlice) + assert np.all( + [ + key in ["e_xx", "e_xy", "e_yy", "theta", "mask"] + for key in unrotated_strain_map.slicelabels + ] + ) + theta = -np.arctan2(xaxis_y, xaxis_x) + cost = np.cos(theta) + sint = np.sin(theta) + cost2 = cost**2 + sint2 = sint**2 + + Rx, Ry = unrotated_strain_map.get_slice("e_xx").data.shape + rotated_strain_map = RealSlice( + data=np.zeros((5, Rx, Ry)), + slicelabels=["e_xx", "e_xy", "e_yy", "theta", "mask"], + name=unrotated_strain_map.name + "_rotated".format(np.degrees(theta)), + ) + + rotated_strain_map.data[0, :, :] = ( + cost2 * unrotated_strain_map.get_slice("e_xx").data + - 2 * cost * sint * unrotated_strain_map.get_slice("e_xy").data + + sint2 * unrotated_strain_map.get_slice("e_yy").data + ) + rotated_strain_map.data[1, :, :] = ( + cost + * sint + * ( + unrotated_strain_map.get_slice("e_xx").data + - unrotated_strain_map.get_slice("e_yy").data + ) + + (cost2 - sint2) * unrotated_strain_map.get_slice("e_xy").data + ) + rotated_strain_map.data[2, :, :] = ( + sint2 * unrotated_strain_map.get_slice("e_xx").data + + 2 * cost * sint * unrotated_strain_map.get_slice("e_xy").data + + cost2 * unrotated_strain_map.get_slice("e_yy").data + ) + if flip_theta == True: + rotated_strain_map.data[3, :, :] = -unrotated_strain_map.get_slice("theta").data + else: + rotated_strain_map.data[3, :, :] = unrotated_strain_map.get_slice("theta").data + rotated_strain_map.data[4, :, :] = unrotated_strain_map.get_slice("mask").data + return rotated_strain_map diff --git a/py4DSTEM/process/strain.py b/py4DSTEM/process/strain/strain.py similarity index 86% rename from py4DSTEM/process/strain.py rename to py4DSTEM/process/strain/strain.py index db252f75b..751016a89 100644 --- a/py4DSTEM/process/strain.py +++ b/py4DSTEM/process/strain/strain.py @@ -1,5 +1,6 @@ # Defines the Strain class +import warnings from typing import Optional import matplotlib.pyplot as plt @@ -8,20 +9,33 @@ from py4DSTEM.braggvectors import BraggVectors from py4DSTEM.data import Data, RealSlice from py4DSTEM.preprocess.utils import get_maxima_2D +from py4DSTEM.process.strain.latticevectors import ( + add_indices_to_braggvectors, + fit_lattice_vectors_all_DPs, + get_reference_g1g2, + get_rotated_strain_map, + get_strain_from_reference_g1g2, + index_bragg_directions, +) from py4DSTEM.visualize import add_bragg_index_labels, add_pointlabels, add_vector, show +warnings.simplefilter(action="always", category=UserWarning) + class StrainMap(RealSlice, Data): """ - Stores strain map. - - TODO add docs - + Storage and processing methods for 4D-STEM datasets. + """ def __init__(self, braggvectors: BraggVectors, name: Optional[str] = "strainmap"): + """ - TODO + Accepts: + braggvectors (BraggVectors): BraggVectors for Strain Map + name (str): the name of the strainmap + Returns: + A new StrainMap instance. """ assert isinstance( braggvectors, BraggVectors @@ -58,6 +72,12 @@ def __init__(self, braggvectors: BraggVectors, name: Optional[str] = "strainmap" # re-calibration are issued self.calstate = self.braggvectors.calstate assert self.calstate["center"], "braggvectors must be centered" + if self.calstate["rotate"] == False: + warnings.warn( + ("Real to reciprocal space rotaiton not calibrated"), + UserWarning, + ) + # get the BVM # a new BVM using the current calstate is computed self.bvm = self.braggvectors.histogram(mode="cal") @@ -110,16 +130,18 @@ def choose_lattice_vectors( minSpacing=0, edgeBoundary=1, maxNumPeaks=10, - figsize=(12, 6), + x0=None, + y0=None, + figsize=(14, 9), c_indices="lightblue", c0="g", c1="r", c2="r", c_vectors="r", c_vectorlabels="w", - size_indices=20, + size_indices=15, width_vectors=1, - size_vectorlabels=20, + size_vectorlabels=15, vis_params={}, returncalc=False, returnfig=False, @@ -198,6 +220,7 @@ def choose_lattice_vectors( ), "The calibration state has changed! To resync the calibration state, use `.reset_calstate`." # find the maxima + g = get_maxima_2D( self.bvm.data, subpixel=subpixel, @@ -220,10 +243,34 @@ def choose_lattice_vectors( g2y = gy[index_g2] - g0[1] g1, g2 = (g1x, g1y), (g2x, g2y) + # if x0 is None: + # x0 = self.braggvectors.Qshape[0] / 2 + # if y0 is None: + # y0 = self.braggvectors.Qshape[0] / 2 + + # index braggvectors + # _, _, braggdirections = index_bragg_directions( + # x0, y0, g["x"], g["y"], g1, g2 + # ) + + _, _, braggdirections = index_bragg_directions( + g0[0], g0[1], g["x"], g["y"], g1, g2 + ) + + self.braggdirections = braggdirections + # make the figure - fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize) - show(self.bvm.data, figax=(fig, ax1), **vis_params) - show(self.bvm.data, figax=(fig, ax2), **vis_params) + fig, ax = plt.subplots(1, 3, figsize=figsize) + show(self.bvm.data, figax=(fig, ax[0]), **vis_params) + show(self.bvm.data, figax=(fig, ax[1]), **vis_params) + self.show_bragg_indexing( + self.bvm.data, + bragg_directions=braggdirections, + points=True, + figax=(fig, ax[2]), + size=size_indices, + **vis_params, + ) # Add indices to left panel d = {"x": gx, "y": gy, "size": size_indices, "color": c_indices} @@ -251,10 +298,10 @@ def choose_lattice_vectors( "fontweight": "bold", "labels": [str(index_g2)], } - add_pointlabels(ax1, d) - add_pointlabels(ax1, d0) - add_pointlabels(ax1, d1) - add_pointlabels(ax1, d2) + add_pointlabels(ax[0], d) + add_pointlabels(ax[0], d0) + add_pointlabels(ax[0], d1) + add_pointlabels(ax[0], d2) # Add vectors to right panel dg1 = { @@ -279,8 +326,8 @@ def choose_lattice_vectors( "labelsize": size_vectorlabels, "labelcolor": c_vectorlabels, } - add_vector(ax2, dg1) - add_vector(ax2, dg2) + add_vector(ax[1], dg1) + add_vector(ax[1], dg2) # store vectors self.g = g @@ -290,18 +337,16 @@ def choose_lattice_vectors( # return if returncalc and returnfig: - return (g0, g1, g2), (fig, (ax1, ax2)) + return (g0, g1, g2), (fig, ax) elif returncalc: return (g0, g1, g2) elif returnfig: - return (fig, (ax1, ax2)) + return (fig, ax) else: return def fit_lattice_vectors( self, - x0=None, - y0=None, max_peak_spacing=2, mask=None, plot=True, @@ -337,31 +382,6 @@ def fit_lattice_vectors( self.calstate == self.braggvectors.calstate ), "The calibration state has changed! To resync the calibration state, use `.reset_calstate`." - if x0 is None: - x0 = self.braggvectors.Qshape[0] / 2 - if y0 is None: - y0 = self.braggvectors.Qshape[0] / 2 - - # index braggvectors - from py4DSTEM.process.latticevectors import index_bragg_directions - - _, _, braggdirections = index_bragg_directions( - x0, y0, self.g["x"], self.g["y"], self.g1, self.g2 - ) - - self.braggdirections = braggdirections - - if plot: - self.show_bragg_indexing( - self.bvm, - bragg_directions=braggdirections, - points=True, - **vis_params, - ) - - # add indicies to braggvectors - from py4DSTEM.process.latticevectors import add_indices_to_braggvectors - bragg_vectors_indexed = add_indices_to_braggvectors( self.braggvectors, self.braggdirections, @@ -374,13 +394,11 @@ def fit_lattice_vectors( self.bragg_vectors_indexed = bragg_vectors_indexed # fit bragg vectors - from py4DSTEM.process.latticevectors import fit_lattice_vectors_all_DPs - g1g2_map = fit_lattice_vectors_all_DPs(self.bragg_vectors_indexed) self.g1g2_map = g1g2_map if returncalc: - braggdirections, bragg_vectors_indexed, g1g2_map + self.braggdirections, self.bragg_vectors_indexed, self.g1g2_map def get_strain( self, mask=None, g_reference=None, flip_theta=False, returncalc=False, **kwargs @@ -407,30 +425,24 @@ def get_strain( if mask is None: mask = np.ones(self.g1g2_map.shape, dtype="bool") - from py4DSTEM.process.latticevectors import get_strain_from_reference_region + # strainmap_g1g2 = get_strain_from_reference_region( + # self.g1g2_map, + # mask=mask, + # ) - strainmap_g1g2 = get_strain_from_reference_region( - self.g1g2_map, - mask=mask, - ) - else: - from py4DSTEM.process.latticevectors import get_reference_g1g2 + # g1_ref, g2_ref = get_reference_g1g2(self.g1g2_map, mask) + # strain_map = get_strain_from_reference_g1g2(self.g1g2_map, g1_ref, g2_ref) + # else: - g1_ref, g2_ref = get_reference_g1g2(self.g1g2_map, mask) + g1_ref, g2_ref = get_reference_g1g2(self.g1g2_map, mask) - from py4DSTEM.process.latticevectors import get_strain_from_reference_g1g2 - - strainmap_g1g2 = get_strain_from_reference_g1g2( - self.g1g2_map, g1_ref, g2_ref - ) + strainmap_g1g2 = get_strain_from_reference_g1g2(self.g1g2_map, g1_ref, g2_ref) self.strainmap_g1g2 = strainmap_g1g2 if g_reference is None: g_reference = np.subtract(self.g1, self.g2) - from py4DSTEM.process.latticevectors import get_rotated_strain_map - strainmap_rotated = get_rotated_strain_map( self.strainmap_g1g2, xaxis_x=g_reference[0], @@ -529,6 +541,7 @@ def show_bragg_indexing( points=True, pointcolor="r", pointsize=50, + figax=None, returnfig=False, **kwargs, ): @@ -544,7 +557,13 @@ def show_bragg_indexing( for k in ("qx", "qy", "h", "k"): assert k in bragg_directions.data.dtype.fields - fig, ax = show(ar, returnfig=True, **kwargs) + if figax is None: + fig, ax = show(ar, returnfig=True, **kwargs) + else: + fig = figax[0] + ax = figax[1] + show(ar, figax=figax, **kwargs) + d = { "bragg_directions": bragg_directions, "voffset": voffset, @@ -560,7 +579,6 @@ def show_bragg_indexing( if returnfig: return fig, ax else: - plt.show() return def copy(self, name=None): @@ -585,7 +603,7 @@ def copy(self, name=None): strainmap_copy.metadata = self.metadata[k].copy() return strainmap_copy - # IO methods + # TODO IO methods # read @classmethod From 73b4fe798431c0b1aae1ec07f6233d5041e40690 Mon Sep 17 00:00:00 2001 From: Stephanie Ribet Date: Mon, 16 Oct 2023 07:05:13 -0400 Subject: [PATCH 02/10] bug fix for calibrated strain --- py4DSTEM/process/strain/latticevectors.py | 12 +++++++++++- py4DSTEM/process/strain/strain.py | 5 ++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/py4DSTEM/process/strain/latticevectors.py b/py4DSTEM/process/strain/latticevectors.py index 90f7f938d..26c8d66a5 100644 --- a/py4DSTEM/process/strain/latticevectors.py +++ b/py4DSTEM/process/strain/latticevectors.py @@ -116,10 +116,19 @@ def add_indices_to_braggvectors( shape=braggpeaks.Rshape, ) + calstate = braggpeaks.calstate + # loop over all the scan positions for Rx, Ry in tqdmnd(mask.shape[0], mask.shape[1]): if mask[Rx, Ry]: - pl = braggpeaks.cal[Rx, Ry] + pl = braggpeaks.get_vectors( + Rx, + Ry, + center=True, + ellipse=calstate["ellipse"], + rotate=calstate["rotate"], + pixel=False, + ) for i in range(pl.data.shape[0]): r2 = (pl.data["qx"][i] - lattice.data["qx"] + qx_shift) ** 2 + ( pl.data["qy"][i] - lattice.data["qy"] + qy_shift @@ -378,6 +387,7 @@ def get_strain_from_reference_g1g2(g1g2_map, g1, g2): ] return strain_map + def get_rotated_strain_map(unrotated_strain_map, xaxis_x, xaxis_y, flip_theta): """ Starting from a strain map defined with respect to the xy coordinate system of diff --git a/py4DSTEM/process/strain/strain.py b/py4DSTEM/process/strain/strain.py index 751016a89..47545c04b 100644 --- a/py4DSTEM/process/strain/strain.py +++ b/py4DSTEM/process/strain/strain.py @@ -25,11 +25,10 @@ class StrainMap(RealSlice, Data): """ Storage and processing methods for 4D-STEM datasets. - + """ def __init__(self, braggvectors: BraggVectors, name: Optional[str] = "strainmap"): - """ Accepts: braggvectors (BraggVectors): BraggVectors for Strain Map @@ -95,7 +94,7 @@ def braggvectors(self, x): ), f".braggvectors must be BraggVectors, not type {type(x)}" assert ( x.calibration.origin is not None - ), f"braggvectors must have a calibrated origin" + ), "braggvectors must have a calibrated origin" self._braggvectors = x self._braggvectors.tree(self, force=True) From b134f848f7b502a776ecd4c4257404c23257b27b Mon Sep 17 00:00:00 2001 From: bsavitzky Date: Mon, 23 Oct 2023 12:03:06 +0100 Subject: [PATCH 03/10] bugfix --- py4DSTEM/visualize/vis_special.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/py4DSTEM/visualize/vis_special.py b/py4DSTEM/visualize/vis_special.py index cfa017299..ba0ee024a 100644 --- a/py4DSTEM/visualize/vis_special.py +++ b/py4DSTEM/visualize/vis_special.py @@ -156,16 +156,16 @@ def show_amorphous_ring_fit( mask=np.logical_not(mask), mask_color="empty", returnfig=True, - returnclipvals=True, + return_intensity_range=True, **kwargs, ) show( fit, scaling=scaling, figax=(fig, ax), - clipvals="manual", - min=vmin, - max=vmax, + intensity_range="absolute", + vmin=vmin, + vmax=vmax, cmap=cmap_fit, mask=mask, mask_color="empty", From e76bd2a39e3a29524021db2adf5f2dc269067bbb Mon Sep 17 00:00:00 2001 From: bsavitzky Date: Mon, 23 Oct 2023 12:11:49 +0100 Subject: [PATCH 04/10] rms import * --- py4DSTEM/process/strain/__init__.py | 12 +++++++++++- py4DSTEM/process/strain/latticevectors.py | 1 + 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/py4DSTEM/process/strain/__init__.py b/py4DSTEM/process/strain/__init__.py index b47682aa4..213d5e812 100644 --- a/py4DSTEM/process/strain/__init__.py +++ b/py4DSTEM/process/strain/__init__.py @@ -1,2 +1,12 @@ from py4DSTEM.process.strain.strain import StrainMap -from py4DSTEM.process.strain.latticevectors import * +from py4DSTEM.process.strain.latticevectors import ( + index_bragg_directions, + add_indices_to_braggvectors, + fit_lattice_vectors, + fit_lattice_vectors_all_DPs, + get_reference_g1g2, + get_strain_from_reference_g1g2, + get_rotated_strain_map, + +) + diff --git a/py4DSTEM/process/strain/latticevectors.py b/py4DSTEM/process/strain/latticevectors.py index 26c8d66a5..30e5cc989 100644 --- a/py4DSTEM/process/strain/latticevectors.py +++ b/py4DSTEM/process/strain/latticevectors.py @@ -456,3 +456,4 @@ def get_rotated_strain_map(unrotated_strain_map, xaxis_x, xaxis_y, flip_theta): rotated_strain_map.data[3, :, :] = unrotated_strain_map.get_slice("theta").data rotated_strain_map.data[4, :, :] = unrotated_strain_map.get_slice("mask").data return rotated_strain_map + From 78050bd7ea010306f7f1c257e6a09af2b001e452 Mon Sep 17 00:00:00 2001 From: bsavitzky Date: Mon, 23 Oct 2023 13:58:29 +0100 Subject: [PATCH 05/10] bugfixes to strainmapping --- py4DSTEM/process/strain/strain.py | 155 ++++++++++++++++++++++-------- 1 file changed, 115 insertions(+), 40 deletions(-) diff --git a/py4DSTEM/process/strain/strain.py b/py4DSTEM/process/strain/strain.py index 47545c04b..d0848182f 100644 --- a/py4DSTEM/process/strain/strain.py +++ b/py4DSTEM/process/strain/strain.py @@ -5,7 +5,7 @@ import matplotlib.pyplot as plt import numpy as np -from py4DSTEM import PointList +from py4DSTEM import PointList, PointListArray, tqdmnd from py4DSTEM.braggvectors import BraggVectors from py4DSTEM.data import Data, RealSlice from py4DSTEM.preprocess.utils import get_maxima_2D @@ -73,7 +73,7 @@ def __init__(self, braggvectors: BraggVectors, name: Optional[str] = "strainmap" assert self.calstate["center"], "braggvectors must be centered" if self.calstate["rotate"] == False: warnings.warn( - ("Real to reciprocal space rotaiton not calibrated"), + ("Real to reciprocal space rotation not calibrated"), UserWarning, ) @@ -98,6 +98,19 @@ def braggvectors(self, x): self._braggvectors = x self._braggvectors.tree(self, force=True) + @property + def rshape(self): + return self._braggvectors.Rshape + + @property + def qshape(self): + return self._braggvectors.Qshape + + @property + def origin(self): + return self.calibration.get_origin_mean() + + def reset_calstate(self): """ Resets the calibration state. This recomputes the BVM, and removes any computations @@ -117,9 +130,9 @@ def reset_calstate(self): def choose_lattice_vectors( self, - index_g0, - index_g1, - index_g2, + index_g1 = None, + index_g2 = None, + index_origin = None, subpixel="multicorr", upsample_factor=16, sigma=0, @@ -155,12 +168,12 @@ def choose_lattice_vectors( Parameters ---------- - index_g0 : int - selected index for the origin index_g1 : int selected index for g1 index_g2 :int selected index for g2 + index_origin : int + selected index for the origin subpixel : str in ('pixel','poly','multicorr') See the docstring for py4DSTEM.preprocess.get_maxima_2D upsample_factor : int @@ -211,8 +224,8 @@ def choose_lattice_vectors( (optional) : None or (g0,g1,g2) or (fig,(ax1,ax2)) or both of the latter """ # validate inputs - for i in (index_g0, index_g1, index_g2): - assert isinstance(i, (int, np.integer)), "indices must be integers!" + for i in (index_origin, index_g1, index_g2): + assert(isinstance(i, (int, np.integer)) or (i is None)), "indices must be integers!" # check the calstate assert ( self.calstate == self.braggvectors.calstate @@ -233,31 +246,43 @@ def choose_lattice_vectors( maxNumPeaks=maxNumPeaks, ) + # guess the origin and g1 g2 vectors if indices aren't provided + if np.any([x is None for x in (index_g1,index_g2,index_origin)]): + + # get distances and angles from calibrated origin + g_dists = np.hypot(g['x']-self.origin[0], g['y']-self.origin[1]) + g_angles = np.angle(g['x']-self.origin[0] + 1j*(g['y']-self.origin[1])) + + # guess the origin + if index_origin is None: + index_origin = np.argmin(g_dists) + g_dists[index_origin] = 2*np.max(g_dists) + + # guess g1 + if index_g1 is None: + index_g1 = np.argmin(g_dists) + g_dists[index_g1] = 2*np.max(g_dists) + + # guess g2 + if index_g2 is None: + angle_scaling = np.cos(g_angles - g_angles[index_g1])**2 + index_g2 = np.argmin(g_dists*(angle_scaling+0.1)) + + # get the lattice vectors gx, gy = g["x"], g["y"] - g0 = gx[index_g0], gy[index_g0] + g0 = gx[index_origin], gy[index_origin] g1x = gx[index_g1] - g0[0] g1y = gy[index_g1] - g0[1] g2x = gx[index_g2] - g0[0] g2y = gy[index_g2] - g0[1] g1, g2 = (g1x, g1y), (g2x, g2y) - # if x0 is None: - # x0 = self.braggvectors.Qshape[0] / 2 - # if y0 is None: - # y0 = self.braggvectors.Qshape[0] / 2 - - # index braggvectors - # _, _, braggdirections = index_bragg_directions( - # x0, y0, g["x"], g["y"], g1, g2 - # ) - + # index the lattice vectors _, _, braggdirections = index_bragg_directions( g0[0], g0[1], g["x"], g["y"], g1, g2 ) - self.braggdirections = braggdirections - # make the figure fig, ax = plt.subplots(1, 3, figsize=figsize) show(self.bvm.data, figax=(fig, ax[0]), **vis_params) @@ -274,12 +299,12 @@ def choose_lattice_vectors( # Add indices to left panel d = {"x": gx, "y": gy, "size": size_indices, "color": c_indices} d0 = { - "x": gx[index_g0], - "y": gy[index_g0], + "x": gx[index_origin], + "y": gy[index_origin], "size": size_indices, "color": c0, "fontweight": "bold", - "labels": [str(index_g0)], + "labels": [str(index_origin)], } d1 = { "x": gx[index_g1], @@ -304,8 +329,8 @@ def choose_lattice_vectors( # Add vectors to right panel dg1 = { - "x0": gx[index_g0], - "y0": gy[index_g0], + "x0": gx[index_origin], + "y0": gy[index_origin], "vx": g1[0], "vy": g1[1], "width": width_vectors, @@ -315,8 +340,8 @@ def choose_lattice_vectors( "labelcolor": c_vectorlabels, } dg2 = { - "x0": gx[index_g0], - "y0": gy[index_g0], + "x0": gx[index_origin], + "y0": gy[index_origin], "vx": g2[0], "vy": g2[1], "width": width_vectors, @@ -334,6 +359,11 @@ def choose_lattice_vectors( self.g1 = g1 self.g2 = g2 + # center the bragg directions and store + braggdirections.data['qx'] -= self.origin[0] + braggdirections.data['qy'] -= self.origin[1] + self.braggdirections = braggdirections + # return if returncalc and returnfig: return (g0, g1, g2), (fig, ax) @@ -381,23 +411,68 @@ def fit_lattice_vectors( self.calstate == self.braggvectors.calstate ), "The calibration state has changed! To resync the calibration state, use `.reset_calstate`." - bragg_vectors_indexed = add_indices_to_braggvectors( - self.braggvectors, - self.braggdirections, - maxPeakSpacing=max_peak_spacing, - qx_shift=self.braggvectors.Qshape[0] / 2, - qy_shift=self.braggvectors.Qshape[1] / 2, - mask=mask, - ) - self.bragg_vectors_indexed = bragg_vectors_indexed + ### add indices to the bragg vectors - # fit bragg vectors + # validate mask + if mask is None: + mask = np.ones(self.braggvectors.Rshape, dtype=bool) + assert ( + mask.shape == self.braggvectors.Rshape + ), "mask must have same shape as pointlistarray" + assert mask.dtype == bool, "mask must be boolean" + + # set up new braggpeaks PLA + indexed_braggpeaks = PointListArray( + dtype = [ + ("qx", float), + ("qy", float), + ("intensity", float), + ("h", int), + ("k", int), + ], + shape=self.braggvectors.Rshape, + ) + calstate = self.braggvectors.calstate + + # loop over all the scan positions + for Rx, Ry in tqdmnd(mask.shape[0], mask.shape[1]): + if mask[Rx, Ry]: + pl = self.braggvectors.get_vectors( + Rx, + Ry, + center=True, + ellipse=calstate["ellipse"], + rotate=calstate["rotate"], + pixel=False, + ) + for i in range(pl.data.shape[0]): + r = np.hypot( + pl.data["qx"][i]-self.braggdirections.data["qx"], + pl.data["qy"][i]-self.braggdirections.data["qy"] + ) + ind = np.argmin(r) + if r[ind] <= max_peak_spacing: + indexed_braggpeaks[Rx, Ry].add_data_by_field( + ( + pl.data["qx"][i], + pl.data["qy"][i], + pl.data["intensity"][i], + self.braggdirections.data["h"][ind], + self.braggdirections.data["k"][ind], + ) + ) + self.bragg_vectors_indexed = indexed_braggpeaks + + + ### fit bragg vectors g1g2_map = fit_lattice_vectors_all_DPs(self.bragg_vectors_indexed) self.g1g2_map = g1g2_map + + # return if returncalc: - self.braggdirections, self.bragg_vectors_indexed, self.g1g2_map + return self.braggdirections, self.bragg_vectors_indexed, self.g1g2_map def get_strain( self, mask=None, g_reference=None, flip_theta=False, returncalc=False, **kwargs From 85e214174c3c993f41092ae9471f1317d0fb13fc Mon Sep 17 00:00:00 2001 From: bsavitzky Date: Mon, 23 Oct 2023 16:09:41 +0100 Subject: [PATCH 06/10] strain map updates --- py4DSTEM/process/strain/strain.py | 362 ++++++++++++++++++++++++++++-- py4DSTEM/visualize/vis_special.py | 301 ------------------------- 2 files changed, 341 insertions(+), 322 deletions(-) diff --git a/py4DSTEM/process/strain/strain.py b/py4DSTEM/process/strain/strain.py index d0848182f..cd662b8f1 100644 --- a/py4DSTEM/process/strain/strain.py +++ b/py4DSTEM/process/strain/strain.py @@ -4,6 +4,7 @@ from typing import Optional import matplotlib.pyplot as plt +from mpl_toolkits.axes_grid1 import make_axes_locatable import numpy as np from py4DSTEM import PointList, PointListArray, tqdmnd from py4DSTEM.braggvectors import BraggVectors @@ -18,6 +19,7 @@ index_bragg_directions, ) from py4DSTEM.visualize import add_bragg_index_labels, add_pointlabels, add_vector, show +from py4DSTEM.visualize import ax_addaxes, ax_addaxes_QtoR warnings.simplefilter(action="always", category=UserWarning) @@ -110,6 +112,12 @@ def qshape(self): def origin(self): return self.calibration.get_origin_mean() + @property + def mask(self): + try: + return self.g1g2_map['mask'].data.astype('bool') + except: + return np.ones(self.rshape, dtype=bool) def reset_calstate(self): """ @@ -366,9 +374,9 @@ def choose_lattice_vectors( # return if returncalc and returnfig: - return (g0, g1, g2), (fig, ax) + return (self.g0, self.g1, self.g2, self.braggdirections), (fig, ax) elif returncalc: - return (g0, g1, g2) + return (self.g0, self.g1, self.g2, self.braggdirections) elif returnfig: return (fig, ax) else: @@ -378,8 +386,6 @@ def fit_lattice_vectors( self, max_peak_spacing=2, mask=None, - plot=True, - vis_params={}, returncalc=False, ): """ @@ -388,10 +394,6 @@ def fit_lattice_vectors( reciprocal lattice directions. Args: - x0 : floagt - x-coord of origin - y0 : float - y-coord of origin max_peak_spacing: float Maximum distance from the ideal lattice points to include a peak for indexing @@ -399,10 +401,6 @@ def fit_lattice_vectors( Boolean mask, same shape as the pointlistarray, indicating which locations should be indexed. This can be used to index different regions of the scan with different lattices - plot:bool - plot results if tru - vis_params : dict - additional visualization parameters passed to `show` returncalc : bool if True, returns bragg_directions, bragg_vectors_indexed, g1g2_map """ @@ -472,7 +470,7 @@ def fit_lattice_vectors( # return if returncalc: - return self.braggdirections, self.bragg_vectors_indexed, self.g1g2_map + return self.bragg_vectors_indexed, self.g1g2_map def get_strain( self, mask=None, g_reference=None, flip_theta=False, returncalc=False, **kwargs @@ -497,8 +495,8 @@ def get_strain( ), "The calibration state has changed! To resync the calibration state, use `.reset_calstate`." if mask is None: - mask = np.ones(self.g1g2_map.shape, dtype="bool") - + mask = self.mask + #mask = np.ones(self.g1g2_map.shape, dtype="bool") # strainmap_g1g2 = get_strain_from_reference_region( # self.g1g2_map, # mask=mask, @@ -524,9 +522,12 @@ def get_strain( flip_theta=flip_theta, ) - self.strainmap_rotated = strainmap_rotated - - from py4DSTEM.visualize import show_strain + self.data[0] = strainmap_rotated['e_xx'].data + self.data[1] = strainmap_rotated['e_yy'].data + self.data[2] = strainmap_rotated['e_xy'].data + self.data[3] = strainmap_rotated['theta'].data + self.data[4] = strainmap_rotated['mask'].data + self.g_reference = g_reference figsize = kwargs.pop("figsize", (14, 4)) vrange_exx = kwargs.pop("vrange_exx", [-2.0, 2.0]) @@ -535,8 +536,7 @@ def get_strain( bkgrd = kwargs.pop("bkgrd", False) axes_plots = kwargs.pop("axes_plots", ()) - fig, ax = show_strain( - self.strainmap_rotated, + fig, ax = self.show_strain( vrange_exx=vrange_exx, vrange_theta=vrange_theta, ticknumber=ticknumber, @@ -554,7 +554,327 @@ def get_strain( ax[1][1].imshow(mask, alpha=0.2, cmap="binary") if returncalc: - return self.strainmap_rotated + return self.strainmap + + + def show_strain( + self, + vrange_exx, + vrange_theta, + vrange_exy=None, + vrange_eyy=None, + flip_theta=False, + bkgrd=True, + show_cbars=("exx", "eyy", "exy", "theta"), + bordercolor="k", + borderwidth=1, + titlesize=24, + ticklabelsize=16, + ticknumber=5, + unitlabelsize=24, + show_axes=False, + axes_position = (0,0), + axes_length=10, + axes_width=1, + axes_color="w", + xaxis_space="Q", + labelaxes=True, + QR_rotation=0, + axes_labelsize=12, + axes_labelcolor="r", + axes_plots=("exx"), + cmap="RdBu_r", + mask_color = 'k', + layout=0, + figsize=(12, 12), + returnfig=False, + ): + """ + Display a strain map, showing the 4 strain components (e_xx,e_yy,e_xy,theta), and + masking each image with strainmap.get_slice('mask') + + Args: + vrange_exx (length 2 list or tuple): + vrange_theta (length 2 list or tuple): + vrange_exy (length 2 list or tuple): + vrange_eyy (length 2 list or tuple): + flip_theta (bool): if True, take negative of angle + bkgrd (bool): + show_cbars (tuple of strings): Show colorbars for the specified axes. Must be a + tuple containing any, all, or none of ('exx','eyy','exy','theta'). + bordercolor (color): + borderwidth (number): + titlesize (number): + ticklabelsize (number): + ticknumber (number): number of ticks on colorbars + unitlabelsize (number): + show_axes (bool): + axes_x0 (number): + axes_y0 (number): + xaxis_x (number): + xaxis_y (number): + axes_length (number): + axes_width (number): + axes_color (color): + xaxis_space (string): must be 'Q' or 'R' + labelaxes (bool): + QR_rotation (number): + axes_labelsize (number): + axes_labelcolor (color): + axes_plots (tuple of strings): controls if coordinate axes showing the + orientation of the strain matrices are overlaid over any of the plots. + Must be a tuple of strings containing any, all, or none of + ('exx','eyy','exy','theta'). + cmap (colormap): + layout=0 (int): determines the layout of the grid which the strain components + will be plotted in. Must be in (0,1,2). 0=(2x2), 1=(1x4), 2=(4x1). + figsize (length 2 tuple of numbers): + returnfig (bool): + """ + # Lookup table for different layouts + assert layout in (0, 1, 2) + layout_lookup = { + 0: ["left", "right", "left", "right"], + 1: ["bottom", "bottom", "bottom", "bottom"], + 2: ["right", "right", "right", "right"], + } + layout_p = layout_lookup[layout] + + # Contrast limits + if vrange_exy is None: + vrange_exy = vrange_exx + if vrange_eyy is None: + vrange_eyy = vrange_exx + for vrange in (vrange_exx, vrange_eyy, vrange_exy, vrange_theta): + assert len(vrange) == 2, "vranges must have length 2" + vmin_exx, vmax_exx = vrange_exx[0] / 100.0, vrange_exx[1] / 100.0 + vmin_eyy, vmax_eyy = vrange_eyy[0] / 100.0, vrange_eyy[1] / 100.0 + vmin_exy, vmax_exy = vrange_exy[0] / 100.0, vrange_exy[1] / 100.0 + # theta is plotted in units of degrees + vmin_theta, vmax_theta = vrange_theta[0] / (180.0 / np.pi), vrange_theta[1] / ( + 180.0 / np.pi + ) + + # Get images + e_xx = np.ma.array( + self.get_slice("exx").data, mask=self.get_slice("mask").data == False + ) + e_yy = np.ma.array( + self.get_slice("eyy").data, mask=self.get_slice("mask").data == False + ) + e_xy = np.ma.array( + self.get_slice("exy").data, mask=self.get_slice("mask").data == False + ) + theta = np.ma.array( + self.get_slice("theta").data, + mask=self.get_slice("mask").data == False, + ) + if flip_theta == True: + theta = -theta + + ## Plot + + # modify the figsize according to the image aspect ratio + ratio = np.sqrt(self.rshape[1]/self.rshape[0]) + figsize_mean = np.mean(figsize) + figsize = (figsize_mean*ratio, figsize_mean/ratio) + + # set up layout + if layout == 0: + fig, ((ax11, ax12), (ax21, ax22)) = plt.subplots(2, 2, figsize=figsize) + elif layout == 1: + figsize = (figsize[0]*np.sqrt(2),figsize[1]/np.sqrt(2)) + fig, (ax11, ax12, ax21, ax22) = plt.subplots(1, 4, figsize=figsize) + else: + figsize = (figsize[0]/np.sqrt(2),figsize[1]*np.sqrt(2)) + fig, (ax11, ax12, ax21, ax22) = plt.subplots(4, 1, figsize=figsize) + + # display images, returning cbar axis references + cax11 = show( + e_xx, + figax=(fig, ax11), + vmin=vmin_exx, + vmax=vmax_exx, + intensity_range="absolute", + cmap=cmap, + mask = self.mask, + mask_color = mask_color, + returncax=True, + ) + cax12 = show( + e_yy, + figax=(fig, ax12), + vmin=vmin_eyy, + vmax=vmax_eyy, + intensity_range="absolute", + cmap=cmap, + mask = self.mask, + mask_color = mask_color, + returncax=True, + ) + cax21 = show( + e_xy, + figax=(fig, ax21), + vmin=vmin_exy, + vmax=vmax_exy, + intensity_range="absolute", + cmap=cmap, + mask = self.mask, + mask_color = mask_color, + returncax=True, + ) + cax22 = show( + theta, + figax=(fig, ax22), + vmin=vmin_theta, + vmax=vmax_theta, + intensity_range="absolute", + cmap=cmap, + mask = self.mask, + mask_color = mask_color, + returncax=True, + ) + ax11.set_title(r"$\epsilon_{xx}$", size=titlesize) + ax12.set_title(r"$\epsilon_{yy}$", size=titlesize) + ax21.set_title(r"$\epsilon_{xy}$", size=titlesize) + ax22.set_title(r"$\theta$", size=titlesize) + + # Add black background + if bkgrd: + mask = np.ma.masked_where( + self.get_slice("mask").data.astype(bool), + np.zeros_like(self.get_slice("mask").data), + ) + ax11.matshow(mask, cmap="gray") + ax12.matshow(mask, cmap="gray") + ax21.matshow(mask, cmap="gray") + ax22.matshow(mask, cmap="gray") + + # add colorbars + show_cbars = np.array( + [ + "exx" in show_cbars, + "eyy" in show_cbars, + "exy" in show_cbars, + "theta" in show_cbars, + ] + ) + if np.any(show_cbars): + divider11 = make_axes_locatable(ax11) + divider12 = make_axes_locatable(ax12) + divider21 = make_axes_locatable(ax21) + divider22 = make_axes_locatable(ax22) + cbax11 = divider11.append_axes(layout_p[0], size="4%", pad=0.15) + cbax12 = divider12.append_axes(layout_p[1], size="4%", pad=0.15) + cbax21 = divider21.append_axes(layout_p[2], size="4%", pad=0.15) + cbax22 = divider22.append_axes(layout_p[3], size="4%", pad=0.15) + for ind, show_cbar, cax, cbax, vmin, vmax, tickside, tickunits in zip( + range(4), + show_cbars, + (cax11, cax12, cax21, cax22), + (cbax11, cbax12, cbax21, cbax22), + (vmin_exx, vmin_eyy, vmin_exy, vmin_theta), + (vmax_exx, vmax_eyy, vmax_exy, vmax_theta), + (layout_p[0], layout_p[1], layout_p[2], layout_p[3]), + ("% ", " %", "% ", r" $^\circ$"), + ): + if show_cbar: + ticks = np.linspace(vmin, vmax, ticknumber, endpoint=True) + if ind < 3: + ticklabels = np.round( + np.linspace(100 * vmin, 100 * vmax, ticknumber, endpoint=True), + decimals=2, + ).astype(str) + else: + ticklabels = np.round( + np.linspace( + (180 / np.pi) * vmin, + (180 / np.pi) * vmax, + ticknumber, + endpoint=True, + ), + decimals=2, + ).astype(str) + + if tickside in ("left", "right"): + cb = plt.colorbar( + cax, cax=cbax, ticks=ticks, orientation="vertical" + ) + cb.ax.set_yticklabels(ticklabels, size=ticklabelsize) + cbax.yaxis.set_ticks_position(tickside) + cbax.set_ylabel(tickunits, size=unitlabelsize, rotation=0) + cbax.yaxis.set_label_position(tickside) + else: + cb = plt.colorbar( + cax, cax=cbax, ticks=ticks, orientation="horizontal" + ) + cb.ax.set_xticklabels(ticklabels, size=ticklabelsize) + cbax.xaxis.set_ticks_position(tickside) + cbax.set_xlabel(tickunits, size=unitlabelsize, rotation=0) + cbax.xaxis.set_label_position(tickside) + else: + cbax.axis("off") + + # Add coordinate axes + if show_axes: + assert xaxis_space in ("R", "Q"), "xaxis_space must be 'R' or 'Q'" + show_which_axes = np.array( + [ + "exx" in axes_plots, + "eyy" in axes_plots, + "exy" in axes_plots, + "theta" in axes_plots, + ] + ) + for _show, _ax in zip(show_which_axes, (ax11, ax12, ax21, ax22)): + if _show: + if xaxis_space == "R": + ax_addaxes( + _ax, + self.g_reference[0], + self.g_reference[1], + axes_length, + axes_position[0], + axes_position[1], + width=axes_width, + color=axes_color, + labelaxes=labelaxes, + labelsize=axes_labelsize, + labelcolor=axes_labelcolor, + ) + else: + ax_addaxes_QtoR( + _ax, + self.g_reference[0], + self.g_reference[1], + axes_length, + axes_position[0], + axes_position[1], + QR_rotation, + width=axes_width, + color=axes_color, + labelaxes=labelaxes, + labelsize=axes_labelsize, + labelcolor=axes_labelcolor, + ) + + # Add borders + if bordercolor is not None: + for ax in (ax11, ax12, ax21, ax22): + for s in ["bottom", "top", "left", "right"]: + ax.spines[s].set_color(bordercolor) + ax.spines[s].set_linewidth(borderwidth) + ax.set_xticks([]) + ax.set_yticks([]) + + if not returnfig: + plt.show() + return + else: + axs = ((ax11, ax12), (ax21, ax22)) + return fig, axs + + def show_lattice_vectors( ar, diff --git a/py4DSTEM/visualize/vis_special.py b/py4DSTEM/visualize/vis_special.py index ba0ee024a..8612d65b8 100644 --- a/py4DSTEM/visualize/vis_special.py +++ b/py4DSTEM/visualize/vis_special.py @@ -404,307 +404,6 @@ def show_class_BPs_grid( return fig, axs -def show_strain( - strainmap, - vrange_exx, - vrange_theta, - vrange_exy=None, - vrange_eyy=None, - flip_theta=False, - bkgrd=True, - show_cbars=("exx", "eyy", "exy", "theta"), - bordercolor="k", - borderwidth=1, - titlesize=24, - ticklabelsize=16, - ticknumber=5, - unitlabelsize=24, - show_axes=True, - axes_x0=0, - axes_y0=0, - xaxis_x=1, - xaxis_y=0, - axes_length=10, - axes_width=1, - axes_color="r", - xaxis_space="Q", - labelaxes=True, - QR_rotation=0, - axes_labelsize=12, - axes_labelcolor="r", - axes_plots=("exx"), - cmap="RdBu_r", - layout=0, - figsize=(12, 12), - returnfig=False, -): - """ - Display a strain map, showing the 4 strain components (e_xx,e_yy,e_xy,theta), and - masking each image with strainmap.get_slice('mask') - - Args: - strainmap (RealSlice): - vrange_exx (length 2 list or tuple): - vrange_theta (length 2 list or tuple): - vrange_exy (length 2 list or tuple): - vrange_eyy (length 2 list or tuple): - flip_theta (bool): if True, take negative of angle - bkgrd (bool): - show_cbars (tuple of strings): Show colorbars for the specified axes. Must be a - tuple containing any, all, or none of ('exx','eyy','exy','theta'). - bordercolor (color): - borderwidth (number): - titlesize (number): - ticklabelsize (number): - ticknumber (number): number of ticks on colorbars - unitlabelsize (number): - show_axes (bool): - axes_x0 (number): - axes_y0 (number): - xaxis_x (number): - xaxis_y (number): - axes_length (number): - axes_width (number): - axes_color (color): - xaxis_space (string): must be 'Q' or 'R' - labelaxes (bool): - QR_rotation (number): - axes_labelsize (number): - axes_labelcolor (color): - axes_plots (tuple of strings): controls if coordinate axes showing the - orientation of the strain matrices are overlaid over any of the plots. - Must be a tuple of strings containing any, all, or none of - ('exx','eyy','exy','theta'). - cmap (colormap): - layout=0 (int): determines the layout of the grid which the strain components - will be plotted in. Must be in (0,1,2). 0=(2x2), 1=(1x4), 2=(4x1). - figsize (length 2 tuple of numbers): - returnfig (bool): - """ - # Lookup table for different layouts - assert layout in (0, 1, 2) - layout_lookup = { - 0: ["left", "right", "left", "right"], - 1: ["bottom", "bottom", "bottom", "bottom"], - 2: ["right", "right", "right", "right"], - } - layout_p = layout_lookup[layout] - - # Contrast limits - if vrange_exy is None: - vrange_exy = vrange_exx - if vrange_eyy is None: - vrange_eyy = vrange_exx - for vrange in (vrange_exx, vrange_eyy, vrange_exy, vrange_theta): - assert len(vrange) == 2, "vranges must have length 2" - vmin_exx, vmax_exx = vrange_exx[0] / 100.0, vrange_exx[1] / 100.0 - vmin_eyy, vmax_eyy = vrange_eyy[0] / 100.0, vrange_eyy[1] / 100.0 - vmin_exy, vmax_exy = vrange_exy[0] / 100.0, vrange_exy[1] / 100.0 - # theta is plotted in units of degrees - vmin_theta, vmax_theta = vrange_theta[0] / (180.0 / np.pi), vrange_theta[1] / ( - 180.0 / np.pi - ) - - # Get images - e_xx = np.ma.array( - strainmap.get_slice("e_xx").data, mask=strainmap.get_slice("mask").data == False - ) - e_yy = np.ma.array( - strainmap.get_slice("e_yy").data, mask=strainmap.get_slice("mask").data == False - ) - e_xy = np.ma.array( - strainmap.get_slice("e_xy").data, mask=strainmap.get_slice("mask").data == False - ) - theta = np.ma.array( - strainmap.get_slice("theta").data, - mask=strainmap.get_slice("mask").data == False, - ) - if flip_theta == True: - theta = -theta - - # Plot - if layout == 0: - fig, ((ax11, ax12), (ax21, ax22)) = plt.subplots(2, 2, figsize=figsize) - elif layout == 1: - fig, (ax11, ax12, ax21, ax22) = plt.subplots(1, 4, figsize=figsize) - else: - fig, (ax11, ax12, ax21, ax22) = plt.subplots(4, 1, figsize=figsize) - cax11 = show( - e_xx, - figax=(fig, ax11), - vmin=vmin_exx, - vmax=vmax_exx, - intensity_range="absolute", - cmap=cmap, - returncax=True, - ) - cax12 = show( - e_yy, - figax=(fig, ax12), - vmin=vmin_eyy, - vmax=vmax_eyy, - intensity_range="absolute", - cmap=cmap, - returncax=True, - ) - cax21 = show( - e_xy, - figax=(fig, ax21), - vmin=vmin_exy, - vmax=vmax_exy, - intensity_range="absolute", - cmap=cmap, - returncax=True, - ) - cax22 = show( - theta, - figax=(fig, ax22), - vmin=vmin_theta, - vmax=vmax_theta, - intensity_range="absolute", - cmap=cmap, - returncax=True, - ) - ax11.set_title(r"$\epsilon_{xx}$", size=titlesize) - ax12.set_title(r"$\epsilon_{yy}$", size=titlesize) - ax21.set_title(r"$\epsilon_{xy}$", size=titlesize) - ax22.set_title(r"$\theta$", size=titlesize) - - # Add black background - if bkgrd: - mask = np.ma.masked_where( - strainmap.get_slice("mask").data.astype(bool), - np.zeros_like(strainmap.get_slice("mask").data), - ) - ax11.matshow(mask, cmap="gray") - ax12.matshow(mask, cmap="gray") - ax21.matshow(mask, cmap="gray") - ax22.matshow(mask, cmap="gray") - - # Colorbars - show_cbars = np.array( - [ - "exx" in show_cbars, - "eyy" in show_cbars, - "exy" in show_cbars, - "theta" in show_cbars, - ] - ) - if np.any(show_cbars): - divider11 = make_axes_locatable(ax11) - divider12 = make_axes_locatable(ax12) - divider21 = make_axes_locatable(ax21) - divider22 = make_axes_locatable(ax22) - cbax11 = divider11.append_axes(layout_p[0], size="4%", pad=0.15) - cbax12 = divider12.append_axes(layout_p[1], size="4%", pad=0.15) - cbax21 = divider21.append_axes(layout_p[2], size="4%", pad=0.15) - cbax22 = divider22.append_axes(layout_p[3], size="4%", pad=0.15) - for ind, show_cbar, cax, cbax, vmin, vmax, tickside, tickunits in zip( - range(4), - show_cbars, - (cax11, cax12, cax21, cax22), - (cbax11, cbax12, cbax21, cbax22), - (vmin_exx, vmin_eyy, vmin_exy, vmin_theta), - (vmax_exx, vmax_eyy, vmax_exy, vmax_theta), - (layout_p[0], layout_p[1], layout_p[2], layout_p[3]), - ("% ", " %", "% ", r" $^\circ$"), - ): - if show_cbar: - ticks = np.linspace(vmin, vmax, ticknumber, endpoint=True) - if ind < 3: - ticklabels = np.round( - np.linspace(100 * vmin, 100 * vmax, ticknumber, endpoint=True), - decimals=2, - ).astype(str) - else: - ticklabels = np.round( - np.linspace( - (180 / np.pi) * vmin, - (180 / np.pi) * vmax, - ticknumber, - endpoint=True, - ), - decimals=2, - ).astype(str) - - if tickside in ("left", "right"): - cb = plt.colorbar( - cax, cax=cbax, ticks=ticks, orientation="vertical" - ) - cb.ax.set_yticklabels(ticklabels, size=ticklabelsize) - cbax.yaxis.set_ticks_position(tickside) - cbax.set_ylabel(tickunits, size=unitlabelsize, rotation=0) - cbax.yaxis.set_label_position(tickside) - else: - cb = plt.colorbar( - cax, cax=cbax, ticks=ticks, orientation="horizontal" - ) - cb.ax.set_xticklabels(ticklabels, size=ticklabelsize) - cbax.xaxis.set_ticks_position(tickside) - cbax.set_xlabel(tickunits, size=unitlabelsize, rotation=0) - cbax.xaxis.set_label_position(tickside) - else: - cbax.axis("off") - - # Add coordinate axes - if show_axes: - assert xaxis_space in ("R", "Q"), "xaxis_space must be 'R' or 'Q'" - show_which_axes = np.array( - [ - "exx" in axes_plots, - "eyy" in axes_plots, - "exy" in axes_plots, - "theta" in axes_plots, - ] - ) - for _show, _ax in zip(show_which_axes, (ax11, ax12, ax21, ax22)): - if _show: - if xaxis_space == "R": - ax_addaxes( - _ax, - xaxis_x, - xaxis_y, - axes_length, - axes_x0, - axes_y0, - width=axes_width, - color=axes_color, - labelaxes=labelaxes, - labelsize=axes_labelsize, - labelcolor=axes_labelcolor, - ) - else: - ax_addaxes_QtoR( - _ax, - xaxis_x, - xaxis_y, - axes_length, - axes_x0, - axes_y0, - QR_rotation, - width=axes_width, - color=axes_color, - labelaxes=labelaxes, - labelsize=axes_labelsize, - labelcolor=axes_labelcolor, - ) - - # Add borders - if bordercolor is not None: - for ax in (ax11, ax12, ax21, ax22): - for s in ["bottom", "top", "left", "right"]: - ax.spines[s].set_color(bordercolor) - ax.spines[s].set_linewidth(borderwidth) - ax.set_xticks([]) - ax.set_yticks([]) - - if not returnfig: - plt.show() - return - else: - axs = ((ax11, ax12), (ax21, ax22)) - return fig, axs - def show_pointlabels( ar, x, y, color="lightblue", size=20, alpha=1, returnfig=False, **kwargs From 143f373a9e99564505f17e1394d177e052fc1038 Mon Sep 17 00:00:00 2001 From: bsavitzky Date: Mon, 23 Oct 2023 16:39:35 +0100 Subject: [PATCH 07/10] autoformats --- py4DSTEM/process/latticevectors/index.py | 2 - py4DSTEM/process/strain/__init__.py | 2 - py4DSTEM/process/strain/latticevectors.py | 1 - py4DSTEM/process/strain/strain.py | 90 +++++++++++------------ 4 files changed, 44 insertions(+), 51 deletions(-) diff --git a/py4DSTEM/process/latticevectors/index.py b/py4DSTEM/process/latticevectors/index.py index 2d243cd0c..03cdf07ce 100644 --- a/py4DSTEM/process/latticevectors/index.py +++ b/py4DSTEM/process/latticevectors/index.py @@ -108,8 +108,6 @@ def generate_lattice(ux, uy, vx, vy, x0, y0, Q_Nx, Q_Ny, h_max=None, k_max=None) return ideal_lattice - - def bragg_vector_intensity_map_by_index(braggpeaks, h, k, symmetric=False): """ Returns a correlation intensity map for an indexed (h,k) Bragg vector diff --git a/py4DSTEM/process/strain/__init__.py b/py4DSTEM/process/strain/__init__.py index 213d5e812..b487c916b 100644 --- a/py4DSTEM/process/strain/__init__.py +++ b/py4DSTEM/process/strain/__init__.py @@ -7,6 +7,4 @@ get_reference_g1g2, get_strain_from_reference_g1g2, get_rotated_strain_map, - ) - diff --git a/py4DSTEM/process/strain/latticevectors.py b/py4DSTEM/process/strain/latticevectors.py index 30e5cc989..26c8d66a5 100644 --- a/py4DSTEM/process/strain/latticevectors.py +++ b/py4DSTEM/process/strain/latticevectors.py @@ -456,4 +456,3 @@ def get_rotated_strain_map(unrotated_strain_map, xaxis_x, xaxis_y, flip_theta): rotated_strain_map.data[3, :, :] = unrotated_strain_map.get_slice("theta").data rotated_strain_map.data[4, :, :] = unrotated_strain_map.get_slice("mask").data return rotated_strain_map - diff --git a/py4DSTEM/process/strain/strain.py b/py4DSTEM/process/strain/strain.py index cd662b8f1..538c90825 100644 --- a/py4DSTEM/process/strain/strain.py +++ b/py4DSTEM/process/strain/strain.py @@ -115,7 +115,7 @@ def origin(self): @property def mask(self): try: - return self.g1g2_map['mask'].data.astype('bool') + return self.g1g2_map["mask"].data.astype("bool") except: return np.ones(self.rshape, dtype=bool) @@ -138,9 +138,9 @@ def reset_calstate(self): def choose_lattice_vectors( self, - index_g1 = None, - index_g2 = None, - index_origin = None, + index_g1=None, + index_g2=None, + index_origin=None, subpixel="multicorr", upsample_factor=16, sigma=0, @@ -233,7 +233,9 @@ def choose_lattice_vectors( """ # validate inputs for i in (index_origin, index_g1, index_g2): - assert(isinstance(i, (int, np.integer)) or (i is None)), "indices must be integers!" + assert isinstance(i, (int, np.integer)) or ( + i is None + ), "indices must be integers!" # check the calstate assert ( self.calstate == self.braggvectors.calstate @@ -255,27 +257,27 @@ def choose_lattice_vectors( ) # guess the origin and g1 g2 vectors if indices aren't provided - if np.any([x is None for x in (index_g1,index_g2,index_origin)]): - + if np.any([x is None for x in (index_g1, index_g2, index_origin)]): # get distances and angles from calibrated origin - g_dists = np.hypot(g['x']-self.origin[0], g['y']-self.origin[1]) - g_angles = np.angle(g['x']-self.origin[0] + 1j*(g['y']-self.origin[1])) + g_dists = np.hypot(g["x"] - self.origin[0], g["y"] - self.origin[1]) + g_angles = np.angle( + g["x"] - self.origin[0] + 1j * (g["y"] - self.origin[1]) + ) # guess the origin if index_origin is None: index_origin = np.argmin(g_dists) - g_dists[index_origin] = 2*np.max(g_dists) + g_dists[index_origin] = 2 * np.max(g_dists) # guess g1 if index_g1 is None: index_g1 = np.argmin(g_dists) - g_dists[index_g1] = 2*np.max(g_dists) + g_dists[index_g1] = 2 * np.max(g_dists) # guess g2 if index_g2 is None: - angle_scaling = np.cos(g_angles - g_angles[index_g1])**2 - index_g2 = np.argmin(g_dists*(angle_scaling+0.1)) - + angle_scaling = np.cos(g_angles - g_angles[index_g1]) ** 2 + index_g2 = np.argmin(g_dists * (angle_scaling + 0.1)) # get the lattice vectors gx, gy = g["x"], g["y"] @@ -368,8 +370,8 @@ def choose_lattice_vectors( self.g2 = g2 # center the bragg directions and store - braggdirections.data['qx'] -= self.origin[0] - braggdirections.data['qy'] -= self.origin[1] + braggdirections.data["qx"] -= self.origin[0] + braggdirections.data["qy"] -= self.origin[1] self.braggdirections = braggdirections # return @@ -409,7 +411,6 @@ def fit_lattice_vectors( self.calstate == self.braggvectors.calstate ), "The calibration state has changed! To resync the calibration state, use `.reset_calstate`." - ### add indices to the bragg vectors # validate mask @@ -422,7 +423,7 @@ def fit_lattice_vectors( # set up new braggpeaks PLA indexed_braggpeaks = PointListArray( - dtype = [ + dtype=[ ("qx", float), ("qy", float), ("intensity", float), @@ -446,8 +447,8 @@ def fit_lattice_vectors( ) for i in range(pl.data.shape[0]): r = np.hypot( - pl.data["qx"][i]-self.braggdirections.data["qx"], - pl.data["qy"][i]-self.braggdirections.data["qy"] + pl.data["qx"][i] - self.braggdirections.data["qx"], + pl.data["qy"][i] - self.braggdirections.data["qy"], ) ind = np.argmin(r) if r[ind] <= max_peak_spacing: @@ -462,12 +463,10 @@ def fit_lattice_vectors( ) self.bragg_vectors_indexed = indexed_braggpeaks - ### fit bragg vectors g1g2_map = fit_lattice_vectors_all_DPs(self.bragg_vectors_indexed) self.g1g2_map = g1g2_map - # return if returncalc: return self.bragg_vectors_indexed, self.g1g2_map @@ -496,7 +495,7 @@ def get_strain( if mask is None: mask = self.mask - #mask = np.ones(self.g1g2_map.shape, dtype="bool") + # mask = np.ones(self.g1g2_map.shape, dtype="bool") # strainmap_g1g2 = get_strain_from_reference_region( # self.g1g2_map, # mask=mask, @@ -522,11 +521,11 @@ def get_strain( flip_theta=flip_theta, ) - self.data[0] = strainmap_rotated['e_xx'].data - self.data[1] = strainmap_rotated['e_yy'].data - self.data[2] = strainmap_rotated['e_xy'].data - self.data[3] = strainmap_rotated['theta'].data - self.data[4] = strainmap_rotated['mask'].data + self.data[0] = strainmap_rotated["e_xx"].data + self.data[1] = strainmap_rotated["e_yy"].data + self.data[2] = strainmap_rotated["e_xy"].data + self.data[3] = strainmap_rotated["theta"].data + self.data[4] = strainmap_rotated["mask"].data self.g_reference = g_reference figsize = kwargs.pop("figsize", (14, 4)) @@ -556,7 +555,6 @@ def get_strain( if returncalc: return self.strainmap - def show_strain( self, vrange_exx, @@ -573,7 +571,7 @@ def show_strain( ticknumber=5, unitlabelsize=24, show_axes=False, - axes_position = (0,0), + axes_position=(0, 0), axes_length=10, axes_width=1, axes_color="w", @@ -584,7 +582,7 @@ def show_strain( axes_labelcolor="r", axes_plots=("exx"), cmap="RdBu_r", - mask_color = 'k', + mask_color="k", layout=0, figsize=(12, 12), returnfig=False, @@ -675,18 +673,18 @@ def show_strain( ## Plot # modify the figsize according to the image aspect ratio - ratio = np.sqrt(self.rshape[1]/self.rshape[0]) + ratio = np.sqrt(self.rshape[1] / self.rshape[0]) figsize_mean = np.mean(figsize) - figsize = (figsize_mean*ratio, figsize_mean/ratio) + figsize = (figsize_mean * ratio, figsize_mean / ratio) # set up layout if layout == 0: fig, ((ax11, ax12), (ax21, ax22)) = plt.subplots(2, 2, figsize=figsize) elif layout == 1: - figsize = (figsize[0]*np.sqrt(2),figsize[1]/np.sqrt(2)) + figsize = (figsize[0] * np.sqrt(2), figsize[1] / np.sqrt(2)) fig, (ax11, ax12, ax21, ax22) = plt.subplots(1, 4, figsize=figsize) else: - figsize = (figsize[0]/np.sqrt(2),figsize[1]*np.sqrt(2)) + figsize = (figsize[0] / np.sqrt(2), figsize[1] * np.sqrt(2)) fig, (ax11, ax12, ax21, ax22) = plt.subplots(4, 1, figsize=figsize) # display images, returning cbar axis references @@ -697,8 +695,8 @@ def show_strain( vmax=vmax_exx, intensity_range="absolute", cmap=cmap, - mask = self.mask, - mask_color = mask_color, + mask=self.mask, + mask_color=mask_color, returncax=True, ) cax12 = show( @@ -708,8 +706,8 @@ def show_strain( vmax=vmax_eyy, intensity_range="absolute", cmap=cmap, - mask = self.mask, - mask_color = mask_color, + mask=self.mask, + mask_color=mask_color, returncax=True, ) cax21 = show( @@ -719,8 +717,8 @@ def show_strain( vmax=vmax_exy, intensity_range="absolute", cmap=cmap, - mask = self.mask, - mask_color = mask_color, + mask=self.mask, + mask_color=mask_color, returncax=True, ) cax22 = show( @@ -730,8 +728,8 @@ def show_strain( vmax=vmax_theta, intensity_range="absolute", cmap=cmap, - mask = self.mask, - mask_color = mask_color, + mask=self.mask, + mask_color=mask_color, returncax=True, ) ax11.set_title(r"$\epsilon_{xx}$", size=titlesize) @@ -782,7 +780,9 @@ def show_strain( ticks = np.linspace(vmin, vmax, ticknumber, endpoint=True) if ind < 3: ticklabels = np.round( - np.linspace(100 * vmin, 100 * vmax, ticknumber, endpoint=True), + np.linspace( + 100 * vmin, 100 * vmax, ticknumber, endpoint=True + ), decimals=2, ).astype(str) else: @@ -874,8 +874,6 @@ def show_strain( axs = ((ax11, ax12), (ax21, ax22)) return fig, axs - - def show_lattice_vectors( ar, x0, From c926d8e21a85f8a4bd3b7a602596aebbb88afb26 Mon Sep 17 00:00:00 2001 From: bsavitzky Date: Mon, 23 Oct 2023 17:06:17 +0100 Subject: [PATCH 08/10] autoformats --- py4DSTEM/visualize/vis_special.py | 1 - 1 file changed, 1 deletion(-) diff --git a/py4DSTEM/visualize/vis_special.py b/py4DSTEM/visualize/vis_special.py index 8612d65b8..d1efbd023 100644 --- a/py4DSTEM/visualize/vis_special.py +++ b/py4DSTEM/visualize/vis_special.py @@ -404,7 +404,6 @@ def show_class_BPs_grid( return fig, axs - def show_pointlabels( ar, x, y, color="lightblue", size=20, alpha=1, returnfig=False, **kwargs ): From 3048ebf49eb8d2d0d4d93bda9cf07e3348ed352b Mon Sep 17 00:00:00 2001 From: bsavitzky Date: Mon, 23 Oct 2023 18:06:13 +0100 Subject: [PATCH 09/10] rms deprecated latticevectors module --- py4DSTEM/process/__init__.py | 1 - py4DSTEM/process/latticevectors/__init__.py | 3 - py4DSTEM/process/latticevectors/fit.py | 71 ------ py4DSTEM/process/latticevectors/index.py | 147 ----------- .../process/latticevectors/initialguess.py | 229 ------------------ 5 files changed, 451 deletions(-) delete mode 100644 py4DSTEM/process/latticevectors/__init__.py delete mode 100644 py4DSTEM/process/latticevectors/fit.py delete mode 100644 py4DSTEM/process/latticevectors/index.py delete mode 100644 py4DSTEM/process/latticevectors/initialguess.py diff --git a/py4DSTEM/process/__init__.py b/py4DSTEM/process/__init__.py index 0df11ef01..6f0019019 100644 --- a/py4DSTEM/process/__init__.py +++ b/py4DSTEM/process/__init__.py @@ -1,7 +1,6 @@ from py4DSTEM.process.polar import PolarDatacube from py4DSTEM.process.strain.strain import StrainMap -from py4DSTEM.process import latticevectors from py4DSTEM.process import phase from py4DSTEM.process import calibration from py4DSTEM.process import utils diff --git a/py4DSTEM/process/latticevectors/__init__.py b/py4DSTEM/process/latticevectors/__init__.py deleted file mode 100644 index cda4f91e5..000000000 --- a/py4DSTEM/process/latticevectors/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from py4DSTEM.process.latticevectors.initialguess import * -from py4DSTEM.process.latticevectors.index import * -from py4DSTEM.process.latticevectors.fit import * diff --git a/py4DSTEM/process/latticevectors/fit.py b/py4DSTEM/process/latticevectors/fit.py deleted file mode 100644 index d36b10bca..000000000 --- a/py4DSTEM/process/latticevectors/fit.py +++ /dev/null @@ -1,71 +0,0 @@ -# Functions for fitting lattice vectors to measured Bragg peak positions - -import numpy as np -from numpy.linalg import lstsq - -from emdfile import tqdmnd, PointList, PointListArray -from py4DSTEM.data import RealSlice - - -def fit_lattice_vectors_masked(braggpeaks, mask, x0=0, y0=0, minNumPeaks=5): - """ - Fits lattice vectors g1,g2 to each diffraction pattern in braggpeaks corresponding - to a scan position for which mask==True. - - Args: - braggpeaks (PointList): A 6 coordinate PointList containing the data to fit. - Coords are 'qx','qy' (the bragg peak positions), 'intensity' (used as a - weighting factor when fitting), 'h','k' (indexing). May optionally also - contain 'index_mask' (bool), indicating which peaks have been successfully - indixed and should be used. - mask (boolean array): real space shaped (R_Nx,R_Ny); fit lattice vectors where - mask is True - x0 (float): x-coord of the origin - y0 (float): y-coord of the origin - minNumPeaks (int): if there are fewer than minNumPeaks peaks found in braggpeaks - which can be indexed, return None for all return parameters - - Returns: - (RealSlice): A RealSlice ``g1g2map`` containing the following 8 arrays: - - * ``g1g2_map.get_slice('x0')`` x-coord of the origin of the best fit lattice - * ``g1g2_map.get_slice('y0')`` y-coord of the origin - * ``g1g2_map.get_slice('g1x')`` x-coord of the first lattice vector - * ``g1g2_map.get_slice('g1y')`` y-coord of the first lattice vector - * ``g1g2_map.get_slice('g2x')`` x-coord of the second lattice vector - * ``g1g2_map.get_slice('g2y')`` y-coord of the second lattice vector - * ``g1g2_map.get_slice('error')`` the fit error - * ``g1g2_map.get_slice('mask')`` 1 for successful fits, 0 for unsuccessful - fits - """ - assert isinstance(braggpeaks, PointListArray) - assert np.all( - [name in braggpeaks.dtype.names for name in ("qx", "qy", "intensity")] - ) - - # Make RealSlice to contain outputs - slicelabels = ("x0", "y0", "g1x", "g1y", "g2x", "g2y", "error", "mask") - g1g2_map = RealSlice( - data=np.zeros((braggpeaks.shape[0], braggpeaks.shape[1], 8)), - slicelabels=slicelabels, - name="g1g2_map", - ) - - # Fit lattice vectors - for Rx, Ry in tqdmnd(braggpeaks.shape[0], braggpeaks.shape[1]): - if mask[Rx, Ry]: - braggpeaks_curr = braggpeaks.get_pointlist(Rx, Ry) - qx0, qy0, g1x, g1y, g2x, g2y, error = fit_lattice_vectors( - braggpeaks_curr, x0, y0, minNumPeaks - ) - # Store data - if g1x is not None: - g1g2_map.get_slice("x0").data[Rx, Ry] = qx0 - g1g2_map.get_slice("y0").data[Rx, Ry] = qx0 - g1g2_map.get_slice("g1x").data[Rx, Ry] = g1x - g1g2_map.get_slice("g1y").data[Rx, Ry] = g1y - g1g2_map.get_slice("g2x").data[Rx, Ry] = g2x - g1g2_map.get_slice("g2y").data[Rx, Ry] = g2y - g1g2_map.get_slice("error").data[Rx, Ry] = error - g1g2_map.get_slice("mask").data[Rx, Ry] = 1 - return g1g2_map diff --git a/py4DSTEM/process/latticevectors/index.py b/py4DSTEM/process/latticevectors/index.py deleted file mode 100644 index 03cdf07ce..000000000 --- a/py4DSTEM/process/latticevectors/index.py +++ /dev/null @@ -1,147 +0,0 @@ -# Functions for indexing the Bragg directions - -import numpy as np -from numpy.linalg import lstsq - -from emdfile import tqdmnd, PointList, PointListArray - - -def get_selected_lattice_vectors(gx, gy, i0, i1, i2): - """ - From a set of reciprocal lattice points (gx,gy), and indices in those arrays which - specify the center beam, the first basis lattice vector, and the second basis lattice - vector, computes and returns the lattice vectors g1 and g2. - - Args: - gx (1d array): the reciprocal lattice points x-coords - gy (1d array): the reciprocal lattice points y-coords - i0 (int): index in the (gx,gy) arrays specifying the center beam - i1 (int): index in the (gx,gy) arrays specifying the first basis lattice vector - i2 (int): index in the (gx,gy) arrays specifying the second basis lattice vector - - Returns: - (2-tuple of 2-tuples) A 2-tuple containing - - * **g1**: *(2-tuple)* the first lattice vector, (g1x,g1y) - * **g2**: *(2-tuple)* the second lattice vector, (g2x,g2y) - """ - for i in (i0, i1, i2): - assert isinstance(i, (int, np.integer)) - g1x = gx[i1] - gx[i0] - g1y = gy[i1] - gy[i0] - g2x = gx[i2] - gx[i0] - g2y = gy[i2] - gy[i0] - return (g1x, g1y), (g2x, g2y) - - -def generate_lattice(ux, uy, vx, vy, x0, y0, Q_Nx, Q_Ny, h_max=None, k_max=None): - """ - Returns a full reciprocal lattice stretching to the limits of the diffraction pattern - by making linear combinations of the lattice vectors up to (±h_max,±k_max). - - This can be useful when there are false peaks or missing peaks in the braggvectormap, - which can cause errors in the strain finding routines that rely on those peaks for - indexing. This allows us to create a reference lattice that has all combinations of - the lattice vectors all the way out to the edges of the frame, and excluding any - erroneous intermediate peaks. - - Args: - ux (float): x-coord of the u lattice vector - uy (float): y-coord of the u lattice vector - vx (float): x-coord of the v lattice vector - vy (float): y-coord of the v lattice vector - x0 (float): x-coord of the lattice origin - y0 (float): y-coord of the lattice origin - Q_Nx (int): diffraction pattern size in the x-direction - Q_Ny (int): diffraction pattern size in the y-direction - h_max, k_max (int): maximal indices for generating the lattice (the lattive is - always trimmed to fit inside the pattern so you can overestimate these, or - leave unspecified and they will be automatically found) - - Returns: - (PointList): A 4-coordinate PointList, ('qx','qy','h','k'), containing points - corresponding to linear combinations of the u and v vectors, with associated - indices - """ - - # Matrix of lattice vectors - beta = np.array([[ux, uy], [vx, vy]]) - - # If no max index is specified, (over)estimate based on image size - if (h_max is None) or (k_max is None): - (y, x) = np.mgrid[0:Q_Ny, 0:Q_Nx] - x = x - x0 - y = y - y0 - h_max = np.max(np.ceil(np.abs((x / ux, y / uy)))) - k_max = np.max(np.ceil(np.abs((x / vx, y / vy)))) - - (hlist, klist) = np.meshgrid( - np.arange(-h_max, h_max + 1), np.arange(-k_max, k_max + 1) - ) - - M_ideal = np.vstack((hlist.ravel(), klist.ravel())).T - ideal_peaks = np.matmul(M_ideal, beta) - - coords = [("qx", float), ("qy", float), ("h", int), ("k", int)] - - ideal_data = np.zeros(len(ideal_peaks[:, 0]), dtype=coords) - ideal_data["qx"] = ideal_peaks[:, 0] - ideal_data["qy"] = ideal_peaks[:, 1] - ideal_data["h"] = M_ideal[:, 0] - ideal_data["k"] = M_ideal[:, 1] - - ideal_lattice = PointList(data=ideal_data) - - # shift to the DP center - ideal_lattice.data["qx"] += x0 - ideal_lattice.data["qy"] += y0 - - # trim peaks outside the image - deletePeaks = ( - (ideal_lattice.data["qx"] > Q_Nx) - | (ideal_lattice.data["qx"] < 0) - | (ideal_lattice.data["qy"] > Q_Ny) - | (ideal_lattice.data["qy"] < 0) - ) - ideal_lattice.remove(deletePeaks) - - return ideal_lattice - - -def bragg_vector_intensity_map_by_index(braggpeaks, h, k, symmetric=False): - """ - Returns a correlation intensity map for an indexed (h,k) Bragg vector - Used to obtain a darkfield image corresponding to the (h,k) reflection - or a bightfield image when h=k=0 - - Args: - braggpeaks (PointListArray): must contain the coordinates 'h','k', and - 'intensity' - h, k (int): indices for the reflection to generate an intensity map from - symmetric (bool): if set to true, returns sum of intensity of (h,k), (-h,k), - (h,-k), (-h,-k) - - Returns: - (numpy array): a map of the intensity of the (h,k) Bragg vector correlation. - Same shape as the pointlistarray. - """ - assert isinstance(braggpeaks, PointListArray), "braggpeaks must be a PointListArray" - assert np.all([name in braggpeaks.dtype.names for name in ("h", "k", "intensity")]) - intensity_map = np.zeros(braggpeaks.shape, dtype=float) - - for Rx in range(braggpeaks.shape[0]): - for Ry in range(braggpeaks.shape[1]): - pl = braggpeaks.get_pointlist(Rx, Ry) - if pl.length > 0: - if symmetric: - matches = np.logical_and( - np.abs(pl.data["h"]) == np.abs(h), - np.abs(pl.data["k"]) == np.abs(k), - ) - else: - matches = np.logical_and(pl.data["h"] == h, pl.data["k"] == k) - - if len(matches) > 0: - intensity_map[Rx, Ry] = np.sum(pl.data["intensity"][matches]) - - return intensity_map diff --git a/py4DSTEM/process/latticevectors/initialguess.py b/py4DSTEM/process/latticevectors/initialguess.py deleted file mode 100644 index d8054143f..000000000 --- a/py4DSTEM/process/latticevectors/initialguess.py +++ /dev/null @@ -1,229 +0,0 @@ -# Obtain an initial guess at the lattice vectors - -import numpy as np -from scipy.ndimage import gaussian_filter -from skimage.transform import radon - -from py4DSTEM.process.utils import get_maxima_1D - - -def get_radon_scores( - braggvectormap, - mask=None, - N_angles=200, - sigma=2, - minSpacing=2, - minRelativeIntensity=0.05, -): - """ - Calculates a score function, score(angle), representing the likelihood that angle is - a principle lattice direction of the lattice in braggvectormap. - - The procedure is as follows: - If mask is not None, ignore any data in braggvectormap where mask is False. Useful - for removing the unscattered beam, which can dominate the results. - Take the Radon transform of the (masked) Bragg vector map. - For each angle, get the corresponding slice of the sinogram, and calculate its score. - If we let R_theta(r) be the sinogram slice at angle theta, and where r is the - sinogram position coordinate, then the score of the slice is given by - score(theta) = sum_i(R_theta(r_i)) / N_i - Here, r_i are the positions r of all local maxima in R_theta(r), and N_i is the - number of such maxima. Thus the score is large when there are few maxima which are - high intensity. - - Args: - braggvectormap (ndarray): the Bragg vector map - mask (ndarray of bools): ignore data in braggvectormap wherever mask==False - N_angles (int): the number of angles at which to calculate the score - sigma (float): smoothing parameter for local maximum identification - minSpacing (float): if two maxima are found in a radon slice closer than - minSpacing, the dimmer of the two is removed - minRelativeIntensity (float): maxima in each radon slice dimmer than - minRelativeIntensity compared to the most intense maximum are removed - - Returns: - (3-tuple) A 3-tuple containing: - - * **scores**: *(ndarray, len N_angles, floats)* the scores for each angle - * **thetas**: *(ndarray, len N_angles, floats)* the angles, in radians - * **sinogram**: *(ndarray)* the radon transform of braggvectormap*mask - """ - # Get sinogram - thetas = np.linspace(0, 180, N_angles) - if mask is not None: - sinogram = radon(braggvectormap * mask, theta=thetas, circle=False) - else: - sinogram = radon(braggvectormap, theta=thetas, circle=False) - - # Get scores - N_maxima = np.empty_like(thetas) - total_intensity = np.empty_like(thetas) - for i in range(len(thetas)): - theta = thetas[i] - - # Get radon transform slice - ind = np.argmin(np.abs(thetas - theta)) - sinogram_theta = sinogram[:, ind] - sinogram_theta = gaussian_filter(sinogram_theta, 2) - - # Get maxima - maxima = get_maxima_1D(sinogram_theta, sigma, minSpacing, minRelativeIntensity) - - # Calculate metrics - N_maxima[i] = len(maxima) - total_intensity[i] = np.sum(sinogram_theta[maxima]) - scores = total_intensity / N_maxima - - return scores, np.radians(thetas), sinogram - - -def get_lattice_directions_from_scores( - thetas, scores, sigma=2, minSpacing=2, minRelativeIntensity=0.05, index1=0, index2=0 -): - """ - Get the lattice directions from the scores of the radon transform slices. - - Args: - thetas (ndarray): the angles, in radians - scores (ndarray): the scores - sigma (float): gaussian blur for local maxima identification - minSpacing (float): minimum spacing for local maxima identification - minRelativeIntensity (float): minumum intensity, relative to the brightest - maximum, for local maxima identification - index1 (int): specifies which local maximum to use for the first lattice - direction, in order of maximum intensity - index2 (int): specifies the local maximum for the second lattice direction - - Returns: - (2-tuple) A 2-tuple containing: - - * **theta1**: *(float)* the first lattice direction, in radians - * **theta2**: *(float)* the second lattice direction, in radians - """ - assert len(thetas) == len(scores), "Size of thetas and scores must match" - - # Get first lattice direction - maxima1 = get_maxima_1D( - scores, sigma, minSpacing, minRelativeIntensity - ) # Get maxima - thetas_max1 = thetas[maxima1] - scores_max1 = scores[maxima1] - dtype = np.dtype( - [("thetas", thetas.dtype), ("scores", scores.dtype)] - ) # Sort by intensity - ar_structured = np.empty(len(thetas_max1), dtype=dtype) - ar_structured["thetas"] = thetas_max1 - ar_structured["scores"] = scores_max1 - ar_structured = np.sort(ar_structured, order="scores")[::-1] - theta1 = ar_structured["thetas"][index1] # Get direction 1 - - # Apply sin**2 damping - scores_damped = scores * np.sin(thetas - theta1) ** 2 - - # Get second lattice direction - maxima2 = get_maxima_1D( - scores_damped, sigma, minSpacing, minRelativeIntensity - ) # Get maxima - thetas_max2 = thetas[maxima2] - scores_max2 = scores[maxima2] - dtype = np.dtype( - [("thetas", thetas.dtype), ("scores", scores.dtype)] - ) # Sort by intensity - ar_structured = np.empty(len(thetas_max2), dtype=dtype) - ar_structured["thetas"] = thetas_max2 - ar_structured["scores"] = scores_max2 - ar_structured = np.sort(ar_structured, order="scores")[::-1] - theta2 = ar_structured["thetas"][index2] # Get direction 2 - - return theta1, theta2 - - -def get_lattice_vector_lengths( - u_theta, - v_theta, - thetas, - sinogram, - spacing_thresh=1.5, - sigma=1, - minSpacing=2, - minRelativeIntensity=0.1, -): - """ - Gets the lengths of the two lattice vectors from their angles and the sinogram. - - First, finds the spacing between peaks in the sinogram slices projected down the u- - and v- directions, u_proj and v_proj. Then, finds the lengths by taking:: - - |u| = v_proj/sin(u_theta-v_theta) - |v| = u_proj/sin(u_theta-v_theta) - - The most important thresholds for this function are spacing_thresh, which discards - any detected spacing between adjacent radon projection peaks which deviate from the - median spacing by more than this fraction, and minRelativeIntensity, which discards - detected maxima (from which spacings are then calculated) below this threshold - relative to the brightest maximum. - - Args: - u_theta (float): the angle of u, in radians - v_theta (float): the angle of v, in radians - thetas (ndarray): the angles corresponding to the sinogram - sinogram (ndarray): the sinogram - spacing_thresh (float): ignores spacings which are greater than spacing_thresh - times the median spacing - sigma (float): gaussian blur for local maxima identification - minSpacing (float): minimum spacing for local maxima identification - minRelativeIntensity (float): minumum intensity, relative to the brightest - maximum, for local maxima identification - - Returns: - (2-tuple) A 2-tuple containing: - - * **u_length**: *(float)* the length of u, in pixels - * **v_length**: *(float)* the length of v, in pixels - """ - assert ( - len(thetas) == sinogram.shape[1] - ), "thetas must corresponding to the number of sinogram projection directions." - - # Get u projected spacing - ind = np.argmin(np.abs(thetas - u_theta)) - sinogram_slice = sinogram[:, ind] - maxima = get_maxima_1D(sinogram_slice, sigma, minSpacing, minRelativeIntensity) - spacings = np.sort(np.arange(sinogram_slice.shape[0])[maxima]) - spacings = spacings[1:] - spacings[:-1] - mask = ( - np.array( - [ - max(i, np.median(spacings)) / min(i, np.median(spacings)) - for i in spacings - ] - ) - < spacing_thresh - ) - spacings = spacings[mask] - u_projected_spacing = np.mean(spacings) - - # Get v projected spacing - ind = np.argmin(np.abs(thetas - v_theta)) - sinogram_slice = sinogram[:, ind] - maxima = get_maxima_1D(sinogram_slice, sigma, minSpacing, minRelativeIntensity) - spacings = np.sort(np.arange(sinogram_slice.shape[0])[maxima]) - spacings = spacings[1:] - spacings[:-1] - mask = ( - np.array( - [ - max(i, np.median(spacings)) / min(i, np.median(spacings)) - for i in spacings - ] - ) - < spacing_thresh - ) - spacings = spacings[mask] - v_projected_spacing = np.mean(spacings) - - # Get u and v lengths - sin_uv = np.sin(np.abs(u_theta - v_theta)) - u_length = v_projected_spacing / sin_uv - v_length = u_projected_spacing / sin_uv - - return u_length, v_length From 9d2e36c14d479c13c597026b585ffe8523de8a2c Mon Sep 17 00:00:00 2001 From: bsavitzky Date: Mon, 23 Oct 2023 18:08:29 +0100 Subject: [PATCH 10/10] rms deprecated latticevectors module --- py4DSTEM/process/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/py4DSTEM/process/__init__.py b/py4DSTEM/process/__init__.py index 6f0019019..0509d181e 100644 --- a/py4DSTEM/process/__init__.py +++ b/py4DSTEM/process/__init__.py @@ -5,6 +5,5 @@ from py4DSTEM.process import calibration from py4DSTEM.process import utils from py4DSTEM.process import classification -from py4DSTEM.process import latticevectors from py4DSTEM.process import diffraction from py4DSTEM.process import wholepatternfit