From a0ea19973a4769f2a515d5f8e2d53b2a5d3127b8 Mon Sep 17 00:00:00 2001 From: Colin Date: Fri, 12 May 2023 11:48:34 -0700 Subject: [PATCH 01/37] Adding grain clustering function --- py4DSTEM/process/diffraction/crystal.py | 1 + py4DSTEM/process/diffraction/crystal_ACOM.py | 241 ++++++++++++++++++- py4DSTEM/process/diffraction/crystal_viz.py | 6 +- 3 files changed, 243 insertions(+), 5 deletions(-) diff --git a/py4DSTEM/process/diffraction/crystal.py b/py4DSTEM/process/diffraction/crystal.py index e0b171875..5bd931b1f 100644 --- a/py4DSTEM/process/diffraction/crystal.py +++ b/py4DSTEM/process/diffraction/crystal.py @@ -36,6 +36,7 @@ class Crystal: orientation_plan, match_orientations, match_single_pattern, + cluster_grains, calculate_strain, save_ang_file, symmetry_reduce_directions, diff --git a/py4DSTEM/process/diffraction/crystal_ACOM.py b/py4DSTEM/process/diffraction/crystal_ACOM.py index bb4623491..67c0fe0b4 100644 --- a/py4DSTEM/process/diffraction/crystal_ACOM.py +++ b/py4DSTEM/process/diffraction/crystal_ACOM.py @@ -2,6 +2,7 @@ import matplotlib.pyplot as plt import os from typing import Union, Optional +import time, sys from py4DSTEM.process.diffraction.utils import Orientation, OrientationMap, axisEqual3D from py4DSTEM.process.utils import electron_wavelength_angstrom @@ -753,9 +754,10 @@ def match_orientations( self, bragg_peaks_array: PointListArray, num_matches_return: int = 1, - min_number_peaks = 3, - inversion_symmetry = True, - multiple_corr_reset = True, + min_number_peaks: int = 3, + inversion_symmetry: bool = True, + multiple_corr_reset: bool = True, + return_orientation: bool = True, progress_bar: bool = True, ): ''' @@ -786,7 +788,13 @@ def match_orientations( orientation_map.set_orientation(orientation,rx,ry) - return orientation_map + # assign and return + self.orientation_map = orientation_map + + if return_orientation: + return orientation_map + else: + return def match_single_pattern( self, @@ -1563,6 +1571,207 @@ def match_single_pattern( + + +def cluster_grains( + self, + threshold_add = 1.0, + threshold_grow = 0.0, + angle_tolerance_deg = 5.0, + progress_bar = True, + ): + """ + Cluster grains from a specific radial bin + + Parameters + -------- + corr_threshold_add: float + Minimum signal required for a probe position to initialize a cluster. + corr_threshold_grow: float + Minimum signal required for a probe position to be added to a cluster. + angle_tolerance_deg: float + Rotation rolerance for clustering grains. + progress_bar: bool + Turns on the progress bar for the polar transformation + + Returns + -------- + + + """ + + # symmetry operators + sym = self.symmetry_operators + + # Get data + matrix = self.orientation_map.matrix.copy() + corr = self.orientation_map.corr.copy() + corr_init = corr.copy() + mark = corr >= threshold_grow + corr[np.logical_not(mark)] = 0 + + # phi = np.squeeze(self.radial_peaks[:,:,radial_index,:,0]).copy() + # sig = np.squeeze(self.radial_peaks[:,:,radial_index,:,1]).copy() + # sig_init = sig.copy() + # mark = sig >= threshold_grow + # sig[np.logical_not(mark)] = 0 + + # init + self.cluster_sizes = np.array((), dtype='int') + self.cluster_sig = np.array(()) + self.cluster_inds = [] + inds_all = np.zeros_like(corr, dtype='int') + inds_all.ravel()[:] = np.arange(inds_all.size) + + # Tolerance + tol = np.deg2rad(angle_tolerance_deg) + + # Main loop + search = True + while search is True: + inds_grain = np.argmax(corr) + val = corr.ravel()[inds_grain] + + if val < threshold_add: + search = False + + else: + # progressbar + if progress_bar: + comp = 1 - np.mean(np.max(mark,axis = 2)) + update_progress(comp) + + # # Start cluster + # x,y,z = np.unravel_index(inds_grain, sig.shape) + # mark[x,y,z] = False + # sig[x,y,z] = 0 + # phi_cluster = phi[x,y,z] + + # # Neighbors to search + # xr = np.clip(x + np.arange(-1,2,dtype='int'), 0, sig.shape[0] - 1) + # yr = np.clip(y + np.arange(-1,2,dtype='int'), 0, sig.shape[1] - 1) + # inds_cand = inds_all[xr,yr,:].ravel() + # inds_cand = np.delete(inds_cand, mark.ravel()[inds_cand] == False) + # # [mark[xr,yr,:].ravel()] + + # if inds_cand.size == 0: + # grow = False + # else: + # grow = True + + # # grow the cluster + # while grow is True: + # inds_new = np.array((),dtype='int') + + # keep = np.zeros(inds_cand.size, dtype='bool') + # for a0 in range(inds_cand.size): + # xc,yc,zc = np.unravel_index(inds_cand[a0], sig.shape) + + # phi_test = phi[xc,yc,zc] + # dphi = np.mod(phi_cluster - phi_test + np.pi/2.0, np.pi) - np.pi/2.0 + + # if np.abs(dphi) < tol: + # keep[a0] = True + + # sig[xc,yc,zc] = 0 + # mark[xc,yc,zc] = False + + # xr = np.clip(xc + np.arange(-1,2,dtype='int'), 0, sig.shape[0] - 1) + # yr = np.clip(yc + np.arange(-1,2,dtype='int'), 0, sig.shape[1] - 1) + # inds_add = inds_all[xr,yr,:].ravel() + # inds_new = np.append(inds_new, inds_add) + + + # inds_grain = np.append(inds_grain, inds_cand[keep]) + # inds_cand = np.unique(np.delete(inds_new, mark.ravel()[inds_new] == False)) + + # if inds_cand.size == 0: + # grow = False + + # # convert grain to x,y coordinates, add = list + # xg,yg,zg = np.unravel_index(inds_grain, sig.shape) + # xyg = np.unique(np.vstack((xg,yg)), axis = 1) + # sig_mean = np.mean(sig_init.ravel()[inds_grain]) + # self.cluster_sizes = np.append(self.cluster_sizes, xyg.shape[1]) + # self.cluster_sig = np.append(self.cluster_sig, sig_mean) + # self.cluster_inds.append(xyg) + search = False + + # finish progressbar + if progress_bar: + update_progress(1) + +# def cluster_plot_size( +# self, +# area_max = None, +# weight_intensity = False, +# pixel_area = 1.0, +# pixel_area_units = 'px^2', +# figsize = (8,6), +# returnfig = False, +# ): +# """ +# Plot the cluster sizes + +# Parameters +# -------- +# area_max: int (optional) +# Max area bin in pixels +# weight_intensity: bool +# Weight histogram by the peak intensity. +# pixel_area: float +# Size of pixel area unit square +# pixel_area_units: string +# Units of the pixel area +# figsize: tuple +# Size of the figure panel +# returnfig: bool +# Setting this to true returns the figure and axis handles + +# Returns +# -------- +# fig, ax (optional) +# Figure and axes handles + +# """ + +# if area_max is None: +# area_max = np.max(self.cluster_sizes) +# area = np.arange(area_max) +# sub = self.cluster_sizes.astype('int') < area_max +# if weight_intensity: +# hist = np.bincount( +# self.cluster_sizes[sub], +# weights = self.cluster_sig[sub], +# minlength = area_max, +# ) +# else: +# hist = np.bincount( +# self.cluster_sizes[sub], +# minlength = area_max, +# ) + + +# # plotting +# fig,ax = plt.subplots(figsize = figsize) +# ax.bar( +# area * pixel_area, +# hist, +# width = 0.8 * pixel_area, +# ) + +# ax.set_xlabel('Grain Area [' + pixel_area_units + ']') +# if weight_intensity: +# ax.set_ylabel('Total Signal [arb. units]') +# else: +# ax.set_ylabel('Number of Grains') + +# if returnfig: +# return fig,ax + + + + def calculate_strain( self, bragg_peaks_array: PointListArray, @@ -2066,3 +2275,27 @@ def symmetry_reduce_directions( # "-3m": ["fiber", [0, 0, 1], [90.0, 60.0]], # "-3m": ["fiber", [0, 0, 1], [180.0, 30.0]], + + +# Progressbar taken from stackexchange: +# https://stackoverflow.com/questions/3160699/python-progress-bar +def update_progress(progress): + barLength = 60 # Modify this to change the length of the progress bar + status = "" + if isinstance(progress, int): + progress = float(progress) + if not isinstance(progress, float): + progress = 0 + status = "error: progress var must be float\r\n" + if progress < 0: + progress = 0 + status = "Halt...\r\n" + if progress >= 1: + progress = 1 + status = "Done...\r\n" + block = int(round(barLength*progress)) + text = "\rPercent: [{0}] {1}% {2}".format( "#"*block + "-"*(barLength-block), + np.round(progress*100,4), + status) + sys.stdout.write(text) + sys.stdout.flush() diff --git a/py4DSTEM/process/diffraction/crystal_viz.py b/py4DSTEM/process/diffraction/crystal_viz.py index f0ed95663..a2247b3e0 100644 --- a/py4DSTEM/process/diffraction/crystal_viz.py +++ b/py4DSTEM/process/diffraction/crystal_viz.py @@ -955,7 +955,7 @@ def overline(x): def plot_orientation_maps( self, - orientation_map, + orientation_map = None, orientation_ind: int = 0, dir_in_plane_degrees: float = 0.0, corr_range: np.ndarray = np.array([0, 5]), @@ -976,6 +976,7 @@ def plot_orientation_maps( Args: orientation_map (OrientationMap): Class containing orientation matrices, correlation values, etc. + Optional - can reference internally stored OrientationMap. orientation_ind (int): Which orientation match to plot if num_matches > 1 dir_in_plane_degrees (float): In-plane angle to plot in degrees. Default is 0 / x-axis / vertical down. corr_range (np.ndarray): Correlation intensity range for the plot @@ -1003,6 +1004,9 @@ def plot_orientation_maps( """ # Inputs + if orientation_map is None: + orientation_map = self.orientation_map + # Legend size leg_size = np.array([300, 300], dtype="int") From 234e372058ad14f1504fd10e6a918d8624cf0701 Mon Sep 17 00:00:00 2001 From: Colin Date: Fri, 12 May 2023 14:18:39 -0700 Subject: [PATCH 02/37] Fixing bugs in the grain clustering --- py4DSTEM/process/diffraction/crystal.py | 2 + py4DSTEM/process/diffraction/crystal_ACOM.py | 214 +++++++------------ py4DSTEM/process/diffraction/crystal_viz.py | 90 ++++++++ 3 files changed, 166 insertions(+), 140 deletions(-) diff --git a/py4DSTEM/process/diffraction/crystal.py b/py4DSTEM/process/diffraction/crystal.py index 5bd931b1f..f93702c5d 100644 --- a/py4DSTEM/process/diffraction/crystal.py +++ b/py4DSTEM/process/diffraction/crystal.py @@ -52,6 +52,8 @@ class Crystal: plot_orientation_plan, plot_orientation_maps, plot_fiber_orientation_maps, + plot_clusters, + plot_cluster_size, ) from py4DSTEM.process.diffraction.crystal_calibrate import ( diff --git a/py4DSTEM/process/diffraction/crystal_ACOM.py b/py4DSTEM/process/diffraction/crystal_ACOM.py index 67c0fe0b4..6a8fe5280 100644 --- a/py4DSTEM/process/diffraction/crystal_ACOM.py +++ b/py4DSTEM/process/diffraction/crystal_ACOM.py @@ -1576,7 +1576,7 @@ def match_single_pattern( def cluster_grains( self, threshold_add = 1.0, - threshold_grow = 0.0, + threshold_grow = 0.1, angle_tolerance_deg = 5.0, progress_bar = True, ): @@ -1604,23 +1604,19 @@ def cluster_grains( sym = self.symmetry_operators # Get data + # Correlation data = signal to cluster with + sig = self.orientation_map.corr.copy() + sig_init = sig.copy() + mark = sig >= threshold_grow + sig[np.logical_not(mark)] = 0 + # orientation matrix used for angle tolerance matrix = self.orientation_map.matrix.copy() - corr = self.orientation_map.corr.copy() - corr_init = corr.copy() - mark = corr >= threshold_grow - corr[np.logical_not(mark)] = 0 - - # phi = np.squeeze(self.radial_peaks[:,:,radial_index,:,0]).copy() - # sig = np.squeeze(self.radial_peaks[:,:,radial_index,:,1]).copy() - # sig_init = sig.copy() - # mark = sig >= threshold_grow - # sig[np.logical_not(mark)] = 0 - + # init self.cluster_sizes = np.array((), dtype='int') self.cluster_sig = np.array(()) self.cluster_inds = [] - inds_all = np.zeros_like(corr, dtype='int') + inds_all = np.zeros_like(sig, dtype='int') inds_all.ravel()[:] = np.arange(inds_all.size) # Tolerance @@ -1629,8 +1625,9 @@ def cluster_grains( # Main loop search = True while search is True: - inds_grain = np.argmax(corr) - val = corr.ravel()[inds_grain] + inds_grain = np.argmax(sig) + + val = sig.ravel()[inds_grain] if val < threshold_add: search = False @@ -1641,136 +1638,73 @@ def cluster_grains( comp = 1 - np.mean(np.max(mark,axis = 2)) update_progress(comp) - # # Start cluster - # x,y,z = np.unravel_index(inds_grain, sig.shape) - # mark[x,y,z] = False - # sig[x,y,z] = 0 - # phi_cluster = phi[x,y,z] - - # # Neighbors to search - # xr = np.clip(x + np.arange(-1,2,dtype='int'), 0, sig.shape[0] - 1) - # yr = np.clip(y + np.arange(-1,2,dtype='int'), 0, sig.shape[1] - 1) - # inds_cand = inds_all[xr,yr,:].ravel() - # inds_cand = np.delete(inds_cand, mark.ravel()[inds_cand] == False) - # # [mark[xr,yr,:].ravel()] - - # if inds_cand.size == 0: - # grow = False - # else: - # grow = True - - # # grow the cluster - # while grow is True: - # inds_new = np.array((),dtype='int') - - # keep = np.zeros(inds_cand.size, dtype='bool') - # for a0 in range(inds_cand.size): - # xc,yc,zc = np.unravel_index(inds_cand[a0], sig.shape) - - # phi_test = phi[xc,yc,zc] - # dphi = np.mod(phi_cluster - phi_test + np.pi/2.0, np.pi) - np.pi/2.0 - - # if np.abs(dphi) < tol: - # keep[a0] = True + # Start cluster + x,y,z = np.unravel_index(inds_grain, sig.shape) + mark[x,y,z] = False + sig[x,y,z] = 0 + matrix_cluster = matrix[x,y,z] - # sig[xc,yc,zc] = 0 - # mark[xc,yc,zc] = False + # Neighbors to search + xr = np.clip(x + np.arange(-1,2,dtype='int'), 0, sig.shape[0] - 1) + yr = np.clip(y + np.arange(-1,2,dtype='int'), 0, sig.shape[1] - 1) + inds_cand = inds_all[xr[:,None],yr[None],:].ravel() + inds_cand = np.delete(inds_cand, mark.ravel()[inds_cand] == False) - # xr = np.clip(xc + np.arange(-1,2,dtype='int'), 0, sig.shape[0] - 1) - # yr = np.clip(yc + np.arange(-1,2,dtype='int'), 0, sig.shape[1] - 1) - # inds_add = inds_all[xr,yr,:].ravel() - # inds_new = np.append(inds_new, inds_add) - - - # inds_grain = np.append(inds_grain, inds_cand[keep]) - # inds_cand = np.unique(np.delete(inds_new, mark.ravel()[inds_new] == False)) - - # if inds_cand.size == 0: - # grow = False - - # # convert grain to x,y coordinates, add = list - # xg,yg,zg = np.unravel_index(inds_grain, sig.shape) - # xyg = np.unique(np.vstack((xg,yg)), axis = 1) - # sig_mean = np.mean(sig_init.ravel()[inds_grain]) - # self.cluster_sizes = np.append(self.cluster_sizes, xyg.shape[1]) - # self.cluster_sig = np.append(self.cluster_sig, sig_mean) - # self.cluster_inds.append(xyg) - search = False + if inds_cand.size == 0: + grow = False + else: + grow = True + + # grow the cluster + while grow is True: + inds_new = np.array((),dtype='int') + + keep = np.zeros(inds_cand.size, dtype='bool') + for a0 in range(inds_cand.size): + xc,yc,zc = np.unravel_index(inds_cand[a0], sig.shape) + + # Angle test between orientation matrices + dphi = np.min(np.arccos(np.clip((np.trace( + self.symmetry_operators @ matrix[xc,yc,zc] \ + @ np.transpose(matrix_cluster), + axis1=1, + axis2=2)-1)/2,-1,1))) + # dphi = np.min(np.arccos(np.clip((np.trace( + # matrix_cluster @ \ + # np.transpose(self.symmetry_operators @ matrix[xc,yc,zc],(0,2,1)), + # axis1=1, + # axis2=2)-1)/2,-1,1))) + + if np.abs(dphi) < tol: + keep[a0] = True + + sig[xc,yc,zc] = 0 + mark[xc,yc,zc] = False + + xr = np.clip(xc + np.arange(-1,2,dtype='int'), 0, sig.shape[0] - 1) + yr = np.clip(yc + np.arange(-1,2,dtype='int'), 0, sig.shape[1] - 1) + inds_add = inds_all[xr[:,None],yr[None],:].ravel() + inds_new = np.append(inds_new, inds_add) + + inds_grain = np.append(inds_grain, inds_cand[keep]) + inds_cand = np.unique(np.delete(inds_new, mark.ravel()[inds_new] == False)) + # print(inds_cand) + + if inds_cand.size == 0: + grow = False + + # convert grain to x,y coordinates, add = list + xg,yg,zg = np.unravel_index(inds_grain, sig.shape) + xyg = np.unique(np.vstack((xg,yg)), axis = 1) + sig_mean = np.mean(sig_init.ravel()[inds_grain]) + self.cluster_sizes = np.append(self.cluster_sizes, xyg.shape[1]) + self.cluster_sig = np.append(self.cluster_sig, sig_mean) + self.cluster_inds.append(xyg) # finish progressbar if progress_bar: update_progress(1) -# def cluster_plot_size( -# self, -# area_max = None, -# weight_intensity = False, -# pixel_area = 1.0, -# pixel_area_units = 'px^2', -# figsize = (8,6), -# returnfig = False, -# ): -# """ -# Plot the cluster sizes - -# Parameters -# -------- -# area_max: int (optional) -# Max area bin in pixels -# weight_intensity: bool -# Weight histogram by the peak intensity. -# pixel_area: float -# Size of pixel area unit square -# pixel_area_units: string -# Units of the pixel area -# figsize: tuple -# Size of the figure panel -# returnfig: bool -# Setting this to true returns the figure and axis handles - -# Returns -# -------- -# fig, ax (optional) -# Figure and axes handles - -# """ - -# if area_max is None: -# area_max = np.max(self.cluster_sizes) -# area = np.arange(area_max) -# sub = self.cluster_sizes.astype('int') < area_max -# if weight_intensity: -# hist = np.bincount( -# self.cluster_sizes[sub], -# weights = self.cluster_sig[sub], -# minlength = area_max, -# ) -# else: -# hist = np.bincount( -# self.cluster_sizes[sub], -# minlength = area_max, -# ) - - -# # plotting -# fig,ax = plt.subplots(figsize = figsize) -# ax.bar( -# area * pixel_area, -# hist, -# width = 0.8 * pixel_area, -# ) - -# ax.set_xlabel('Grain Area [' + pixel_area_units + ']') -# if weight_intensity: -# ax.set_ylabel('Total Signal [arb. units]') -# else: -# ax.set_ylabel('Number of Grains') - -# if returnfig: -# return fig,ax - - - def calculate_strain( self, @@ -2292,10 +2226,10 @@ def update_progress(progress): status = "Halt...\r\n" if progress >= 1: progress = 1 - status = "Done...\r\n" + status = "Done\r\n" block = int(round(barLength*progress)) text = "\rPercent: [{0}] {1}% {2}".format( "#"*block + "-"*(barLength-block), - np.round(progress*100,4), + np.round(progress*100,2), status) sys.stdout.write(text) sys.stdout.flush() diff --git a/py4DSTEM/process/diffraction/crystal_viz.py b/py4DSTEM/process/diffraction/crystal_viz.py index a2247b3e0..b4cc7e65e 100644 --- a/py4DSTEM/process/diffraction/crystal_viz.py +++ b/py4DSTEM/process/diffraction/crystal_viz.py @@ -1623,6 +1623,96 @@ def plot_fiber_orientation_maps( else: return images_orientation +def plot_clusters( + self, + returnfig = False, + ): + 1+1 + + +def plot_cluster_size( + self, + area_min = None, + area_max = None, + weight_intensity = False, + pixel_area = 1.0, + pixel_area_units = 'px^2', + figsize = (8,6), + returnfig = False, + ): + """ + Plot the cluster sizes + + Parameters + -------- + area_min: int (optional) + Min area to include + area_max: int (optional) + Max area bin in pixels + weight_intensity: bool + Weight histogram by the peak intensity. + pixel_area: float + Size of pixel area unit square + pixel_area_units: string + Units of the pixel area + figsize: tuple + Size of the figure panel + returnfig: bool + Setting this to true returns the figure and axis handles + + Returns + -------- + fig, ax (optional) + Figure and axes handles + + """ + + if area_max is None: + area_max = np.max(self.cluster_sizes) + area = np.arange(area_max) + if area_min is None: + sub = self.cluster_sizes.astype('int') < area_max + else: + sub = np.logical_and( + self.cluster_sizes.astype('int') < area_max, + self.cluster_sizes.astype('int') >= area_min, + ) + + + if weight_intensity: + hist = np.bincount( + self.cluster_sizes[sub], + weights = self.cluster_sig[sub], + minlength = area_max, + ) + else: + hist = np.bincount( + self.cluster_sizes[sub], + minlength = area_max, + ) + + + # plotting + fig,ax = plt.subplots(figsize = figsize) + ax.bar( + area * pixel_area, + hist, + width = 0.8 * pixel_area, + ) + + ax.set_xlabel('Grain Area [' + pixel_area_units + ']') + if weight_intensity: + ax.set_ylabel('Total Signal [arb. units]') + else: + ax.set_ylabel('Number of Grains') + + if returnfig: + return fig,ax + + + + + def axisEqual3D(ax): extents = np.array([getattr(ax, "get_{}lim".format(dim))() for dim in "xyz"]) From 7c89abbdc08fd0390bbf2024c9e4b50fc8c5258f Mon Sep 17 00:00:00 2001 From: Colin Date: Fri, 12 May 2023 18:03:11 -0700 Subject: [PATCH 03/37] More plotting tools --- py4DSTEM/process/diffraction/crystal.py | 1 + py4DSTEM/process/diffraction/crystal_ACOM.py | 98 +++++++++++++-- py4DSTEM/process/diffraction/crystal_viz.py | 120 ++++++++++++++++++- py4DSTEM/process/diffraction/utils.py | 11 ++ 4 files changed, 222 insertions(+), 8 deletions(-) diff --git a/py4DSTEM/process/diffraction/crystal.py b/py4DSTEM/process/diffraction/crystal.py index f93702c5d..709093f4d 100644 --- a/py4DSTEM/process/diffraction/crystal.py +++ b/py4DSTEM/process/diffraction/crystal.py @@ -37,6 +37,7 @@ class Crystal: match_orientations, match_single_pattern, cluster_grains, + cluster_orientation_map, calculate_strain, save_ang_file, symmetry_reduce_directions, diff --git a/py4DSTEM/process/diffraction/crystal_ACOM.py b/py4DSTEM/process/diffraction/crystal_ACOM.py index 6a8fe5280..8440b4836 100644 --- a/py4DSTEM/process/diffraction/crystal_ACOM.py +++ b/py4DSTEM/process/diffraction/crystal_ACOM.py @@ -1581,7 +1581,7 @@ def cluster_grains( progress_bar = True, ): """ - Cluster grains from a specific radial bin + Cluster grains using rotation criterion, and correlation values. Parameters -------- @@ -1616,6 +1616,7 @@ def cluster_grains( self.cluster_sizes = np.array((), dtype='int') self.cluster_sig = np.array(()) self.cluster_inds = [] + self.cluster_orientation = [] inds_all = np.zeros_like(sig, dtype='int') inds_all.ravel()[:] = np.arange(inds_all.size) @@ -1643,6 +1644,7 @@ def cluster_grains( mark[x,y,z] = False sig[x,y,z] = 0 matrix_cluster = matrix[x,y,z] + orientation_cluster = self.orientation_map.get_orientation_single(x,y,z) # Neighbors to search xr = np.clip(x + np.arange(-1,2,dtype='int'), 0, sig.shape[0] - 1) @@ -1669,11 +1671,6 @@ def cluster_grains( @ np.transpose(matrix_cluster), axis1=1, axis2=2)-1)/2,-1,1))) - # dphi = np.min(np.arccos(np.clip((np.trace( - # matrix_cluster @ \ - # np.transpose(self.symmetry_operators @ matrix[xc,yc,zc],(0,2,1)), - # axis1=1, - # axis2=2)-1)/2,-1,1))) if np.abs(dphi) < tol: keep[a0] = True @@ -1688,7 +1685,6 @@ def cluster_grains( inds_grain = np.append(inds_grain, inds_cand[keep]) inds_cand = np.unique(np.delete(inds_new, mark.ravel()[inds_new] == False)) - # print(inds_cand) if inds_cand.size == 0: grow = False @@ -1699,6 +1695,7 @@ def cluster_grains( sig_mean = np.mean(sig_init.ravel()[inds_grain]) self.cluster_sizes = np.append(self.cluster_sizes, xyg.shape[1]) self.cluster_sig = np.append(self.cluster_sig, sig_mean) + self.cluster_orientation.append(orientation_cluster) self.cluster_inds.append(xyg) # finish progressbar @@ -1706,6 +1703,93 @@ def cluster_grains( update_progress(1) + + +def cluster_orientation_map( + self, + stripe_width = 1, + area_min = 2, + ): + """ + Produce a new orientation map from the clustered grains. + Use a stripe pattern for the overlapping grains. + + Parameters + -------- + stripe_width: int + Width of strips in the overlapping regions. + + Returns + -------- + + orientation_map + + """ + + # init + orientation_map = OrientationMap( + num_x = self.orientation_map.num_x, + num_y = self.orientation_map.num_y, + num_matches=1) + im_grain = np.zeros(( + self.orientation_map.num_x, + self.orientation_map.num_y), dtype='bool') + im_count = np.zeros(( + self.orientation_map.num_x, + self.orientation_map.num_y)) + im_mark = np.zeros(( + self.orientation_map.num_x, + self.orientation_map.num_y)) + + # coordinates + xa,ya = np.meshgrid( + range(self.orientation_map.num_x), + range(self.orientation_map.num_y), + indexing = 'ij') + + # Loop over grains to determine number in each pixel + for a0 in range(self.cluster_sizes.shape[0]): + if self.cluster_sizes[a0] >= area_min: + im_grain[:] = False + im_grain[ + self.cluster_inds[a0][0,:], + self.cluster_inds[a0][1,:], + ] = True + im_count += im_grain + im_stripe = im_count >= 2 + im_single = np.logical_not(im_stripe) + + # loop over grains + for a0 in range(1): + if self.cluster_sizes[a0] >= area_min: + im_grain[:] = False + im_grain[ + self.cluster_inds[a0][0,:], + self.cluster_inds[a0][1,:], + ] = True + + sub = np.logical_and( + im_grain, + im_single) + print(np.sum(sub)) + + sub = np.logical_and( + im_grain, + im_stripe) + print(np.sum(sub)) + + + + + fig,ax = plt.subplots(figsize=(8,8)) + ax.imshow(im_stripe) + + + + + return orientation_map + + def calculate_strain( self, bragg_peaks_array: PointListArray, diff --git a/py4DSTEM/process/diffraction/crystal_viz.py b/py4DSTEM/process/diffraction/crystal_viz.py index b4cc7e65e..19bdc62ab 100644 --- a/py4DSTEM/process/diffraction/crystal_viz.py +++ b/py4DSTEM/process/diffraction/crystal_viz.py @@ -5,6 +5,8 @@ from mpl_toolkits.mplot3d import Axes3D, art3d from scipy.signal import medfilt from scipy.ndimage import gaussian_filter +from scipy.ndimage.morphology import distance_transform_edt +from skimage.morphology import dilation, erosion import warnings import numpy as np @@ -1625,9 +1627,125 @@ def plot_fiber_orientation_maps( def plot_clusters( self, + area_min = 2, + outline_grains = True, + outline_thickness = 1, + fill_grains = 0.25, + smooth_grains = 1.0, + cmap = 'viridis', + figsize = (8,8), returnfig = False, ): - 1+1 + """ + Plot the clusters as an image. + + Parameters + -------- + area_min: int (optional) + Min cluster size to include, in units of probe positions. + outline_grains: bool (optional) + Set to True to draw grains with outlines + outline_thickness: int (optional) + Thickenss of the grain outline + fill_grains: float (optional) + Outlined grains are filled with this value in pixels. + smooth_grains: float (optional) + Grain boundaries are smoothed by this value in pixels. + figsize: tuple + Size of the figure panel + returnfig: bool + Setting this to true returns the figure and axis handles + + Returns + -------- + fig, ax (optional) + Figure and axes handles + + """ + + # init + im_plot = np.zeros(( + self.orientation_map.num_x, + self.orientation_map.num_y, + )) + im_grain = np.zeros(( + self.orientation_map.num_x, + self.orientation_map.num_y, + ), dtype='bool') + + # make plotting image + + for a0 in range(self.cluster_sizes.shape[0]): + if self.cluster_sizes[a0] >= area_min: + if outline_grains: + im_grain[:] = False + im_grain[ + self.cluster_inds[a0][0,:], + self.cluster_inds[a0][1,:], + ] = True + + im_dist = \ + distance_transform_edt( + erosion(np.invert(im_grain),footprint=np.ones((3,3),dtype='bool')) + ) \ + - \ + distance_transform_edt( + im_grain + ) + im_dist = gaussian_filter( + im_dist, + sigma = smooth_grains, + mode = 'nearest') + im_add = np.exp(im_dist**2 / (-0.5*outline_thickness**2)) + + if fill_grains > 0: + im_dist = \ + distance_transform_edt( + erosion(np.invert(im_grain),footprint=np.ones((3,3),dtype='bool')) + ) + im_dist = gaussian_filter( + im_dist, + sigma = smooth_grains, + mode = 'nearest') + im_add += fill_grains*np.exp(im_dist**2 / (-0.5*outline_thickness**2)) + + + # im_add = 1 - np.exp( + # distance_transform_edt(im_grain)**2 \ + # / (-2*outline_thickness**2)) + im_plot += im_add + # im_plot = np.minimum(im_plot, im_add) + else: + # xg,yg = np.unravel_index(self.cluster_inds[a0], im_plot.shape) + im_grain[:] = False + im_grain[ + self.cluster_inds[a0][0,:], + self.cluster_inds[a0][1,:], + ] = True + im_plot += gaussian_filter( + im_grain.astype('float'), + sigma = smooth_grains, + mode = 'nearest') + + # im_plot[ + # self.cluster_inds[a0][0,:], + # self.cluster_inds[a0][1,:], + # ] += 1 + + if outline_grains: + im_plot = np.clip(im_plot,0,2) + + + # plotting + fig,ax = plt.subplots(figsize=figsize) + ax.imshow( + im_plot, + # vmin = -3, + # vmax = 3, + cmap = cmap, + ) + + def plot_cluster_size( diff --git a/py4DSTEM/process/diffraction/utils.py b/py4DSTEM/process/diffraction/utils.py index c3ed3085a..a390e99c8 100644 --- a/py4DSTEM/process/diffraction/utils.py +++ b/py4DSTEM/process/diffraction/utils.py @@ -25,6 +25,7 @@ def __post_init__(self): self.angles = np.zeros((self.num_matches, 3)) + @dataclass class OrientationMap: """ @@ -66,6 +67,16 @@ def get_orientation(self, ind_x, ind_y): orientation.angles = self.angles[ind_x, ind_y] return orientation + def get_orientation_single(self, ind_x, ind_y, ind_match): + orientation = Orientation(num_matches=1) + orientation.matrix = self.matrix[ind_x, ind_y, ind_match] + orientation.family = self.family[ind_x, ind_y, ind_match] + orientation.corr = self.corr[ind_x, ind_y, ind_match] + orientation.inds = self.inds[ind_x, ind_y, ind_match] + orientation.mirror = self.mirror[ind_x, ind_y, ind_match] + orientation.angles = self.angles[ind_x, ind_y, ind_match] + return orientation + # def __copy__(self): # return OrientationMap(self.name) # def __deepcopy__(self, memo): From e0e0a12e52fc7903b4a93811be725cfd127799c1 Mon Sep 17 00:00:00 2001 From: Colin Date: Fri, 12 May 2023 18:30:22 -0700 Subject: [PATCH 04/37] Updated viz --- py4DSTEM/process/diffraction/crystal_ACOM.py | 66 +++++++++++++++----- 1 file changed, 50 insertions(+), 16 deletions(-) diff --git a/py4DSTEM/process/diffraction/crystal_ACOM.py b/py4DSTEM/process/diffraction/crystal_ACOM.py index 8440b4836..6e11960f6 100644 --- a/py4DSTEM/process/diffraction/crystal_ACOM.py +++ b/py4DSTEM/process/diffraction/crystal_ACOM.py @@ -1707,7 +1707,7 @@ def cluster_grains( def cluster_orientation_map( self, - stripe_width = 1, + stripe_width = (2,2), area_min = 2, ): """ @@ -1741,11 +1741,11 @@ def cluster_orientation_map( self.orientation_map.num_x, self.orientation_map.num_y)) - # coordinates - xa,ya = np.meshgrid( - range(self.orientation_map.num_x), - range(self.orientation_map.num_y), - indexing = 'ij') + # # coordinates + # xa,ya = np.meshgrid( + # range(self.orientation_map.num_x), + # range(self.orientation_map.num_y), + # indexing = 'ij') # Loop over grains to determine number in each pixel for a0 in range(self.cluster_sizes.shape[0]): @@ -1759,8 +1759,19 @@ def cluster_orientation_map( im_stripe = im_count >= 2 im_single = np.logical_not(im_stripe) + # prefactor for stripes + if stripe_width[0] == 0: + dx = 0 + else: + dx = 1/stripe_width[0] + if stripe_width[1] == 0: + dy = 0 + else: + dy = 1/stripe_width[1] + + # loop over grains - for a0 in range(1): + for a0 in range(self.cluster_sizes.shape[0]): if self.cluster_sizes[a0] >= area_min: im_grain[:] = False im_grain[ @@ -1768,21 +1779,44 @@ def cluster_orientation_map( self.cluster_inds[a0][1,:], ] = True + # non-overlapping grains sub = np.logical_and( im_grain, im_single) - print(np.sum(sub)) - + x,y = np.squeeze(np.unravel_index(np.where(sub.ravel()), im_grain.shape)) + for a1 in range(x.size): + orientation_map.set_orientation( + self.cluster_orientation[a0], + x[a1], + y[a1]) + + # overlapping grains sub = np.logical_and( im_grain, im_stripe) - print(np.sum(sub)) - - - - - fig,ax = plt.subplots(figsize=(8,8)) - ax.imshow(im_stripe) + x,y = np.squeeze(np.unravel_index(np.where(sub.ravel()), im_grain.shape)) + for a1 in range(x.size): + d = np.mod( + x[a1]*dx + \ + y[a1]*dy + \ + im_mark[x[a1],y[a1]] + \ + + 0.5, + im_count[x[a1],y[a1]]) + if d < 1.0: + orientation_map.set_orientation( + self.cluster_orientation[a0], + x[a1], + y[a1]) + im_mark[x[a1],y[a1]] += 1 + + # print(np.sum(sub)) + # fig,ax = plt.subplots(figsize=(12,12)) + # ax.imshow(im_stripe) + # ax.scatter( + # y, + # x, + # s = 1, + # c = 'r') From a5cfd363811ef9acd874a25ca7052f15e0f76588 Mon Sep 17 00:00:00 2001 From: Colin Date: Fri, 12 May 2023 18:39:57 -0700 Subject: [PATCH 05/37] Cleaning up --- py4DSTEM/process/diffraction/crystal_ACOM.py | 27 +++++++++----------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/py4DSTEM/process/diffraction/crystal_ACOM.py b/py4DSTEM/process/diffraction/crystal_ACOM.py index 6e11960f6..54b233ac4 100644 --- a/py4DSTEM/process/diffraction/crystal_ACOM.py +++ b/py4DSTEM/process/diffraction/crystal_ACOM.py @@ -1716,13 +1716,17 @@ def cluster_orientation_map( Parameters -------- - stripe_width: int + stripe_width: (int,ind) Width of strips in the overlapping regions. + Kind of janky but it mostly works! + area_min: (int) + Minimum size of grains to include Returns -------- orientation_map + The clustered orientation map """ @@ -1783,7 +1787,9 @@ def cluster_orientation_map( sub = np.logical_and( im_grain, im_single) - x,y = np.squeeze(np.unravel_index(np.where(sub.ravel()), im_grain.shape)) + x,y = np.unravel_index(np.where(sub.ravel()), im_grain.shape) + x = np.atleast_1d(np.squeeze(x)) + y = np.atleast_1d(np.squeeze(y)) for a1 in range(x.size): orientation_map.set_orientation( self.cluster_orientation[a0], @@ -1794,7 +1800,9 @@ def cluster_orientation_map( sub = np.logical_and( im_grain, im_stripe) - x,y = np.squeeze(np.unravel_index(np.where(sub.ravel()), im_grain.shape)) + x,y = np.unravel_index(np.where(sub.ravel()), im_grain.shape) + x = np.atleast_1d(np.squeeze(x)) + y = np.atleast_1d(np.squeeze(y)) for a1 in range(x.size): d = np.mod( x[a1]*dx + \ @@ -1802,24 +1810,13 @@ def cluster_orientation_map( im_mark[x[a1],y[a1]] + \ + 0.5, im_count[x[a1],y[a1]]) + if d < 1.0: orientation_map.set_orientation( self.cluster_orientation[a0], x[a1], y[a1]) im_mark[x[a1],y[a1]] += 1 - - # print(np.sum(sub)) - # fig,ax = plt.subplots(figsize=(12,12)) - # ax.imshow(im_stripe) - # ax.scatter( - # y, - # x, - # s = 1, - # c = 'r') - - - return orientation_map From 2ebe41473ceb62ed5fafa229e8fb46dee8e2fac9 Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 15 May 2023 09:56:21 -0700 Subject: [PATCH 06/37] Updating plotting function with new args --- py4DSTEM/process/diffraction/crystal_viz.py | 32 +++++++++------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/py4DSTEM/process/diffraction/crystal_viz.py b/py4DSTEM/process/diffraction/crystal_viz.py index 19bdc62ab..328e3ebb8 100644 --- a/py4DSTEM/process/diffraction/crystal_viz.py +++ b/py4DSTEM/process/diffraction/crystal_viz.py @@ -1752,6 +1752,7 @@ def plot_cluster_size( self, area_min = None, area_max = None, + area_step = 1, weight_intensity = False, pixel_area = 1.0, pixel_area_units = 'px^2', @@ -1764,9 +1765,11 @@ def plot_cluster_size( Parameters -------- area_min: int (optional) - Min area to include + Min area to include in pixels^2 area_max: int (optional) - Max area bin in pixels + Max area bin in pixels^2 + area_step: int (optional) + Step size of the histogram bin in pixels^2 weight_intensity: bool Weight histogram by the peak intensity. pixel_area: float @@ -1787,37 +1790,34 @@ def plot_cluster_size( if area_max is None: area_max = np.max(self.cluster_sizes) - area = np.arange(area_max) + area = np.arange(0,area_max,area_step) if area_min is None: sub = self.cluster_sizes.astype('int') < area_max else: sub = np.logical_and( - self.cluster_sizes.astype('int') < area_max, self.cluster_sizes.astype('int') >= area_min, + self.cluster_sizes.astype('int') < area_max ) - - if weight_intensity: hist = np.bincount( - self.cluster_sizes[sub], + self.cluster_sizes[sub] // area_step, weights = self.cluster_sig[sub], - minlength = area_max, + minlength = area.shape[0], ) else: hist = np.bincount( - self.cluster_sizes[sub], - minlength = area_max, + self.cluster_sizes[sub] // area_step, + minlength = area.shape[0], ) - - + # plotting fig,ax = plt.subplots(figsize = figsize) ax.bar( area * pixel_area, hist, - width = 0.8 * pixel_area, + width = 0.8 * pixel_area * area_step, ) - + ax.set_xlim((0,area_max*pixel_area)) ax.set_xlabel('Grain Area [' + pixel_area_units + ']') if weight_intensity: ax.set_ylabel('Total Signal [arb. units]') @@ -1828,10 +1828,6 @@ def plot_cluster_size( return fig,ax - - - - def axisEqual3D(ax): extents = np.array([getattr(ax, "get_{}lim".format(dim))() for dim in "xyz"]) sz = extents[:, 1] - extents[:, 0] From 5db507f3f511111690705e117bbd089f7d59fd17 Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 15 May 2023 09:58:20 -0700 Subject: [PATCH 07/37] Fix duplicate arg --- py4DSTEM/process/diffraction/crystal_ACOM.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/py4DSTEM/process/diffraction/crystal_ACOM.py b/py4DSTEM/process/diffraction/crystal_ACOM.py index 5c0506457..29aa1251b 100644 --- a/py4DSTEM/process/diffraction/crystal_ACOM.py +++ b/py4DSTEM/process/diffraction/crystal_ACOM.py @@ -759,8 +759,7 @@ def match_orientations( multiple_corr_reset: bool = True, return_orientation: bool = True, progress_bar: bool = True, - return_orientation: bool = True, -): + ): ''' This function computes the orientation of any number of PointLists stored in a PointListArray, and returns an OrienationMap. From 6df3e4592a53f873384614b80bb4356bc9d3f862 Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Sun, 20 Aug 2023 12:11:14 -0400 Subject: [PATCH 08/37] add generic error state and avoid numerical overflow --- py4DSTEM/process/wholepatternfit/wp_models.py | 13 ++++++++++--- py4DSTEM/process/wholepatternfit/wpf.py | 4 ++-- py4DSTEM/process/wholepatternfit/wpf_viz.py | 6 ++++-- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/py4DSTEM/process/wholepatternfit/wp_models.py b/py4DSTEM/process/wholepatternfit/wp_models.py index 8a10303b9..3d53c1743 100644 --- a/py4DSTEM/process/wholepatternfit/wp_models.py +++ b/py4DSTEM/process/wholepatternfit/wp_models.py @@ -583,7 +583,9 @@ def jacobian(self, J: np.ndarray, x: np.ndarray, **static_data) -> None: mask = r_disk < (2 * disk_radius) - top_exp = mask * np.exp(4 * ((mask * r_disk) - disk_radius) / disk_width) + top_exp = mask * np.exp( + np.minimum(30, 4 * ((mask * r_disk) - disk_radius) / disk_width) + ) # dF/d(x0) dx = ( @@ -730,7 +732,9 @@ def __init__( ) ** 2 ) - tests = tests[np.abs(a_dot_b) < 0.9] # this factor of 0.9 sets the parallel cutoff + tests = tests[ + np.abs(a_dot_b) < 0.9 + ] # this factor of 0.9 sets the parallel cutoff # with the parallel vectors filtered, pick the cell with the smallest volume lat_m = tests @ lat_ab V = np.sum( @@ -1037,7 +1041,10 @@ def jacobian(self, J: np.ndarray, x: np.ndarray, **static_data): mask = r_disk < (2 * disk_radius) - top_exp = mask * np.exp(4 * ((mask * r_disk) - disk_radius) / disk_width) + # clamp the argument of the exponent at a very large finite value + top_exp = mask * np.exp( + np.minimum(30, 4 * ((mask * r_disk) - disk_radius) / disk_width) + ) # dF/d(x0) dx = ( diff --git a/py4DSTEM/process/wholepatternfit/wpf.py b/py4DSTEM/process/wholepatternfit/wpf.py index 5851e1f09..2682b5a04 100644 --- a/py4DSTEM/process/wholepatternfit/wpf.py +++ b/py4DSTEM/process/wholepatternfit/wpf.py @@ -406,7 +406,7 @@ def fit_all_patterns( except Exception as err: # print(err) fit_data_single = x0 - fit_metrics_single = [0, 0, 0, 0] + fit_metrics_single = [0, 0, 0, -2] fit_data[:, rx, ry] = fit_data_single fit_metrics[:, rx, ry] = fit_metrics_single @@ -639,7 +639,7 @@ def _fit_single_pattern( except Exception as err: # print(err) fit_coefs = initial_guess - fit_metrics_single = [0, 0, 0, 0] + fit_metrics_single = [0, 0, 0, -2] return fit_coefs, fit_metrics_single else: diff --git a/py4DSTEM/process/wholepatternfit/wpf_viz.py b/py4DSTEM/process/wholepatternfit/wpf_viz.py index 06c55edfb..436ae40a2 100644 --- a/py4DSTEM/process/wholepatternfit/wpf_viz.py +++ b/py4DSTEM/process/wholepatternfit/wpf_viz.py @@ -222,6 +222,7 @@ def show_fit_metrics(self, returnfig=False, **subplots_kwargs): opt_cmap = mpl_c.ListedColormap( ( + (0.6, 0.05, 0.05), (0.8941176470588236, 0.10196078431372549, 0.10980392156862745), (0.21568627450980393, 0.49411764705882355, 0.7215686274509804), (0.30196078431372547, 0.6862745098039216, 0.2901960784313726), @@ -231,11 +232,12 @@ def show_fit_metrics(self, returnfig=False, **subplots_kwargs): ) ) im = ax[0, 1].matshow( - self.fit_metrics["status"].data, cmap=opt_cmap, vmin=-1.5, vmax=4.5 + self.fit_metrics["status"].data, cmap=opt_cmap, vmin=-2.5, vmax=4.5 ) - cbar = fig.colorbar(im, ax=ax[0, 1], ticks=[-1, 0, 1, 2, 3, 4]) + cbar = fig.colorbar(im, ax=ax[0, 1], ticks=[-2, -1, 0, 1, 2, 3, 4]) cbar.ax.set_yticklabels( [ + "Unknown Error", "MINPACK Error", "Max f evals exceeded", "$gtol$ satisfied", From 7aaaff0f8b7d2f9614b9ddb7b8755823fbc0a438 Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Mon, 21 Aug 2023 09:33:16 -0400 Subject: [PATCH 09/37] remove old print --- py4DSTEM/process/wholepatternfit/wpf.py | 1 - 1 file changed, 1 deletion(-) diff --git a/py4DSTEM/process/wholepatternfit/wpf.py b/py4DSTEM/process/wholepatternfit/wpf.py index 2682b5a04..f6366c1fa 100644 --- a/py4DSTEM/process/wholepatternfit/wpf.py +++ b/py4DSTEM/process/wholepatternfit/wpf.py @@ -404,7 +404,6 @@ def fit_all_patterns( opt.status, ] except Exception as err: - # print(err) fit_data_single = x0 fit_metrics_single = [0, 0, 0, -2] From e6c794c337275d94e9b7f31503aa245a6ee6138c Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Mon, 21 Aug 2023 15:24:54 -0400 Subject: [PATCH 10/37] use WPF error state as mask in g vector maps --- py4DSTEM/process/wholepatternfit/wpf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py4DSTEM/process/wholepatternfit/wpf.py b/py4DSTEM/process/wholepatternfit/wpf.py index f6366c1fa..737417b78 100644 --- a/py4DSTEM/process/wholepatternfit/wpf.py +++ b/py4DSTEM/process/wholepatternfit/wpf.py @@ -466,7 +466,7 @@ def get_lattice_maps(self) -> list[RealSlice]: self.fit_data.data[lat.params["uy"].offset], self.fit_data.data[lat.params["vx"].offset], self.fit_data.data[lat.params["vy"].offset], - np.ones(self.fit_data.data.shape[1:], dtype=np.bool_), + self.fit_metrics["status"] >= 0, # negative status indicates fit error ], axis=0, ) From 005e67a738fc6b7bd2f7a61615168564fac0a5df Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Tue, 22 Aug 2023 10:14:03 -0400 Subject: [PATCH 11/37] bugfix --- py4DSTEM/process/wholepatternfit/wpf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py4DSTEM/process/wholepatternfit/wpf.py b/py4DSTEM/process/wholepatternfit/wpf.py index 737417b78..04820c6d3 100644 --- a/py4DSTEM/process/wholepatternfit/wpf.py +++ b/py4DSTEM/process/wholepatternfit/wpf.py @@ -466,7 +466,7 @@ def get_lattice_maps(self) -> list[RealSlice]: self.fit_data.data[lat.params["uy"].offset], self.fit_data.data[lat.params["vx"].offset], self.fit_data.data[lat.params["vy"].offset], - self.fit_metrics["status"] >= 0, # negative status indicates fit error + self.fit_metrics["status"].data >= 0, # negative status indicates fit error ], axis=0, ) From 5b5fee76dc16c33cd0d9c4301c6c938c41007921 Mon Sep 17 00:00:00 2001 From: Stephanie Ribet Date: Wed, 30 Aug 2023 08:24:57 -0700 Subject: [PATCH 12/37] shift center subpixel --- py4DSTEM/datacube/virtualdiffraction.py | 112 ++++++++++++++---------- py4DSTEM/datacube/virtualimage.py | 67 ++++++++++---- 2 files changed, 115 insertions(+), 64 deletions(-) diff --git a/py4DSTEM/datacube/virtualdiffraction.py b/py4DSTEM/datacube/virtualdiffraction.py index e31b984f2..65665728d 100644 --- a/py4DSTEM/datacube/virtualdiffraction.py +++ b/py4DSTEM/datacube/virtualdiffraction.py @@ -3,13 +3,12 @@ # * DataCubeVirtualDiffraction - methods inherited by DataCube for virt diffraction import numpy as np -import dask.array as da from typing import Optional import inspect from emdfile import tqdmnd, Metadata -from py4DSTEM.data import Calibration, DiffractionSlice, Data -from py4DSTEM.visualize.show import show +from py4DSTEM.data import DiffractionSlice, Data +from py4DSTEM.preprocess import get_shifted_ar # Virtual diffraction container class @@ -184,22 +183,33 @@ def get_virtual_diffraction( qx_shift = x0_mean - x0 qy_shift = y0_mean - y0 - # ...for integer shifts - if not subpixel: + if subpixel is False: # round shifts -> int qx_shift = qx_shift.round().astype(int) qy_shift = qy_shift.round().astype(int) - # ...for boolean masks and unmasked - if mask is None or mask.dtype == bool: - # get scan points - mask = np.ones(self.Rshape, dtype=bool) if mask is None else mask - mask_indices = np.nonzero(mask) - # allocate space - virtual_diffraction = np.zeros(self.Qshape) - # loop - for rx, ry in zip(mask_indices[0], mask_indices[1]): - # get shifted DP + # ...for boolean masks and unmasked + if mask is None or mask.dtype == bool: + # get scan points + mask = np.ones(self.Rshape, dtype=bool) if mask is None else mask + mask_indices = np.nonzero(mask) + # allocate space + virtual_diffraction = np.zeros(self.Qshape) + # loop + for rx, ry in zip(mask_indices[0], mask_indices[1]): + # get shifted DP + if subpixel: + DP = get_shifted_ar( + self.data[ + rx, + ry, + :, + :, + ], + qx_shift[rx, ry], + qy_shift[rx, ry], + ) + else: DP = np.roll( self.data[ rx, @@ -210,29 +220,41 @@ def get_virtual_diffraction( (qx_shift[rx, ry], qy_shift[rx, ry]), axis=(0, 1), ) - # compute - if method == "mean": - virtual_diffraction += DP - elif method == "max": - virtual_diffraction = np.maximum(virtual_diffraction, DP) - # normalize means + # compute if method == "mean": - virtual_diffraction /= len(mask_indices[0]) + virtual_diffraction += DP + elif method == "max": + virtual_diffraction = np.maximum(virtual_diffraction, DP) + # normalize means + if method == "mean": + virtual_diffraction /= len(mask_indices[0]) - # ...for floating point and complex masks + # ...for floating point and complex masks + else: + # allocate space + if mask.dtype == "complex": + virtual_diffraction = np.zeros(self.Qshape, dtype="complex") else: - # allocate space - if mask.dtype == "complex": - virtual_diffraction = np.zeros(self.Qshape, dtype="complex") + virtual_diffraction = np.zeros(self.Qshape) + # loop + for rx, ry in tqdmnd( + self.R_Nx, + self.R_Ny, + disable=not verbose, + ): + # get shifted DP + if subpixel: + DP = get_shifted_ar( + self.data[ + rx, + ry, + :, + :, + ], + qx_shift[rx, ry], + qy_shift[rx, ry], + ) else: - virtual_diffraction = np.zeros(self.Qshape) - # loop - for rx, ry in tqdmnd( - self.R_Nx, - self.R_Ny, - disable=not verbose, - ): - # get shifted DP DP = np.roll( self.data[ rx, @@ -243,21 +265,15 @@ def get_virtual_diffraction( (qx_shift[rx, ry], qy_shift[rx, ry]), axis=(0, 1), ) - # compute - w = mask[rx, ry] - if method == "mean": - virtual_diffraction += DP * w - elif method == "max": - virtual_diffraction = np.maximum( - virtual_diffraction, DP * w - ) - if method == "mean": - virtual_diffraction /= np.sum(mask) - # TODO subpixel shifting - else: - raise Exception("subpixel shifting has not been implemented yet!") - pass + # compute + w = mask[rx, ry] + if method == "mean": + virtual_diffraction += DP * w + elif method == "max": + virtual_diffraction = np.maximum(virtual_diffraction, DP * w) + if method == "mean": + virtual_diffraction /= np.sum(mask) # wrap, add to tree, and return diff --git a/py4DSTEM/datacube/virtualimage.py b/py4DSTEM/datacube/virtualimage.py index 4b2eeed39..50a297914 100644 --- a/py4DSTEM/datacube/virtualimage.py +++ b/py4DSTEM/datacube/virtualimage.py @@ -11,7 +11,8 @@ from emdfile import tqdmnd, Metadata from py4DSTEM.data import Calibration, RealSlice, Data, DiffractionSlice -from py4DSTEM.visualize.show import show +from py4DSTEM.preprocess import get_shifted_ar +from py4DSTEM.visualize import show # Virtual image container class @@ -72,6 +73,7 @@ def get_virtual_image( centered=False, calibrated=False, shift_center=False, + subpixel=False, verbose=True, dask=False, return_mask=False, @@ -138,6 +140,8 @@ def get_virtual_image( position and the mean origin position over all patterns, rounded to the nearest integer for speed. Default is False. If `shift_center` is True, `centered` is automatically set to True. + subpixel : bool + if True, applies subpixel shifts to virtual image verbose : bool toggles a progress bar dask : bool @@ -177,8 +181,7 @@ def get_virtual_image( "rectangular", "mask", ), "check doc strings for supported modes" - if shift_center == True: - centered = True + if test_config: for x, y in zip( ["centered", "calibrated", "shift_center"], @@ -242,8 +245,9 @@ def _apply_mask_dask(self, mask): self.calibration.get_origin_shift() is not None ), "origin need to be calibrated" qx_shift, qy_shift = self.calibration.get_origin_shift() - qx_shift = qx_shift.round().astype(int) - qy_shift = qy_shift.round().astype(int) + if subpixel is False: + qx_shift = qx_shift.round().astype(int) + qy_shift = qy_shift.round().astype(int) # if return_mask is True, get+return the mask and skip the computation if return_mask is not False: @@ -251,9 +255,17 @@ def _apply_mask_dask(self, mask): rx, ry = return_mask except TypeError: raise Exception( - f"if `shift_center=True`, return_mask must be a 2-tuple of ints or False, but revieced inpute value of {return_mask}" + f"if `shift_center=True`, return_mask must be a 2-tuple of \ + ints or False, but revieced inpute value of {return_mask}" + ) + if subpixel: + _mask = get_shifted_ar( + mask, qx_shift[rx, ry], qy_shift[rx, ry], bilinear=True + ) + else: + _mask = np.roll( + mask, (qx_shift[rx, ry], qy_shift[rx, ry]), axis=(0, 1) ) - _mask = np.roll(mask, (qx_shift[rx, ry], qy_shift[rx, ry]), axis=(0, 1)) return _mask # allocate space @@ -269,7 +281,14 @@ def _apply_mask_dask(self, mask): disable=not verbose, ): # get shifted mask - _mask = np.roll(mask, (qx_shift[rx, ry], qy_shift[rx, ry]), axis=(0, 1)) + if subpixel: + _mask = get_shifted_ar( + mask, qx_shift[rx, ry], qy_shift[rx, ry], bilinear=True + ) + else: + _mask = np.roll( + mask, (qx_shift[rx, ry], qy_shift[rx, ry]), axis=(0, 1) + ) # add to output array virtual_image[rx, ry] = np.sum(self.data[rx, ry] * _mask) @@ -292,6 +311,7 @@ def _apply_mask_dask(self, mask): "centered": centered, "calibrated": calibrated, "shift_center": shift_center, + "subpixel": subpixel, "verbose": verbose, "dask": dask, "return_mask": return_mask, @@ -318,6 +338,7 @@ def position_detector( centered=None, calibrated=None, shift_center=False, + subpixel=True, scan_position=None, invert=False, color="r", @@ -358,6 +379,8 @@ def position_detector( regardless of the value of `data` (enabling e.g. overlaying the mask for a specific scan position on a max or mean diffraction image.) + subpixel : bool + if True, applies subpixel shifts to virtual image invert : bool if True, invert the masked pixel (i.e. pixels *outside* the detector are overlaid with a mask) @@ -398,19 +421,24 @@ def position_detector( elif isinstance(data, np.ndarray): assert ( data.shape == self.Qshape - ), f"Can't position a detector over an image with a shape that is different from diffraction space. Diffraction space in this dataset has shape {self.Qshape} but the image passed has shape {data.shape}" + ), f"Can't position a detector over an image with a shape that is different \ + from diffraction space. Diffraction space in this dataset has shape {self.Qshape} \ + but the image passed has shape {data.shape}" image = data elif isinstance(data, DiffractionSlice): assert ( data.shape == self.Qshape - ), f"Can't position a detector over an image with a shape that is different from diffraction space. Diffraction space in this dataset has shape {self.Qshape} but the image passed has shape {data.shape}" + ), f"Can't position a detector over an image with a shape that is different \ + from diffraction space. Diffraction space in this dataset has shape {self.Qshape} \ + but the image passed has shape {data.shape}" image = data.data elif isinstance(data, tuple): rx, ry = data[:2] image = self[rx, ry] else: raise Exception( - f"Invalid argument passed to `data`. Expected None or np.ndarray or tuple, not type {type(data)}" + f"Invalid argument passed to `data`. Expected None or np.ndarray or \ + tuple, not type {type(data)}" ) # shift center @@ -419,7 +447,9 @@ def position_detector( elif shift_center == True: assert isinstance( data, tuple - ), "If shift_center is set to True, `data` should be a 2-tuple (rx,ry). To shift the detector mask while using some other input for `data`, set `shift_center` to a 2-tuple (rx,ry)" + ), "If shift_center is set to True, `data` should be a 2-tuple (rx,ry). \ + To shift the detector mask while using some other input for `data`, \ + set `shift_center` to a 2-tuple (rx,ry)" elif isinstance(shift_center, tuple): rx, ry = shift_center[:2] shift_center = True @@ -454,10 +484,15 @@ def position_detector( assert ( self.calibration.get_origin_shift() is not None ), "origin shifts need to be calibrated" - qx_shift, qy_shift = self.calibration.cal.get_origin_shift() - qx_shift = int(np.round(qx_shift[rx, ry])) - qy_shift = int(np.round(qy_shift[rx, ry])) - mask = np.roll(mask, (qx_shift, qy_shift), axis=(0, 1)) + qx_shift, qy_shift = self.calibration.get_origin_shift() + if subpixel: + mask = get_shifted_ar( + mask, qx_shift[rx, ry], qy_shift[rx, ry], bilinear=True + ) + else: + qx_shift = int(np.round(qx_shift[rx, ry])) + qy_shift = int(np.round(qy_shift[rx, ry])) + mask = np.roll(mask, (qx_shift, qy_shift), axis=(0, 1)) # Show show(image, mask=mask, mask_color=color, mask_alpha=alpha, **kwargs) From 05be663325782b0a11be968936d91e6a36661012 Mon Sep 17 00:00:00 2001 From: Stephanie Ribet Date: Wed, 30 Aug 2023 08:30:22 -0700 Subject: [PATCH 13/37] defaults for measure_origin_beamstop --- py4DSTEM/braggvectors/braggvector_methods.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/py4DSTEM/braggvectors/braggvector_methods.py b/py4DSTEM/braggvectors/braggvector_methods.py index 69445f324..267f81e5f 100644 --- a/py4DSTEM/braggvectors/braggvector_methods.py +++ b/py4DSTEM/braggvectors/braggvector_methods.py @@ -421,7 +421,7 @@ def measure_origin( return qx0, qy0, mask def measure_origin_beamstop( - self, center_guess, radii, max_dist=2, max_iter=1, **kwargs + self, center_guess, radii, max_dist=None, max_iter=1, **kwargs ): """ Find the origin from a set of braggpeaks assuming there is a beamstop, by identifying @@ -440,6 +440,9 @@ def measure_origin_beamstop( R_Nx, R_Ny = self.Rshape braggpeaks = self._v_uncal + if max_dist is None: + max_dist = radii[1] + # remove peaks outside the annulus braggpeaks_masked = braggpeaks.copy() for rx in range(R_Nx): @@ -470,7 +473,7 @@ def measure_origin_beamstop( x_r = -x + 2 * center_curr[0] y_r = -y + 2 * center_curr[1] dists = np.hypot(x_r - pl.data["qx"], y_r - pl.data["qy"]) - dists[is_paired] = 2 * max_dist + dists[is_paired] = max_dist matched = dists <= max_dist if any(matched): match = np.argmin(dists) From c71a54f1693b7d2273060e2d04551e3e3eb6a711 Mon Sep 17 00:00:00 2001 From: bsavitzky Date: Wed, 13 Sep 2023 00:57:28 -0400 Subject: [PATCH 14/37] disk detect CUDA+distributed bugfix --- py4DSTEM/braggvectors/diskdetection.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/py4DSTEM/braggvectors/diskdetection.py b/py4DSTEM/braggvectors/diskdetection.py index e726755ba..e23b10a15 100644 --- a/py4DSTEM/braggvectors/diskdetection.py +++ b/py4DSTEM/braggvectors/diskdetection.py @@ -555,6 +555,8 @@ def _find_Bragg_disks_CUDA_unbatched( # Populate a BraggVectors instance and return braggvectors = BraggVectors(datacube.Rshape, datacube.Qshape) braggvectors._v_uncal = peaks + braggvectors._set_raw_vector_getter() + braggvectors._set_cal_vector_getter() return braggvectors @@ -600,6 +602,8 @@ def _find_Bragg_disks_CUDA_batched( # Populate a BraggVectors instance and return braggvectors = BraggVectors(datacube.Rshape, datacube.Qshape) braggvectors._v_uncal = peaks + braggvectors._set_raw_vector_getter() + braggvectors._set_cal_vector_getter() return braggvectors @@ -650,6 +654,8 @@ def _find_Bragg_disks_ipp( # Populate a BraggVectors instance and return braggvectors = BraggVectors(datacube.Rshape, datacube.Qshape) braggvectors._v_uncal = peaks + braggvectors._set_raw_vector_getter() + braggvectors._set_cal_vector_getter() return braggvectors @@ -700,6 +706,8 @@ def _find_Bragg_disks_dask( # Populate a BraggVectors instance and return braggvectors = BraggVectors(datacube.Rshape, datacube.Qshape) braggvectors._v_uncal = peaks + braggvectors._set_raw_vector_getter() + braggvectors._set_cal_vector_getter() return braggvectors From f6ec8468ffdc33e3105891f17d61271d7d211cf0 Mon Sep 17 00:00:00 2001 From: bsavitzky Date: Wed, 13 Sep 2023 01:01:42 -0400 Subject: [PATCH 15/37] runs black to autoformat WPF code --- py4DSTEM/process/wholepatternfit/wpf.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/py4DSTEM/process/wholepatternfit/wpf.py b/py4DSTEM/process/wholepatternfit/wpf.py index 322ee3f94..f206004b4 100644 --- a/py4DSTEM/process/wholepatternfit/wpf.py +++ b/py4DSTEM/process/wholepatternfit/wpf.py @@ -467,7 +467,8 @@ def get_lattice_maps(self) -> list[RealSlice]: self.fit_data.data[lat.params["uy"].offset], self.fit_data.data[lat.params["vx"].offset], self.fit_data.data[lat.params["vy"].offset], - self.fit_metrics["status"].data >= 0, # negative status indicates fit error + self.fit_metrics["status"].data + >= 0, # negative status indicates fit error ], axis=0, ) From 3b6c637f43627d059a7fd653b8149d832e7b4e29 Mon Sep 17 00:00:00 2001 From: bsavitzky Date: Wed, 20 Sep 2023 17:16:26 -0400 Subject: [PATCH 16/37] debugging auto-release pipeline + GH actions --- .github/workflows/pypi_upload.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index 07a95273d..114312efb 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -26,6 +26,11 @@ jobs: with: files: | py4DSTEM/version.py + - name: Debug version file change checker + run: | + echo "Checking variable..." + echo $steps.changed-files-specific.outputs.any_changed + echo "Done" - name: Running if py4DSTEM/version.py file is not changed if: steps.changed-files-specific.outputs.any_changed == 'false' run: | From 2fe8bef3800794bb778b292afb67b2dc4eb6eb92 Mon Sep 17 00:00:00 2001 From: bsavitzky Date: Wed, 20 Sep 2023 17:50:04 -0400 Subject: [PATCH 17/37] debugging auto-release pipeline + GH actions --- .github/workflows/pypi_upload.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index 114312efb..88ecfc5ad 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -29,10 +29,11 @@ jobs: - name: Debug version file change checker run: | echo "Checking variable..." - echo $steps.changed-files-specific.outputs.any_changed + x = steps.changed-files-specific.outputs.any_changed + echo $x echo "Done" - name: Running if py4DSTEM/version.py file is not changed - if: steps.changed-files-specific.outputs.any_changed == 'false' + if: steps.changed-files-specific.outputs.any_changed != 'true' run: | echo "Version file not changed, running script to change the version file." #git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }} From cf14c79c5d13151e1174a5b1380c2141892066fe Mon Sep 17 00:00:00 2001 From: bsavitzky Date: Wed, 20 Sep 2023 18:00:13 -0400 Subject: [PATCH 18/37] debugging auto-release pipeline + GH actions --- .github/workflows/pypi_upload.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index 88ecfc5ad..9571bf7d0 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -29,11 +29,10 @@ jobs: - name: Debug version file change checker run: | echo "Checking variable..." - x = steps.changed-files-specific.outputs.any_changed - echo $x + echo ${{ steps.changed-files-specific.outputs.any_changed }} echo "Done" - name: Running if py4DSTEM/version.py file is not changed - if: steps.changed-files-specific.outputs.any_changed != 'true' + if: ${{ steps.changed-files-specific.outputs.any_changed }} == 'false' run: | echo "Version file not changed, running script to change the version file." #git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }} From 9e241dc5f287ac7cddb331a1845e6ecd1e6d740d Mon Sep 17 00:00:00 2001 From: bsavitzky Date: Wed, 20 Sep 2023 18:59:51 -0400 Subject: [PATCH 19/37] debugging auto-release pipeline + GH actions --- .github/workflows/pypi_upload.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index 9571bf7d0..8a80e743a 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -22,7 +22,7 @@ jobs: token: ${{ secrets.GH_ACTION_VERSION_UPDATE }} - name: Get changed files id: changed-files-specific - uses: tj-actions/changed-files@v32 + uses: tj-actions/changed-files@v39 with: files: | py4DSTEM/version.py From faeec96e147aea7ceee111811d7d6ed1fb9d0621 Mon Sep 17 00:00:00 2001 From: bsavitzky Date: Wed, 20 Sep 2023 19:01:09 -0400 Subject: [PATCH 20/37] debugging auto-release pipeline + GH actions --- .github/workflows/pypi_upload.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index 8a80e743a..81ec51d99 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -32,7 +32,7 @@ jobs: echo ${{ steps.changed-files-specific.outputs.any_changed }} echo "Done" - name: Running if py4DSTEM/version.py file is not changed - if: ${{ steps.changed-files-specific.outputs.any_changed }} == 'false' + if: ${{ steps.changed-files-specific.outputs.any_changed }} == false run: | echo "Version file not changed, running script to change the version file." #git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }} From f612878db2797d9c28f8b1f46fed6ed641997a56 Mon Sep 17 00:00:00 2001 From: bsavitzky Date: Wed, 20 Sep 2023 19:06:14 -0400 Subject: [PATCH 21/37] debugging auto-release pipeline + GH actions --- .github/workflows/pypi_upload.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index 81ec51d99..d32a0865d 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -32,7 +32,7 @@ jobs: echo ${{ steps.changed-files-specific.outputs.any_changed }} echo "Done" - name: Running if py4DSTEM/version.py file is not changed - if: ${{ steps.changed-files-specific.outputs.any_changed }} == false + if: steps.changed-files-specific.outputs.any_changed == 'false' run: | echo "Version file not changed, running script to change the version file." #git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }} From a40d78d28bccf3f936082f33fb98837fdbea14d8 Mon Sep 17 00:00:00 2001 From: bsavitzky Date: Wed, 20 Sep 2023 19:09:38 -0400 Subject: [PATCH 22/37] debugging auto-release pipeline + GH actions --- .github/scripts/update_version.py | 1 - .github/workflows/pypi_upload.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/scripts/update_version.py b/.github/scripts/update_version.py index a9250757e..2aaaa07af 100644 --- a/.github/scripts/update_version.py +++ b/.github/scripts/update_version.py @@ -1,6 +1,5 @@ """ Script to update the patch version number of the py4DSTEM package. -Author: Tara Mishra (Quantumstud) """ version_file_path = "py4DSTEM/version.py" diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index d32a0865d..264c69030 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -1,6 +1,5 @@ # Action to check the version of the package and upload it to PyPI # if the version is higher than the one on PyPI -# Author: @quantumstud name: PyPI Upload on: From e40ce9fc4c4cebfb9b944ef3ec6f907645cedf0a Mon Sep 17 00:00:00 2001 From: bsavitzky Date: Wed, 20 Sep 2023 19:10:15 -0400 Subject: [PATCH 23/37] debugging auto-release pipeline + GH actions --- py4DSTEM/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py4DSTEM/version.py b/py4DSTEM/version.py index 23f00709c..224f1fb74 100644 --- a/py4DSTEM/version.py +++ b/py4DSTEM/version.py @@ -1 +1 @@ -__version__ = "0.14.3" +__version__ = "0.14.4" From db767c2a4a06a5291b807f2d768dec705ac61e8a Mon Sep 17 00:00:00 2001 From: bsavitzky Date: Wed, 20 Sep 2023 20:23:20 -0400 Subject: [PATCH 24/37] release debugging --- .github/workflows/check_install_dev.yml | 4 ++++ .github/workflows/check_install_main.yml | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/check_install_dev.yml b/.github/workflows/check_install_dev.yml index 6987c9274..6e44a6334 100644 --- a/.github/workflows/check_install_dev.yml +++ b/.github/workflows/check_install_dev.yml @@ -17,6 +17,10 @@ jobs: runs-on: [ubuntu-latest] architecture: [x86_64] python-version: ["3.9", "3.10", "3.11",] + include: + - python-version: "3.12.0-beta.4" + runs-on: ubuntu-latest + allow_failure: true # Currently no public runners available for this but this or arm64 should work next time # include: # - python-version: "3.10" diff --git a/.github/workflows/check_install_main.yml b/.github/workflows/check_install_main.yml index 63db352e4..2d1c8ed2a 100644 --- a/.github/workflows/check_install_main.yml +++ b/.github/workflows/check_install_main.yml @@ -17,10 +17,10 @@ jobs: runs-on: [ubuntu-latest, windows-latest, macos-latest] architecture: [x86_64] python-version: ["3.9", "3.10", "3.11",] - include: - - python-version: "3.12.0-beta.4" - runs-on: ubuntu-latest - allow_failure: true + #include: + # - python-version: "3.12.0-beta.4" + # runs-on: ubuntu-latest + # allow_failure: true # Currently no public runners available for this but this or arm64 should work next time # include: # - python-version: "3.10" From d621c19afc2fe664d83c0d1838782c3a901ff321 Mon Sep 17 00:00:00 2001 From: cophus Date: Sun, 24 Sep 2023 13:01:11 -0700 Subject: [PATCH 25/37] Adding min angle between matches. --- py4DSTEM/process/diffraction/crystal_ACOM.py | 103 +++++++++++++++---- 1 file changed, 83 insertions(+), 20 deletions(-) diff --git a/py4DSTEM/process/diffraction/crystal_ACOM.py b/py4DSTEM/process/diffraction/crystal_ACOM.py index f8ff10bf5..aa1fdf093 100644 --- a/py4DSTEM/process/diffraction/crystal_ACOM.py +++ b/py4DSTEM/process/diffraction/crystal_ACOM.py @@ -763,14 +763,16 @@ def match_orientations( self, bragg_peaks_array: PointListArray, num_matches_return: int = 1, + min_angle_between_matches_deg = None, min_number_peaks: int = 3, inversion_symmetry: bool = True, multiple_corr_reset: bool = True, return_orientation: bool = True, progress_bar: bool = True, ): - ''' - This function computes the orientation of any number of PointLists stored in a PointListArray, and returns an OrienationMap. + """ + This function computes the orientation of any number of PointLists stored in a PointListArray, + and returns an OrienationMap. Options are the same as match_single_pattern(). """ orientation_map = OrientationMap( @@ -809,6 +811,7 @@ def match_orientations( orientation = self.match_single_pattern( bragg_peaks=vectors, num_matches_return=num_matches_return, + min_angle_between_matches_deg = min_angle_between_matches_deg, min_number_peaks=min_number_peaks, inversion_symmetry=inversion_symmetry, multiple_corr_reset=multiple_corr_reset, @@ -836,6 +839,7 @@ def match_single_pattern( self, bragg_peaks: PointList, num_matches_return: int = 1, + min_angle_between_matches_deg = None, min_number_peaks=3, inversion_symmetry=True, multiple_corr_reset=True, @@ -849,23 +853,42 @@ def match_single_pattern( """ Solve for the best fit orientation of a single diffraction pattern. - Args: - bragg_peaks (PointList): numpy array containing the Bragg positions and intensities ('qx', 'qy', 'intensity') - num_matches_return (int): return these many matches as 3th dim of orient (matrix) - min_number_peaks (int): Minimum number of peaks required to perform ACOM matching - inversion_symmetry (bool): check for inversion symmetry in the matches - multiple_corr_reset (bool): keep original correlation score for multiple matches - subpixel_tilt (bool): set to false for faster matching, returning the nearest corr point - plot_polar (bool): set to true to plot the polar transform of the diffraction pattern - plot_corr (bool): set to true to plot the resulting correlogram - returnfig (bool): Return figure handles - figsize (list): size of figure - verbose (bool): Print the fitted zone axes, correlation scores - CUDA (bool): Enable CUDA for the FFT steps + Parameters + -------- + bragg_peaks: PointList + numpy array containing the Bragg positions and intensities ('qx', 'qy', 'intensity') + num_matches_return: int + return these many matches as 3th dim of orient (matrix) + min_angle_between_matches_deg: int + Minimum angle between zone axis of multiple matches, in degrees. + Note that I haven't thought how to handle in-plane rotations, since multiple matches are possible. + min_number_peaks: int + Minimum number of peaks required to perform ACOM matching + inversion_symmetry bool + check for inversion symmetry in the matches + multiple_corr_reset bool + keep original correlation score for multiple matches + subpixel_tilt: bool + set to false for faster matching, returning the nearest corr point + plot_polar: bool + set to true to plot the polar transform of the diffraction pattern + plot_corr: bool + set to true to plot the resulting correlogram + returnfig: bool + return figure handles + figsize: list + size of figure + verbose: bool + Print the fitted zone axes, correlation scores + CUDA: bool + Enable CUDA for the FFT steps - Returns: - orientation (Orientation): Orientation class containing all outputs - fig, ax (handles): Figure handles for the plotting output + Returns + -------- + orientation: Orientation + Orientation class containing all outputs + fig, ax: handles + Figure handles for the plotting output """ # init orientation output @@ -1036,6 +1059,24 @@ def match_single_pattern( 0, ) + # If minimum angle is specified and we're on a match later than the first, + # we zero correlation values within the given range. + if min_angle_between_matches_deg is not None: + if match_ind > 0: + inds_previous = orientation.inds[:match_ind, 0] + for a0 in range(inds_previous.size): + mask_zero = np.arccos( + np.clip( + np.sum( + self.orientation_vecs * self.orientation_vecs[inds_previous[a0], :], + axis=1, + ), + -1, + 1, + ) + ) < np.deg2rad(min_angle_between_matches_deg) + corr_full[mask_zero,:] = 0.0 + # Get maximum (non inverted) correlation value ind_phi = np.argmax(corr_full, axis=1) @@ -1103,6 +1144,25 @@ def match_single_pattern( ), 0, ) + + # If minimum angle is specified and we're on a match later than the first, + # we zero correlation values within the given range. + if min_angle_between_matches_deg is not None: + if match_ind > 0: + inds_previous = orientation.inds[:match_ind, 0] + for a0 in range(inds_previous.size): + mask_zero = np.arccos( + np.clip( + np.sum( + self.orientation_vecs * self.orientation_vecs[inds_previous[a0], :], + axis=1, + ), + -1, + 1, + ) + ) < np.deg2rad(min_angle_between_matches_deg) + corr_full_inv[mask_zero,:] = 0.0 + ind_phi_inv = np.argmax(corr_full_inv, axis=1) corr_inv = np.zeros(self.orientation_num_zones, dtype="bool") @@ -1746,6 +1806,7 @@ def cluster_grains( # Main loop search = True + comp = 0.0 while search is True: inds_grain = np.argmax(sig) @@ -1757,8 +1818,10 @@ def cluster_grains( else: # progressbar if progress_bar: - comp = 1 - np.mean(np.max(mark,axis = 2)) - update_progress(comp) + new_comp = 1 - np.mean(np.max(mark,axis = 2)) + if new_comp > comp + 0.001: + comp = new_comp + update_progress(comp) # Start cluster x,y,z = np.unravel_index(inds_grain, sig.shape) From 65206a1f284769adf395d2080cb442d9a4c3c5e1 Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Wed, 27 Sep 2023 11:46:26 -0400 Subject: [PATCH 26/37] format with black --- py4DSTEM/process/diffraction/crystal_ACOM.py | 232 ++++++++++--------- py4DSTEM/process/diffraction/crystal_viz.py | 167 +++++++------ py4DSTEM/process/diffraction/utils.py | 1 - 3 files changed, 200 insertions(+), 200 deletions(-) diff --git a/py4DSTEM/process/diffraction/crystal_ACOM.py b/py4DSTEM/process/diffraction/crystal_ACOM.py index aa1fdf093..e7b54f712 100644 --- a/py4DSTEM/process/diffraction/crystal_ACOM.py +++ b/py4DSTEM/process/diffraction/crystal_ACOM.py @@ -763,15 +763,15 @@ def match_orientations( self, bragg_peaks_array: PointListArray, num_matches_return: int = 1, - min_angle_between_matches_deg = None, + min_angle_between_matches_deg=None, min_number_peaks: int = 3, inversion_symmetry: bool = True, multiple_corr_reset: bool = True, return_orientation: bool = True, progress_bar: bool = True, - ): +): """ - This function computes the orientation of any number of PointLists stored in a PointListArray, + This function computes the orientation of any number of PointLists stored in a PointListArray, and returns an OrienationMap. Options are the same as match_single_pattern(). """ @@ -811,7 +811,7 @@ def match_orientations( orientation = self.match_single_pattern( bragg_peaks=vectors, num_matches_return=num_matches_return, - min_angle_between_matches_deg = min_angle_between_matches_deg, + min_angle_between_matches_deg=min_angle_between_matches_deg, min_number_peaks=min_number_peaks, inversion_symmetry=inversion_symmetry, multiple_corr_reset=multiple_corr_reset, @@ -835,11 +835,12 @@ def match_orientations( else: return + def match_single_pattern( self, bragg_peaks: PointList, num_matches_return: int = 1, - min_angle_between_matches_deg = None, + min_angle_between_matches_deg=None, min_number_peaks=3, inversion_symmetry=True, multiple_corr_reset=True, @@ -855,39 +856,39 @@ def match_single_pattern( Parameters -------- - bragg_peaks: PointList + bragg_peaks: PointList numpy array containing the Bragg positions and intensities ('qx', 'qy', 'intensity') - num_matches_return: int + num_matches_return: int return these many matches as 3th dim of orient (matrix) min_angle_between_matches_deg: int Minimum angle between zone axis of multiple matches, in degrees. Note that I haven't thought how to handle in-plane rotations, since multiple matches are possible. - min_number_peaks: int + min_number_peaks: int Minimum number of peaks required to perform ACOM matching - inversion_symmetry bool + inversion_symmetry bool check for inversion symmetry in the matches - multiple_corr_reset bool + multiple_corr_reset bool keep original correlation score for multiple matches - subpixel_tilt: bool + subpixel_tilt: bool set to false for faster matching, returning the nearest corr point - plot_polar: bool + plot_polar: bool set to true to plot the polar transform of the diffraction pattern - plot_corr: bool + plot_corr: bool set to true to plot the resulting correlogram - returnfig: bool + returnfig: bool return figure handles - figsize: list + figsize: list size of figure - verbose: bool + verbose: bool Print the fitted zone axes, correlation scores - CUDA: bool + CUDA: bool Enable CUDA for the FFT steps Returns -------- - orientation: Orientation + orientation: Orientation Orientation class containing all outputs - fig, ax: handles + fig, ax: handles Figure handles for the plotting output """ @@ -1068,14 +1069,15 @@ def match_single_pattern( mask_zero = np.arccos( np.clip( np.sum( - self.orientation_vecs * self.orientation_vecs[inds_previous[a0], :], + self.orientation_vecs + * self.orientation_vecs[inds_previous[a0], :], axis=1, ), -1, 1, ) ) < np.deg2rad(min_angle_between_matches_deg) - corr_full[mask_zero,:] = 0.0 + corr_full[mask_zero, :] = 0.0 # Get maximum (non inverted) correlation value ind_phi = np.argmax(corr_full, axis=1) @@ -1146,7 +1148,7 @@ def match_single_pattern( ) # If minimum angle is specified and we're on a match later than the first, - # we zero correlation values within the given range. + # we zero correlation values within the given range. if min_angle_between_matches_deg is not None: if match_ind > 0: inds_previous = orientation.inds[:match_ind, 0] @@ -1154,14 +1156,15 @@ def match_single_pattern( mask_zero = np.arccos( np.clip( np.sum( - self.orientation_vecs * self.orientation_vecs[inds_previous[a0], :], + self.orientation_vecs + * self.orientation_vecs[inds_previous[a0], :], axis=1, ), -1, 1, ) ) < np.deg2rad(min_angle_between_matches_deg) - corr_full_inv[mask_zero,:] = 0.0 + corr_full_inv[mask_zero, :] = 0.0 ind_phi_inv = np.argmax(corr_full_inv, axis=1) corr_inv = np.zeros(self.orientation_num_zones, dtype="bool") @@ -1750,17 +1753,13 @@ def match_single_pattern( return orientation - - - - def cluster_grains( self, - threshold_add = 1.0, - threshold_grow = 0.1, - angle_tolerance_deg = 5.0, - progress_bar = True, - ): + threshold_add=1.0, + threshold_grow=0.1, + angle_tolerance_deg=5.0, + progress_bar=True, +): """ Cluster grains using rotation criterion, and correlation values. @@ -1777,7 +1776,7 @@ def cluster_grains( Returns -------- - + """ @@ -1794,11 +1793,11 @@ def cluster_grains( matrix = self.orientation_map.matrix.copy() # init - self.cluster_sizes = np.array((), dtype='int') + self.cluster_sizes = np.array((), dtype="int") self.cluster_sig = np.array(()) self.cluster_inds = [] self.cluster_orientation = [] - inds_all = np.zeros_like(sig, dtype='int') + inds_all = np.zeros_like(sig, dtype="int") inds_all.ravel()[:] = np.arange(inds_all.size) # Tolerance @@ -1814,26 +1813,26 @@ def cluster_grains( if val < threshold_add: search = False - + else: # progressbar if progress_bar: - new_comp = 1 - np.mean(np.max(mark,axis = 2)) + new_comp = 1 - np.mean(np.max(mark, axis=2)) if new_comp > comp + 0.001: comp = new_comp update_progress(comp) # Start cluster - x,y,z = np.unravel_index(inds_grain, sig.shape) - mark[x,y,z] = False - sig[x,y,z] = 0 - matrix_cluster = matrix[x,y,z] - orientation_cluster = self.orientation_map.get_orientation_single(x,y,z) + x, y, z = np.unravel_index(inds_grain, sig.shape) + mark[x, y, z] = False + sig[x, y, z] = 0 + matrix_cluster = matrix[x, y, z] + orientation_cluster = self.orientation_map.get_orientation_single(x, y, z) # Neighbors to search - xr = np.clip(x + np.arange(-1,2,dtype='int'), 0, sig.shape[0] - 1) - yr = np.clip(y + np.arange(-1,2,dtype='int'), 0, sig.shape[1] - 1) - inds_cand = inds_all[xr[:,None],yr[None],:].ravel() + xr = np.clip(x + np.arange(-1, 2, dtype="int"), 0, sig.shape[0] - 1) + yr = np.clip(y + np.arange(-1, 2, dtype="int"), 0, sig.shape[1] - 1) + inds_cand = inds_all[xr[:, None], yr[None], :].ravel() inds_cand = np.delete(inds_cand, mark.ravel()[inds_cand] == False) if inds_cand.size == 0: @@ -1843,39 +1842,59 @@ def cluster_grains( # grow the cluster while grow is True: - inds_new = np.array((),dtype='int') + inds_new = np.array((), dtype="int") - keep = np.zeros(inds_cand.size, dtype='bool') + keep = np.zeros(inds_cand.size, dtype="bool") for a0 in range(inds_cand.size): - xc,yc,zc = np.unravel_index(inds_cand[a0], sig.shape) + xc, yc, zc = np.unravel_index(inds_cand[a0], sig.shape) # Angle test between orientation matrices - dphi = np.min(np.arccos(np.clip((np.trace( - self.symmetry_operators @ matrix[xc,yc,zc] \ - @ np.transpose(matrix_cluster), - axis1=1, - axis2=2)-1)/2,-1,1))) + dphi = np.min( + np.arccos( + np.clip( + ( + np.trace( + self.symmetry_operators + @ matrix[xc, yc, zc] + @ np.transpose(matrix_cluster), + axis1=1, + axis2=2, + ) + - 1 + ) + / 2, + -1, + 1, + ) + ) + ) if np.abs(dphi) < tol: keep[a0] = True - sig[xc,yc,zc] = 0 - mark[xc,yc,zc] = False + sig[xc, yc, zc] = 0 + mark[xc, yc, zc] = False - xr = np.clip(xc + np.arange(-1,2,dtype='int'), 0, sig.shape[0] - 1) - yr = np.clip(yc + np.arange(-1,2,dtype='int'), 0, sig.shape[1] - 1) - inds_add = inds_all[xr[:,None],yr[None],:].ravel() + xr = np.clip( + xc + np.arange(-1, 2, dtype="int"), 0, sig.shape[0] - 1 + ) + yr = np.clip( + yc + np.arange(-1, 2, dtype="int"), 0, sig.shape[1] - 1 + ) + inds_add = inds_all[xr[:, None], yr[None], :].ravel() inds_new = np.append(inds_new, inds_add) inds_grain = np.append(inds_grain, inds_cand[keep]) - inds_cand = np.unique(np.delete(inds_new, mark.ravel()[inds_new] == False)) + inds_cand = np.unique( + np.delete(inds_new, mark.ravel()[inds_new] == False) + ) if inds_cand.size == 0: grow = False # convert grain to x,y coordinates, add = list - xg,yg,zg = np.unravel_index(inds_grain, sig.shape) - xyg = np.unique(np.vstack((xg,yg)), axis = 1) + xg, yg, zg = np.unravel_index(inds_grain, sig.shape) + xyg = np.unique(np.vstack((xg, yg)), axis=1) sig_mean = np.mean(sig_init.ravel()[inds_grain]) self.cluster_sizes = np.append(self.cluster_sizes, xyg.shape[1]) self.cluster_sig = np.append(self.cluster_sig, sig_mean) @@ -1887,13 +1906,11 @@ def cluster_grains( update_progress(1) - - def cluster_orientation_map( self, - stripe_width = (2,2), - area_min = 2, - ): + stripe_width=(2, 2), + area_min=2, +): """ Produce a new orientation map from the clustered grains. Use a stripe pattern for the overlapping grains. @@ -1908,7 +1925,7 @@ def cluster_orientation_map( Returns -------- - + orientation_map The clustered orientation map @@ -1916,18 +1933,15 @@ def cluster_orientation_map( # init orientation_map = OrientationMap( - num_x = self.orientation_map.num_x, - num_y = self.orientation_map.num_y, - num_matches=1) - im_grain = np.zeros(( - self.orientation_map.num_x, - self.orientation_map.num_y), dtype='bool') - im_count = np.zeros(( - self.orientation_map.num_x, - self.orientation_map.num_y)) - im_mark = np.zeros(( - self.orientation_map.num_x, - self.orientation_map.num_y)) + num_x=self.orientation_map.num_x, + num_y=self.orientation_map.num_y, + num_matches=1, + ) + im_grain = np.zeros( + (self.orientation_map.num_x, self.orientation_map.num_y), dtype="bool" + ) + im_count = np.zeros((self.orientation_map.num_x, self.orientation_map.num_y)) + im_mark = np.zeros((self.orientation_map.num_x, self.orientation_map.num_y)) # # coordinates # xa,ya = np.meshgrid( @@ -1940,8 +1954,8 @@ def cluster_orientation_map( if self.cluster_sizes[a0] >= area_min: im_grain[:] = False im_grain[ - self.cluster_inds[a0][0,:], - self.cluster_inds[a0][1,:], + self.cluster_inds[a0][0, :], + self.cluster_inds[a0][1, :], ] = True im_count += im_grain im_stripe = im_count >= 2 @@ -1951,56 +1965,47 @@ def cluster_orientation_map( if stripe_width[0] == 0: dx = 0 else: - dx = 1/stripe_width[0] + dx = 1 / stripe_width[0] if stripe_width[1] == 0: dy = 0 else: - dy = 1/stripe_width[1] - + dy = 1 / stripe_width[1] # loop over grains for a0 in range(self.cluster_sizes.shape[0]): if self.cluster_sizes[a0] >= area_min: im_grain[:] = False im_grain[ - self.cluster_inds[a0][0,:], - self.cluster_inds[a0][1,:], + self.cluster_inds[a0][0, :], + self.cluster_inds[a0][1, :], ] = True # non-overlapping grains - sub = np.logical_and( - im_grain, - im_single) - x,y = np.unravel_index(np.where(sub.ravel()), im_grain.shape) + sub = np.logical_and(im_grain, im_single) + x, y = np.unravel_index(np.where(sub.ravel()), im_grain.shape) x = np.atleast_1d(np.squeeze(x)) y = np.atleast_1d(np.squeeze(y)) for a1 in range(x.size): orientation_map.set_orientation( - self.cluster_orientation[a0], - x[a1], - y[a1]) - + self.cluster_orientation[a0], x[a1], y[a1] + ) + # overlapping grains - sub = np.logical_and( - im_grain, - im_stripe) - x,y = np.unravel_index(np.where(sub.ravel()), im_grain.shape) + sub = np.logical_and(im_grain, im_stripe) + x, y = np.unravel_index(np.where(sub.ravel()), im_grain.shape) x = np.atleast_1d(np.squeeze(x)) y = np.atleast_1d(np.squeeze(y)) for a1 in range(x.size): d = np.mod( - x[a1]*dx + \ - y[a1]*dy + \ - im_mark[x[a1],y[a1]] + \ - + 0.5, - im_count[x[a1],y[a1]]) + x[a1] * dx + y[a1] * dy + im_mark[x[a1], y[a1]] + +0.5, + im_count[x[a1], y[a1]], + ) if d < 1.0: orientation_map.set_orientation( - self.cluster_orientation[a0], - x[a1], - y[a1]) - im_mark[x[a1],y[a1]] += 1 + self.cluster_orientation[a0], x[a1], y[a1] + ) + im_mark[x[a1], y[a1]] += 1 return orientation_map @@ -2545,11 +2550,10 @@ def symmetry_reduce_directions( # "-3m": ["fiber", [0, 0, 1], [180.0, 30.0]], - # Progressbar taken from stackexchange: # https://stackoverflow.com/questions/3160699/python-progress-bar def update_progress(progress): - barLength = 60 # Modify this to change the length of the progress bar + barLength = 60 # Modify this to change the length of the progress bar status = "" if isinstance(progress, int): progress = float(progress) @@ -2562,9 +2566,9 @@ def update_progress(progress): if progress >= 1: progress = 1 status = "Done\r\n" - block = int(round(barLength*progress)) - text = "\rPercent: [{0}] {1}% {2}".format( "#"*block + "-"*(barLength-block), - np.round(progress*100,2), - status) + block = int(round(barLength * progress)) + text = "\rPercent: [{0}] {1}% {2}".format( + "#" * block + "-" * (barLength - block), np.round(progress * 100, 2), status + ) sys.stdout.write(text) sys.stdout.flush() diff --git a/py4DSTEM/process/diffraction/crystal_viz.py b/py4DSTEM/process/diffraction/crystal_viz.py index 1c68d5ba6..e17e87b93 100644 --- a/py4DSTEM/process/diffraction/crystal_viz.py +++ b/py4DSTEM/process/diffraction/crystal_viz.py @@ -991,7 +991,7 @@ def overline(x): def plot_orientation_maps( self, - orientation_map = None, + orientation_map=None, orientation_ind: int = 0, dir_in_plane_degrees: float = 0.0, corr_range: np.ndarray = np.array([0, 5]), @@ -1725,17 +1725,18 @@ def plot_fiber_orientation_maps( else: return images_orientation + def plot_clusters( self, - area_min = 2, - outline_grains = True, - outline_thickness = 1, - fill_grains = 0.25, - smooth_grains = 1.0, - cmap = 'viridis', - figsize = (8,8), - returnfig = False, - ): + area_min=2, + outline_grains=True, + outline_thickness=1, + fill_grains=0.25, + smooth_grains=1.0, + cmap="viridis", + figsize=(8, 8), + returnfig=False, +): """ Plot the clusters as an image. @@ -1761,17 +1762,22 @@ def plot_clusters( fig, ax (optional) Figure and axes handles - """ - + """ + # init - im_plot = np.zeros(( - self.orientation_map.num_x, - self.orientation_map.num_y, - )) - im_grain = np.zeros(( - self.orientation_map.num_x, - self.orientation_map.num_y, - ), dtype='bool') + im_plot = np.zeros( + ( + self.orientation_map.num_x, + self.orientation_map.num_y, + ) + ) + im_grain = np.zeros( + ( + self.orientation_map.num_x, + self.orientation_map.num_y, + ), + dtype="bool", + ) # make plotting image @@ -1780,35 +1786,30 @@ def plot_clusters( if outline_grains: im_grain[:] = False im_grain[ - self.cluster_inds[a0][0,:], - self.cluster_inds[a0][1,:], + self.cluster_inds[a0][0, :], + self.cluster_inds[a0][1, :], ] = True - im_dist = \ - distance_transform_edt( - erosion(np.invert(im_grain),footprint=np.ones((3,3),dtype='bool')) - ) \ - - \ - distance_transform_edt( - im_grain - ) - im_dist = gaussian_filter( - im_dist, - sigma = smooth_grains, - mode = 'nearest') - im_add = np.exp(im_dist**2 / (-0.5*outline_thickness**2)) - + im_dist = distance_transform_edt( + erosion( + np.invert(im_grain), footprint=np.ones((3, 3), dtype="bool") + ) + ) - distance_transform_edt(im_grain) + im_dist = gaussian_filter(im_dist, sigma=smooth_grains, mode="nearest") + im_add = np.exp(im_dist**2 / (-0.5 * outline_thickness**2)) + if fill_grains > 0: - im_dist = \ - distance_transform_edt( - erosion(np.invert(im_grain),footprint=np.ones((3,3),dtype='bool')) + im_dist = distance_transform_edt( + erosion( + np.invert(im_grain), footprint=np.ones((3, 3), dtype="bool") + ) ) im_dist = gaussian_filter( - im_dist, - sigma = smooth_grains, - mode = 'nearest') - im_add += fill_grains*np.exp(im_dist**2 / (-0.5*outline_thickness**2)) - + im_dist, sigma=smooth_grains, mode="nearest" + ) + im_add += fill_grains * np.exp( + im_dist**2 / (-0.5 * outline_thickness**2) + ) # im_add = 1 - np.exp( # distance_transform_edt(im_grain)**2 \ @@ -1819,13 +1820,12 @@ def plot_clusters( # xg,yg = np.unravel_index(self.cluster_inds[a0], im_plot.shape) im_grain[:] = False im_grain[ - self.cluster_inds[a0][0,:], - self.cluster_inds[a0][1,:], + self.cluster_inds[a0][0, :], + self.cluster_inds[a0][1, :], ] = True im_plot += gaussian_filter( - im_grain.astype('float'), - sigma = smooth_grains, - mode = 'nearest') + im_grain.astype("float"), sigma=smooth_grains, mode="nearest" + ) # im_plot[ # self.cluster_inds[a0][0,:], @@ -1833,32 +1833,29 @@ def plot_clusters( # ] += 1 if outline_grains: - im_plot = np.clip(im_plot,0,2) - + im_plot = np.clip(im_plot, 0, 2) # plotting - fig,ax = plt.subplots(figsize=figsize) + fig, ax = plt.subplots(figsize=figsize) ax.imshow( im_plot, # vmin = -3, # vmax = 3, - cmap = cmap, - ) - - + cmap=cmap, + ) def plot_cluster_size( self, - area_min = None, - area_max = None, - area_step = 1, - weight_intensity = False, - pixel_area = 1.0, - pixel_area_units = 'px^2', - figsize = (8,6), - returnfig = False, - ): + area_min=None, + area_max=None, + area_step=1, + weight_intensity=False, + pixel_area=1.0, + pixel_area_units="px^2", + figsize=(8, 6), + returnfig=False, +): """ Plot the cluster sizes @@ -1875,7 +1872,7 @@ def plot_cluster_size( pixel_area: float Size of pixel area unit square pixel_area_units: string - Units of the pixel area + Units of the pixel area figsize: tuple Size of the figure panel returnfig: bool @@ -1890,42 +1887,42 @@ def plot_cluster_size( if area_max is None: area_max = np.max(self.cluster_sizes) - area = np.arange(0,area_max,area_step) + area = np.arange(0, area_max, area_step) if area_min is None: - sub = self.cluster_sizes.astype('int') < area_max + sub = self.cluster_sizes.astype("int") < area_max else: sub = np.logical_and( - self.cluster_sizes.astype('int') >= area_min, - self.cluster_sizes.astype('int') < area_max - ) + self.cluster_sizes.astype("int") >= area_min, + self.cluster_sizes.astype("int") < area_max, + ) if weight_intensity: hist = np.bincount( self.cluster_sizes[sub] // area_step, - weights = self.cluster_sig[sub], - minlength = area.shape[0], - ) + weights=self.cluster_sig[sub], + minlength=area.shape[0], + ) else: hist = np.bincount( self.cluster_sizes[sub] // area_step, - minlength = area.shape[0], - ) - + minlength=area.shape[0], + ) + # plotting - fig,ax = plt.subplots(figsize = figsize) + fig, ax = plt.subplots(figsize=figsize) ax.bar( area * pixel_area, hist, - width = 0.8 * pixel_area * area_step, - ) - ax.set_xlim((0,area_max*pixel_area)) - ax.set_xlabel('Grain Area [' + pixel_area_units + ']') + width=0.8 * pixel_area * area_step, + ) + ax.set_xlim((0, area_max * pixel_area)) + ax.set_xlabel("Grain Area [" + pixel_area_units + "]") if weight_intensity: - ax.set_ylabel('Total Signal [arb. units]') + ax.set_ylabel("Total Signal [arb. units]") else: - ax.set_ylabel('Number of Grains') + ax.set_ylabel("Number of Grains") if returnfig: - return fig,ax + return fig, ax def axisEqual3D(ax): diff --git a/py4DSTEM/process/diffraction/utils.py b/py4DSTEM/process/diffraction/utils.py index aaf4c023c..cfb11f044 100644 --- a/py4DSTEM/process/diffraction/utils.py +++ b/py4DSTEM/process/diffraction/utils.py @@ -26,7 +26,6 @@ def __post_init__(self): self.angles = np.zeros((self.num_matches, 3)) - @dataclass class OrientationMap: """ From f1573a8c3fd8f62f88c906455409ad15cd9795f0 Mon Sep 17 00:00:00 2001 From: Steve Zeltmann <37132012+sezelt@users.noreply.github.com> Date: Wed, 27 Sep 2023 13:51:49 -0400 Subject: [PATCH 27/37] Update check_install_dev.yml Remore 3.12 test --- .github/workflows/check_install_dev.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/check_install_dev.yml b/.github/workflows/check_install_dev.yml index 6e44a6334..82701d50d 100644 --- a/.github/workflows/check_install_dev.yml +++ b/.github/workflows/check_install_dev.yml @@ -17,10 +17,10 @@ jobs: runs-on: [ubuntu-latest] architecture: [x86_64] python-version: ["3.9", "3.10", "3.11",] - include: - - python-version: "3.12.0-beta.4" - runs-on: ubuntu-latest - allow_failure: true + # include: + # - python-version: "3.12.0-beta.4" + # runs-on: ubuntu-latest + # allow_failure: true # Currently no public runners available for this but this or arm64 should work next time # include: # - python-version: "3.10" From 44b33dfdae8d2eb56ab054a6a2a941947ac6dd49 Mon Sep 17 00:00:00 2001 From: bsavitzky Date: Mon, 2 Oct 2023 11:31:52 -0400 Subject: [PATCH 28/37] Update README.md --- README.md | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 0561f098a..aa102542a 100644 --- a/README.md +++ b/README.md @@ -46,42 +46,50 @@ First, download and install Anaconda: www.anaconda.com/download. If you prefer a more lightweight conda client, you can instead install Miniconda: https://docs.conda.io/en/latest/miniconda.html. Then open a conda terminal and run one of the following sets of commands to ensure everything is up-to-date and create a new environment for your py4DSTEM installation: - ``` conda update conda conda create -n py4dstem conda activate py4dstem +conda install -c conda-forge py4dstem pymatgen jupyterlab ``` -Next, install py4DSTEM. To simultaneously install py4DSTEM with `pymatgen` (used in some crystal structure workflows) and `jupyterlab` (providing an interface for running Python notebooks like those provided in the [py4DSTEM tutorials repository](https://github.com/py4dstem/py4DSTEM_tutorials)) run: +In order, these commands +- ensure your installation of anaconda is up-to-date +- make a virtual environment (see below) +- enter the environment +- install py4DSTEM, as well as pymatgen (used for crystal structure calculations) and JupyterLab (an interface for running Python notebooks like those in the [py4DSTEM tutorials repository](https://github.com/py4dstem/py4DSTEM_tutorials)) + + +We've had some recent reports install of `conda` getting stuck trying to solve the environment using the above installation. If you run into this problem, you can install py4DSTEM using `pip` instead of `conda` by running: ``` -conda install -c conda-forge py4dstem pymatgen jupyterlab +conda update conda +conda create -n py4dstem python=3.10 +conda activate py4dstem +pip install py4dstem pymatgen ``` -Or if you would prefer to install only the base modules of **py4DSTEM**, you can instead run: +Both `conda` and `pip` are programs which manage package installations, i.e. make sure different codes you're installing which depend on one another are using mutually compatible versions. Each has advantages and disadvantages; `pip` is a little more bare-bones, and we've seen this install work when `conda` doesn't. If you also want to use Jupyterlab you can then use either `pip install jupyterlab` or `conda install jupyterlab`. + +If you would prefer to install only the base modules of **py4DSTEM**, and skip pymategen and Jupterlab, you can instead run: ``` conda install -c conda-forge py4dstem ``` -In Windows you should then also run: +Finally, regardless of which of the above approaches you used, in Windows you should then also run: ``` conda install pywin32 ``` -In order, these commands -- ensure your installation of anaconda is up-to-date -- make a virtual environment (see below) -- enter the environment -- install py4DSTEM, and optionally also pymatgen and JupyterLab -- on Windows, enable python to talk to the windows API +which enables Python to talk to the Windows API. Please note that virtual environments are used in the instructions above in order to make sure packages that have different dependencies don't conflict with one another. Because these directions install py4DSTEM to its own virtual environment, each time you want to use py4DSTEM you'll need to activate this environment. You can do this in the command line by running `conda activate py4dstem`, or, if you're using the Anaconda Navigator, by clicking on the Environments tab and then clicking on `py4dstem`. +Last - as of the version 0.14.4 update, we've had a few reports of problems upgrading to the newest version. We're not sure what's causing the issue yet, but have found the new version can be installed successfully in these cases using a fresh Anaconda installation. From 23a2e946bc7c39e9da68377d6776682710aab5df Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Mon, 2 Oct 2023 15:18:05 -0400 Subject: [PATCH 29/37] update docstrings --- py4DSTEM/process/diffraction/crystal_ACOM.py | 28 ++++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/py4DSTEM/process/diffraction/crystal_ACOM.py b/py4DSTEM/process/diffraction/crystal_ACOM.py index e7b54f712..54d78409a 100644 --- a/py4DSTEM/process/diffraction/crystal_ACOM.py +++ b/py4DSTEM/process/diffraction/crystal_ACOM.py @@ -771,8 +771,26 @@ def match_orientations( progress_bar: bool = True, ): """ - This function computes the orientation of any number of PointLists stored in a PointListArray, - and returns an OrienationMap. Options are the same as match_single_pattern(). + Parameters + -------- + bragg_peaks_array: PointListArray + PointListArray containing the Bragg peaks and intensities, with calibrations applied + num_matches_return: int + return these many matches as 3th dim of orient (matrix) + min_angle_between_matches_deg: int + Minimum angle between zone axis of multiple matches, in degrees. + Note that I haven't thought how to handle in-plane rotations, since multiple matches are possible. + min_number_peaks: int + Minimum number of peaks required to perform ACOM matching + inversion_symmetry: bool + check for inversion symmetry in the matches + multiple_corr_reset: bool + keep original correlation score for multiple matches + return_orientation: bool + Return orientation map from function for inspection. + The map is always stored in the Crystal object. + progress_bar: bool + Show or hide the progress bar """ orientation_map = OrientationMap( @@ -820,12 +838,6 @@ def match_orientations( ) orientation_map.set_orientation(orientation, rx, ry) - self.orientation_map = orientation_map - - if return_orientation: - return orientation_map - else: - return # assign and return self.orientation_map = orientation_map From 17710bd5c5c7b6fe0063cac51e91fce04da4f379 Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Mon, 2 Oct 2023 15:56:03 -0400 Subject: [PATCH 30/37] replace pbar with tqdm --- py4DSTEM/process/diffraction/crystal_ACOM.py | 39 ++++++++------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/py4DSTEM/process/diffraction/crystal_ACOM.py b/py4DSTEM/process/diffraction/crystal_ACOM.py index 54d78409a..75c729865 100644 --- a/py4DSTEM/process/diffraction/crystal_ACOM.py +++ b/py4DSTEM/process/diffraction/crystal_ACOM.py @@ -3,6 +3,7 @@ import os from typing import Union, Optional import time, sys +from tqdm import tqdm from emdfile import tqdmnd, PointList, PointListArray from py4DSTEM.data import RealSlice @@ -15,7 +16,7 @@ try: import cupy as cp -except: +except ModuleNotFoundError: cp = None @@ -1777,19 +1778,15 @@ def cluster_grains( Parameters -------- - corr_threshold_add: float + threshold_add: float Minimum signal required for a probe position to initialize a cluster. - corr_threshold_grow: float + threshold_grow: float Minimum signal required for a probe position to be added to a cluster. angle_tolerance_deg: float Rotation rolerance for clustering grains. progress_bar: bool Turns on the progress bar for the polar transformation - Returns - -------- - - """ # symmetry operators @@ -1818,6 +1815,7 @@ def cluster_grains( # Main loop search = True comp = 0.0 + pbar = tqdm(total=N, display = not progress_bar) while search is True: inds_grain = np.argmax(sig) @@ -1828,11 +1826,11 @@ def cluster_grains( else: # progressbar - if progress_bar: - new_comp = 1 - np.mean(np.max(mark, axis=2)) - if new_comp > comp + 0.001: - comp = new_comp - update_progress(comp) + # if progress_bar: + # new_comp = 1 - np.mean(np.max(mark, axis=2)) + # if new_comp > comp + 0.001: + # comp = new_comp + # update_progress(comp) # Start cluster x, y, z = np.unravel_index(inds_grain, sig.shape) @@ -1896,6 +1894,8 @@ def cluster_grains( inds_add = inds_all[xr[:, None], yr[None], :].ravel() inds_new = np.append(inds_new, inds_add) + pbar.update(inds_add.size) + inds_grain = np.append(inds_grain, inds_cand[keep]) inds_cand = np.unique( np.delete(inds_new, mark.ravel()[inds_new] == False) @@ -1913,9 +1913,7 @@ def cluster_grains( self.cluster_orientation.append(orientation_cluster) self.cluster_inds.append(xyg) - # finish progressbar - if progress_bar: - update_progress(1) + pbar.close() def cluster_orientation_map( @@ -1929,9 +1927,8 @@ def cluster_orientation_map( Parameters -------- - stripe_width: (int,ind) - Width of strips in the overlapping regions. - Kind of janky but it mostly works! + stripe_width: (int,int) + Width of stripes for plotting maps with overlapping grains area_min: (int) Minimum size of grains to include @@ -1955,12 +1952,6 @@ def cluster_orientation_map( im_count = np.zeros((self.orientation_map.num_x, self.orientation_map.num_y)) im_mark = np.zeros((self.orientation_map.num_x, self.orientation_map.num_y)) - # # coordinates - # xa,ya = np.meshgrid( - # range(self.orientation_map.num_x), - # range(self.orientation_map.num_y), - # indexing = 'ij') - # Loop over grains to determine number in each pixel for a0 in range(self.cluster_sizes.shape[0]): if self.cluster_sizes[a0] >= area_min: From 221b34b95d98942fce60f9e55687a06585a33b1a Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Wed, 4 Oct 2023 12:32:37 -0400 Subject: [PATCH 31/37] tqdm-based progress bar in clustering --- py4DSTEM/process/diffraction/crystal_ACOM.py | 40 +++----------------- 1 file changed, 6 insertions(+), 34 deletions(-) diff --git a/py4DSTEM/process/diffraction/crystal_ACOM.py b/py4DSTEM/process/diffraction/crystal_ACOM.py index 75c729865..115f8140c 100644 --- a/py4DSTEM/process/diffraction/crystal_ACOM.py +++ b/py4DSTEM/process/diffraction/crystal_ACOM.py @@ -788,7 +788,7 @@ def match_orientations( multiple_corr_reset: bool keep original correlation score for multiple matches return_orientation: bool - Return orientation map from function for inspection. + Return orientation map from function for inspection. The map is always stored in the Crystal object. progress_bar: bool Show or hide the progress bar @@ -1815,7 +1815,8 @@ def cluster_grains( # Main loop search = True comp = 0.0 - pbar = tqdm(total=N, display = not progress_bar) + mark_total = np.sum(np.max(mark, axis=2)) + pbar = tqdm(total=mark_total, disable=not progress_bar) while search is True: inds_grain = np.argmax(sig) @@ -1825,12 +1826,9 @@ def cluster_grains( search = False else: - # progressbar - # if progress_bar: - # new_comp = 1 - np.mean(np.max(mark, axis=2)) - # if new_comp > comp + 0.001: - # comp = new_comp - # update_progress(comp) + new_marks = mark_total - np.sum(np.max(mark, axis=2)) + pbar.update(new_marks) + mark_total -= new_marks # Start cluster x, y, z = np.unravel_index(inds_grain, sig.shape) @@ -1894,8 +1892,6 @@ def cluster_grains( inds_add = inds_all[xr[:, None], yr[None], :].ravel() inds_new = np.append(inds_new, inds_add) - pbar.update(inds_add.size) - inds_grain = np.append(inds_grain, inds_cand[keep]) inds_cand = np.unique( np.delete(inds_new, mark.ravel()[inds_new] == False) @@ -2551,27 +2547,3 @@ def symmetry_reduce_directions( # "-3m": ["fiber", [0, 0, 1], [90.0, 60.0]], # "-3m": ["fiber", [0, 0, 1], [180.0, 30.0]], - - -# Progressbar taken from stackexchange: -# https://stackoverflow.com/questions/3160699/python-progress-bar -def update_progress(progress): - barLength = 60 # Modify this to change the length of the progress bar - status = "" - if isinstance(progress, int): - progress = float(progress) - if not isinstance(progress, float): - progress = 0 - status = "error: progress var must be float\r\n" - if progress < 0: - progress = 0 - status = "Halt...\r\n" - if progress >= 1: - progress = 1 - status = "Done\r\n" - block = int(round(barLength * progress)) - text = "\rPercent: [{0}] {1}% {2}".format( - "#" * block + "-" * (barLength - block), np.round(progress * 100, 2), status - ) - sys.stdout.write(text) - sys.stdout.flush() From a48093f9a9b8bc398bc9b94f3dba4519be9dc31d Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Thu, 5 Oct 2023 09:23:51 -0400 Subject: [PATCH 32/37] move progress update step --- py4DSTEM/process/diffraction/crystal_ACOM.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/py4DSTEM/process/diffraction/crystal_ACOM.py b/py4DSTEM/process/diffraction/crystal_ACOM.py index 115f8140c..da553456f 100644 --- a/py4DSTEM/process/diffraction/crystal_ACOM.py +++ b/py4DSTEM/process/diffraction/crystal_ACOM.py @@ -1826,10 +1826,6 @@ def cluster_grains( search = False else: - new_marks = mark_total - np.sum(np.max(mark, axis=2)) - pbar.update(new_marks) - mark_total -= new_marks - # Start cluster x, y, z = np.unravel_index(inds_grain, sig.shape) mark[x, y, z] = False @@ -1909,6 +1905,11 @@ def cluster_grains( self.cluster_orientation.append(orientation_cluster) self.cluster_inds.append(xyg) + # update progressbar + new_marks = mark_total - np.sum(np.max(mark, axis=2)) + pbar.update(new_marks) + mark_total -= new_marks + pbar.close() From b067fc17d2bedc1be2be93137106884694c43eaa Mon Sep 17 00:00:00 2001 From: alex-rakowski Date: Fri, 6 Oct 2023 03:17:00 -0700 Subject: [PATCH 33/37] adding "sphinx_rtd_theme" to conf.py --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 30ee084fe..faad8dee0 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -36,7 +36,7 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ["sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx.ext.intersphinx"] +extensions = ["sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx.ext.intersphinx", "sphinx_rtd_theme"] # Other useful extensions # sphinx_copybutton From 3096f6c8dfc7ff4fa9c719d51f6175bd1e0cfca7 Mon Sep 17 00:00:00 2001 From: alex-rakowski Date: Fri, 6 Oct 2023 03:24:29 -0700 Subject: [PATCH 34/37] adding sphinx_rtd_theme to setup.py --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 069bf1600..55acea582 100644 --- a/setup.py +++ b/setup.py @@ -40,6 +40,7 @@ "emdfile >= 0.0.13", "mpire >= 2.7.1", "threadpoolctl >= 3.1.0", + "sphinx_rtd_theme", ], extras_require={ "ipyparallel": ["ipyparallel >= 6.2.4", "dill >= 0.3.3"], From d7ac9e1ab015898fb8660bf4ac4f057abec4ecfe Mon Sep 17 00:00:00 2001 From: alex-rakowski Date: Fri, 6 Oct 2023 03:28:18 -0700 Subject: [PATCH 35/37] black style conf.py --- docs/source/conf.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index faad8dee0..6da66611e 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -36,7 +36,12 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ["sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx.ext.intersphinx", "sphinx_rtd_theme"] +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", + "sphinx.ext.intersphinx", + "sphinx_rtd_theme", +] # Other useful extensions # sphinx_copybutton From b19d173fae794605e5e67850b89233029ae31652 Mon Sep 17 00:00:00 2001 From: alex-rakowski Date: Fri, 6 Oct 2023 11:26:33 -0700 Subject: [PATCH 36/37] adding sphinx_rtd_theme to docs requirements.txt --- docs/requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 43dbc0817..03ecc7e26 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,3 @@ emdfile -# py4dstem \ No newline at end of file +sphinx_rtd_theme +# py4dstem From e07dddd061e3bbcbdb8c60b74394a85bbfed1364 Mon Sep 17 00:00:00 2001 From: alex-rakowski Date: Fri, 6 Oct 2023 11:27:29 -0700 Subject: [PATCH 37/37] removing sphinx_rtd_theme from setup.py --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 55acea582..069bf1600 100644 --- a/setup.py +++ b/setup.py @@ -40,7 +40,6 @@ "emdfile >= 0.0.13", "mpire >= 2.7.1", "threadpoolctl >= 3.1.0", - "sphinx_rtd_theme", ], extras_require={ "ipyparallel": ["ipyparallel >= 6.2.4", "dill >= 0.3.3"],