diff --git a/src/nomad_measurements/utils.py b/src/nomad_measurements/utils.py index 7fbed4aa..61b984a1 100644 --- a/src/nomad_measurements/utils.py +++ b/src/nomad_measurements/utils.py @@ -261,3 +261,35 @@ def modify_scan_data(scan_data: dict, scan_type: str): data = data[0].reshape(-1) output[key] = data * value[0].units return output + +def get_bounding_range_2d(ax1, ax2): + ''' + Calculates the range of the smallest rectangular grid that can contain arbitrarily + distributed 2D data. + + Args: + ax1 (np.ndarray): array of first axis values + ax2 (np.ndarray): array of second axis values + + Returns: + (list, list): ax1_range, ax2_range + ''' + ax1_range_length = np.max(ax1) - np.min(ax1) + ax2_range_length = np.max(ax2) - np.min(ax2) + + if ax1_range_length > ax2_range_length: + ax1_range = [np.min(ax1),np.max(ax1)] + ax2_mid = np.min(ax2) + ax2_range_length/2 + ax2_range = [ + ax2_mid-ax1_range_length/2, + ax2_mid+ax1_range_length/2, + ] + else: + ax2_range = [np.min(ax2),np.max(ax2)] + ax1_mid = np.min(ax1) + ax1_range_length/2 + ax1_range = [ + ax1_mid-ax2_range_length/2, + ax1_mid+ax2_range_length/2, + ] + + return ax1_range, ax2_range \ No newline at end of file diff --git a/src/nomad_measurements/xrd/plotting.py b/src/nomad_measurements/xrd/plotting.py deleted file mode 100644 index 9293918e..00000000 --- a/src/nomad_measurements/xrd/plotting.py +++ /dev/null @@ -1,165 +0,0 @@ -import numpy as np -import plotly.express as px -from scipy.interpolate import griddata - -def plot_1d(x, y): - ''' - Plot the 1D diffractogram. - - Args: - x (np.ndarray): array of x values - y (np.ndarray): array of y values - - Returns: - (dict, dict): line_linear, line_log - ''' - fig_line_linear = px.line( - x = x, - y = y, - labels = { - 'x': '2θ (°)', - 'y': 'Intensity', - }, - title = 'Intensity (linear scale)', - ) - json_line_linear = fig_line_linear.to_plotly_json() - - fig_line_log = px.line( - x = x, - y = y, - log_y = True, - labels = { - 'x': '2θ (°)', - 'y': 'Intensity', - }, - title = 'Intensity (log scale)', - ) - json_line_log = fig_line_log.to_plotly_json() - - return json_line_linear, json_line_log - -def plot_2d_range(ax1, ax2): - ''' - Calculate the range of the 2D plot for generation of regular grid. - Finds the smallest box that can contain the data. - - Args: - ax1 (np.ndarray): array of first axis values - ax2 (np.ndarray): array of second axis values - - Returns: - (list, list): ax1_range, ax2_range - ''' - ax1_range_length = np.max(ax1) - np.min(ax1) - ax2_range_length = np.max(ax2) - np.min(ax2) - - if ax1_range_length > ax2_range_length: - ax1_range = [np.min(ax1),np.max(ax1)] - ax2_mid = np.min(ax2) + ax2_range_length/2 - ax2_range = [ - ax2_mid-ax1_range_length/2, - ax2_mid+ax1_range_length/2, - ] - else: - ax2_range = [np.min(ax2),np.max(ax2)] - ax1_mid = np.min(ax1) + ax1_range_length/2 - ax1_range = [ - ax1_mid-ax2_range_length/2, - ax1_mid+ax2_range_length/2, - ] - - return ax1_range, ax2_range - -def plot_2d_rsm(two_theta, omega, q_parallel, q_perpendicular, intensity): - ''' - Plot the 2D RSM diffractogram. - - Args: - two_theta (pint.Quantity): array of 2θ values - omega (pint.Quantity): array of ω values - q_parallel (pint.Quantity): array of Q_parallel values - q_perpendicular (pint.Quantity): array of Q_perpendicular values - intensity (pint.Quantity): array of intensity values - - Returns: - (dict, dict): json_2theta_omega, json_q_vector - ''' - # Plot for 2theta-omega RSM - x = omega.magnitude - y = two_theta.magnitude - log_z = np.log10(intensity) - x_range, y_range = plot_2d_range(x, y) - - fig_2theta_omega = px.imshow( - img = np.around(log_z,3).T, - x = np.around(x,3), - y = np.around(y,3), - color_continuous_scale = 'inferno', - ) - fig_2theta_omega.update_layout( - title = 'RSM plot: Intensity (log-scale) vs Axis position', - xaxis_title = 'ω (°)', - yaxis_title = '2θ (°)', - xaxis = dict( - autorange = False, - fixedrange = False, - range = x_range, - ), - yaxis = dict( - autorange = False, - fixedrange = False, - range = y_range, - ), - width = 600, - height = 600, - ) - json_2theta_omega = fig_2theta_omega.to_plotly_json() - - # Plot for RSM in Q-vectors - if q_parallel is not None and q_perpendicular is not None: - x = q_parallel.to('1/angstrom').magnitude.flatten() - y = q_perpendicular.to('1/angstrom').magnitude.flatten() - # q_vectors lead to irregular grid - # generate a regular grid using interpolation - x_regular = np.linspace(x.min(),x.max(),intensity.shape[0]) - y_regular = np.linspace(y.min(),y.max(),intensity.shape[1]) - x_grid, y_grid = np.meshgrid(x_regular,y_regular) - z_interpolated = griddata( - points = (x,y), - values = intensity.flatten(), - xi = (x_grid,y_grid), - method = 'linear', - fill_value = intensity.min(), - ) - log_z_interpolated = np.log10(z_interpolated) - x_range, y_range = plot_2d_range(x_regular,y_regular) - - fig_q_vector = px.imshow( - img = np.around(log_z_interpolated,3), - x = np.around(x_regular,3), - y = np.around(y_regular,3), - color_continuous_scale = 'inferno', - range_color = [np.nanmin(log_z[log_z != -np.inf]), log_z_interpolated.max()], - ) - fig_q_vector.update_layout( - title = 'RSM plot: Intensity (log-scale) vs Q-vectors', - xaxis_title = 'Q_parallel (1/Å)', - yaxis_title = 'Q_perpendicular (1/Å)', - xaxis = dict( - autorange = False, - fixedrange = False, - range = x_range, - ), - yaxis = dict( - autorange = False, - fixedrange = False, - range = y_range, - ), - width = 600, - height = 600, - ) - json_q_vector = fig_q_vector.to_plotly_json() - - return json_2theta_omega, json_q_vector - - return json_2theta_omega, None diff --git a/src/nomad_measurements/xrd/schema.py b/src/nomad_measurements/xrd/schema.py index ff8f2f72..60c07a56 100644 --- a/src/nomad_measurements/xrd/schema.py +++ b/src/nomad_measurements/xrd/schema.py @@ -64,8 +64,7 @@ NOMADMeasurementsCategory, ) from nomad_measurements.xrd import readers -from nomad_measurements.xrd import plotting -from nomad_measurements.utils import merge_sections +from nomad_measurements.utils import merge_sections, get_bounding_range_2d if TYPE_CHECKING: from nomad.datamodel.datamodel import ( @@ -362,7 +361,7 @@ def derive_n_values(self): description='Integration time per channel', ) -class XRDResult1D(XRDResult, PlotSection): +class XRDResult1D(XRDResult): ''' Section containing the result of a 1D X-ray diffraction scan. ''' @@ -379,6 +378,60 @@ class XRDResult1D(XRDResult, PlotSection): }, ) + def generate_plots(self, archive: 'EntryArchive', logger: 'BoundLogger'): + ''' + Plot the 1D diffractogram. + + Args: + archive (EntryArchive): The archive containing the section that is being + normalized. + logger (BoundLogger): A structlog logger. + + Returns: + (dict, dict): line_linear, line_log + ''' + plots = [] + + x = self.two_theta.to('degree').magnitude + y = self.intensity.magnitude + + fig_line_linear = px.line( + x = x, + y = y, + labels = { + 'x': '2θ (°)', + 'y': 'Intensity', + }, + title = 'Intensity (linear scale)', + ) + plots.append( + PlotlyFigure( + label = 'Intensity vs 2Theta (Linear)', + index = 1, + figure = fig_line_linear.to_plotly_json(), + ) + ) + + fig_line_log = px.line( + x = x, + y = y, + log_y = True, + labels = { + 'x': '2θ (°)', + 'y': 'Intensity', + }, + title = 'Intensity (log scale)', + ) + plots.append( + PlotlyFigure( + label = 'Intensity vs 2Theta (Log)', + index = 0, + figure = fig_line_log.to_plotly_json(), + ) + ) + + return plots + def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger'): ''' The normalize function of the `XRDResult` section. @@ -396,24 +449,7 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger'): q=self.q_vector, ) - json_line_linear, json_line_log = plotting.plot_1d( - self.two_theta, - self.intensity, - ) - self.figures = [ - PlotlyFigure( - label = 'Log Plot', - index = 1, - figure = json_line_log, - ), - PlotlyFigure( - label = 'Linear Plot', - index = 2, - figure = json_line_linear, - ), - ] - -class XRDResultRSM(XRDResult, PlotSection): +class XRDResultRSM(XRDResult): ''' Section containing the result of a Reciprocal Space Map (RSM) scan. ''' @@ -437,7 +473,117 @@ class XRDResultRSM(XRDResult, PlotSection): description='The count at each position, dimensionless', ) - def normalize(self, archive: 'EntryArchive', logger: BoundLogger): + def generate_plots(self, archive: 'EntryArchive', logger: 'BoundLogger'): + ''' + Plot the 2D RSM diffractogram. + + Args: + archive (EntryArchive): The archive containing the section that is being + normalized. + logger (BoundLogger): A structlog logger. + + Returns: + (dict, dict): json_2theta_omega, json_q_vector + ''' + plots = [] + + # Plot for 2theta-omega RSM + x = self.omega.to('degree').magnitude + y = self.two_theta.to('degree').magnitude + z = self.intensity.magnitude + log_z = np.log10(z) + x_range, y_range = get_bounding_range_2d(x, y) + + fig_2theta_omega = px.imshow( + img = np.around(log_z,3).T, + x = np.around(x,3), + y = np.around(y,3), + color_continuous_scale = 'inferno', + ) + fig_2theta_omega.update_layout( + title = 'RSM plot: Intensity (log-scale) vs Axis position', + xaxis_title = 'ω (°)', + yaxis_title = '2θ (°)', + xaxis = dict( + autorange = False, + fixedrange = False, + range = x_range, + ), + yaxis = dict( + autorange = False, + fixedrange = False, + range = y_range, + ), + width = 600, + height = 600, + ) + plots.append( + PlotlyFigure( + label = 'RSM 2Theta-Omega', + index = 1, + figure = fig_2theta_omega.to_plotly_json(), + ), + ) + + # Plot for RSM in Q-vectors + print(self.q_parallel) + print(self.q_perpendicular) + print("-here----------------- ") + + if self.q_parallel is not None and self.q_perpendicular is not None: + print("-----------------here- ") + x = self.q_parallel.to('1/angstrom').magnitude.flatten() + y = self.q_perpendicular.to('1/angstrom').magnitude.flatten() + # q_vectors lead to irregular grid + # generate a regular grid using interpolation + x_regular = np.linspace(x.min(),x.max(),z.shape[0]) + y_regular = np.linspace(y.min(),y.max(),z.shape[1]) + x_grid, y_grid = np.meshgrid(x_regular,y_regular) + z_interpolated = griddata( + points = (x,y), + values = z.flatten(), + xi = (x_grid,y_grid), + method = 'linear', + fill_value = z.min(), + ) + log_z_interpolated = np.log10(z_interpolated) + x_range, y_range = get_bounding_range_2d(x_regular,y_regular) + + fig_q_vector = px.imshow( + img = np.around(log_z_interpolated,3), + x = np.around(x_regular,3), + y = np.around(y_regular,3), + color_continuous_scale = 'inferno', + range_color = [np.nanmin(log_z[log_z != -np.inf]), log_z_interpolated.max()], + ) + fig_q_vector.update_layout( + title = 'RSM plot: Intensity (log-scale) vs Q-vectors', + xaxis_title = 'Q_parallel (1/Å)', + yaxis_title = 'Q_perpendicular (1/Å)', + xaxis = dict( + autorange = False, + fixedrange = False, + range = x_range, + ), + yaxis = dict( + autorange = False, + fixedrange = False, + range = y_range, + ), + width = 600, + height = 600, + ) + plots.append( + PlotlyFigure( + label = 'RSM Q-Vectors', + index = 0, + figure = fig_q_vector.to_plotly_json(), + ), + ) + + return plots + + def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger'): super().normalize(archive, logger) var_axis = 'omega' if self.source_peak_wavelength is not None: @@ -451,29 +597,6 @@ def normalize(self, archive: 'EntryArchive', logger: BoundLogger): ) break - json_2theta_omega, json_q_vector = plotting.plot_2d_rsm( - self.two_theta, - self[var_axis], - self.q_parallel, - self.q_perpendicular, - self.intensity, - ) - self.figures = [ - PlotlyFigure( - label = 'RSM (2Theta-Omega)', - index = 2, - figure = json_2theta_omega, - ), - ] - if json_2theta_omega: - self.figures.append( - PlotlyFigure( - label = 'RSM (Q-vectors)', - index = 1, - figure = json_q_vector, - ), - ) - class XRayDiffraction(Measurement): ''' Generic X-ray diffraction measurement. @@ -557,7 +680,7 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger'): ) -class ELNXRayDiffraction(XRayDiffraction, EntryData): +class ELNXRayDiffraction(XRayDiffraction, EntryData, PlotSection): ''' Example section for how XRayDiffraction can be implemented with a general reader for common XRD file types. @@ -811,6 +934,7 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger'): xrd_dict = read_function(file.name, logger) write_function(xrd_dict, archive, logger) super().normalize(archive, logger) + self.figures = self.results[0].generate_plots(archive, logger) if not self.results: return