diff --git a/docs/source/images/DNS_interface_single_crystal_elastic_plotting_tab.png b/docs/source/images/DNS_interface_single_crystal_elastic_plotting_tab.png deleted file mode 100644 index bfda25c6f5eb..000000000000 Binary files a/docs/source/images/DNS_interface_single_crystal_elastic_plotting_tab.png and /dev/null differ diff --git a/docs/source/images/DNS_interface_single_crystal_elastic_plotting_tab_updated.png b/docs/source/images/DNS_interface_single_crystal_elastic_plotting_tab_updated.png new file mode 100644 index 000000000000..7d293fc5b804 Binary files /dev/null and b/docs/source/images/DNS_interface_single_crystal_elastic_plotting_tab_updated.png differ diff --git a/docs/source/interfaces/direct/dns_reduction/dns_single_crystal_elastic_plotting_tab.rst b/docs/source/interfaces/direct/dns_reduction/dns_single_crystal_elastic_plotting_tab.rst index 2f3a8362e9ee..dfe4257b9b61 100644 --- a/docs/source/interfaces/direct/dns_reduction/dns_single_crystal_elastic_plotting_tab.rst +++ b/docs/source/interfaces/direct/dns_reduction/dns_single_crystal_elastic_plotting_tab.rst @@ -5,7 +5,7 @@ DNS Single Crystal Elastic - Plotting Tab The **Plotting** tab offers basic plotting functionality for reduced datasets. -.. image:: ../../../images/DNS_interface_single_crystal_elastic_plotting_tab.png +.. image:: ../../../images/DNS_interface_single_crystal_elastic_plotting_tab_updated.png :align: center :height: 400px @@ -36,7 +36,7 @@ The buttons below the **Plotting** tab have the following functionality (from le | | scattering plane. It works only if two principle axes change in the | | | horizontal plane. | +------------------------------------------------+----------------------------------------------------------------------------+ -| |exp-projections| Projection selector | Turn on/off projections of the intensity function averaged along | +| |exp-projections| Projection selector | Turn on/off projections of the intensity function along | | | :math:`x` and :math:`y` directions. | +------------------------------------------------+----------------------------------------------------------------------------+ | |exp-edge-control| Data | Toggle through drawing of borders of the triangles or quadrilaterals of the| @@ -52,20 +52,21 @@ The buttons below the **Plotting** tab have the following functionality (from le The rest of the buttons have the same functionality as in matplotlib's navigation toolbar. -When the mouse cursor is hovered over the plot, the cursor's :math:`(x, y)` coordinates together with the coresponding -:math:`hkl` values (in relative lattice units, r.l.u.) will be displayed on the right hand side of matplotlib's control buttons. -In addition, the correponding value of intensity (with an error-bar) of the closest measured data point will be displayed as well. -(This does not give the intensity of the quadrilateral, which could involve interpolation.) +When the mouse cursor hovers over the plot, the cursor's :math:`(x, y)` coordinates together with the corresponding +:math:`hkl` values (in relative lattice units, r.l.u.) will be displayed to the right of matplotlib's control buttons. +In addition, the intensity value (with an error-bar) of the closest measured data point will also be shown. Note that when +interpolation is enabled (either in triangulation or quadrilateral mode), the displayed intensity value will not include the +error estimate. Furthermore, since triangulation does not create triangles centered at the measured data points, the error +estimate is not displayed in this mode either. -The **X**, **Y** and **Z** input lines below the navigation buttons allow to manually specify the region to zoom into. The -syntax to be used inside these lines is similar to the python's list slicing (or dnsplot's range). For example, **0:2** in the -**X** input line would imply that the displayed values of X should be in the range from 0 to 2. When the desired ranges of -**X**, **Y** and **Z** are specified, pressing the Enter key inside an input line will update the plot. +The input fields labeled :math:`\mathbf{X_{min}/X_{max}}`, :math:`\mathbf{Y_{min}/Y_{max}}` and :math:`\mathbf{I_{min}/I_{max}}`, +located below the navigation buttons, allow you to manually specify the region to zoom into. Once the desired values are entered, +pressing the Enter key in any input field will update the plot. -The **log** button next to **X, Y, Z** turns on/off the logarithmic intensity scale. The dropdown list nearby to the **log** -button allows to select different colormaps for visualisation. The button next to the dropdown list inverts the colormap color -scale. The **FontSize** dialog allows to change the fontsize of the legend (the Enter key has to be pressed inside the fontsize -box for the change to have an effect). +The **log** button next to :math:`\mathbf{I_{max}}` turns on/off the logarithmic intensity scale. The dropdown list nearby to the +**log** button allows to select different colormaps for visualisation. The button next to the dropdown list inverts the colormap +color scale. The **FontSize** dialog allows to change the fontsize of the legend (the Enter key has to be pressed inside the +fontsize box for the change to have an effect). The **Plot View** menu, which can be accessed from the main menu of the "DNS Reduction" GUI, offers control over the style of the plot. @@ -81,6 +82,7 @@ The **Axes** menu of **Plot View** allows to change the plot axes between (:math **Options**, or (:math:`q_x, q_x`), or (:math:`2 \theta, \omega`). In addition, **Fix Aspect Ratio** can be selected to fix the aspect ratio between the :math:`x` and :math:`y` axis of the plot. This is especially usefull if crystallographic axes are used since then the shown angles will be correct. **Switch Axes** will switch :math:`x` and :math:`y` axes in the plot. +**Fix Aspect Ratio** is not available in (:math:`2 \theta, \omega`) mode. The **Interpolation** menu of **Plot View** allows to generate additional triangles/quadrilaterals in order to make the plot look smoother. For example, if **1->9** option is selected, each quadrilateral will be replaced by 9 quadrilaterals, and so diff --git a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/data_structures/dns_single_crystal_map.py b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/data_structures/dns_single_crystal_map.py index 9d4b33456633..85799f0c4e67 100644 --- a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/data_structures/dns_single_crystal_map.py +++ b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/data_structures/dns_single_crystal_map.py @@ -10,8 +10,10 @@ """ import numpy as np +import scipy from matplotlib import path from matplotlib import tri +from matplotlib.tri import LinearTriInterpolator, UniformTriRefiner from mantidqtinterfaces.dns_powder_tof.data_structures.object_dict import ObjectDict from mantidqtinterfaces.dns_single_crystal_elastic.plot.elastic_single_crystal_helpers import angle_to_q, get_hkl_float_array @@ -26,6 +28,19 @@ def _get_mesh(omega, two_theta, z_mesh, z_error_mesh): return omega_mesh_no_nan, two_theta_mesh_no_nan, z_mesh_no_nan, z_error_mesh_no_nan +def _is_rectangular_mesh(omega, two_theta, z_mesh): + return z_mesh.size == len(omega) * len(two_theta) + + +def _correct_rect_grid(omega_mesh, two_theta_mesh, z_mesh, z_error_mesh, omega, two_theta): + rectangular_grid = _is_rectangular_mesh(omega, two_theta, z_mesh) + if rectangular_grid: + omega_mesh, two_theta_mesh = np.meshgrid(omega, two_theta) + z_mesh = np.reshape(z_mesh, omega_mesh.shape) + z_error_mesh = np.reshape(z_error_mesh, omega_mesh.shape) + return omega_mesh, two_theta_mesh, z_mesh, z_error_mesh, rectangular_grid + + def _correct_omega_offset(omega, omega_offset): return np.subtract(omega, omega_offset) @@ -47,6 +62,15 @@ def _get_hkl_mesh(qx_mesh, qy_mesh, dx, dy): return hklx_mesh, hkly_mesh +def _get_interpolated(value, interpolation_order): + # interpolation_order = 0: no interpolation + # interpolation_order = 1 (2, or 3): the amount of datapoints is + # doubled (tripled or quadrupled), the datapoints are uniformly + # spaced between min and max values + interpolated_values = np.linspace(value.min(), value.max(), value.size * (interpolation_order + 1)) + return interpolated_values + + class DNSScMap(ObjectDict): """ Class for storing data of a single DNS single crystal plot. @@ -58,13 +82,21 @@ def __init__(self, parameter, two_theta=None, omega=None, z_mesh=None, error_mes omega_corrected = _correct_omega_offset(omega, parameter["omega_offset"]) omega_mesh, two_theta_mesh, z_mesh, z_error_mesh = _get_mesh(omega_corrected, two_theta, z_mesh, error_mesh) omega_unique, two_theta_unique = _get_unique(omega_mesh, two_theta_mesh) + omega_mesh, two_theta_mesh, z_mesh, z_error_mesh, rectangular_grid = _correct_rect_grid( + omega_mesh, two_theta_mesh, z_mesh, z_error_mesh, omega_corrected, two_theta_unique + ) qx_mesh, qy_mesh = _get_q_mesh(omega_mesh, two_theta_mesh, parameter["wavelength"]) hklx_mesh, hkly_mesh = _get_hkl_mesh(qx_mesh, qy_mesh, parameter["dx"], parameter["dy"]) # setting attributes dictionary keys self.omega_interpolated = None + self.rectangular_grid = rectangular_grid self.two_theta = two_theta_unique self.omega = omega_unique self.omega_offset = parameter["omega_offset"] + self.omega_mesh = omega_mesh + self.two_theta_mesh = two_theta_mesh + self.qx_mesh = qx_mesh + self.qy_mesh = qy_mesh self.hklx_mesh = hklx_mesh self.hkly_mesh = hkly_mesh self.z_mesh = z_mesh @@ -75,47 +107,107 @@ def __init__(self, parameter, two_theta=None, omega=None, z_mesh=None, error_mes self.hkl2 = parameter["hkl2"] self.wavelength = parameter["wavelength"] self.hkl_mesh = [self.hklx_mesh, self.hkly_mesh, self.z_mesh] + self.qxqy_mesh = [self.qx_mesh, self.qy_mesh, self.z_mesh] + self.angular_mesh = [self.two_theta_mesh, self.omega_mesh, self.z_mesh] + self.triangulation = None + self.x_tri = None # shape (n_triangles, 3) + self.x_face = None # shape (n_triangles,) + self.y_tri = None + self.y_face = None + self.z_tri = None + self.z_face = None + + def _get_z_mesh_interpolation(self, two_theta_interp, omega_interp): + f = scipy.interpolate.RectBivariateSpline(self.two_theta, self.omega, self.z_mesh, kx=1, ky=1) + return f(two_theta_interp, omega_interp) def triangulate(self, mesh_name, switch=False): plot_x, plot_y, _z = getattr(self, mesh_name) + if switch: + plot_x, plot_y = plot_y, plot_x self.triangulation = tri.Triangulation(plot_x.flatten(), plot_y.flatten()) - return self.triangulation def interpolate_triangulation(self, interpolation=0): if self.triangulation is None: - return None - z = self.z_mesh - triangulator = self.triangulation - return [triangulator, z.flatten()] - - def get_dns_map_border(self, mesh_name): + return + z = self.z_mesh.flatten() + triangulation = self.triangulation + interpolator = LinearTriInterpolator(triangulation, z) + refiner = UniformTriRefiner(triangulation) + triangulation_refined, z_refined = refiner.refine_field(z, subdiv=interpolation, triinterpolator=interpolator) + triangles = triangulation_refined.triangles + self.triangulation = triangulation_refined + self.z_mesh = z_refined + self.z_tri = z_refined[triangles] + self.z_face = self.z_tri.mean(axis=1) + # record x,y locations of the centers of interpolated triangles + x = triangulation_refined.x + y = triangulation_refined.y + self.x_tri = x[triangles] + self.y_tri = y[triangles] + self.x_face = self.x_tri.mean(axis=1) + self.y_face = self.y_tri.mean(axis=1) + + def interpolate_quad_mesh(self, interpolation=3): + if not self.rectangular_grid: + return + two_theta_interpolated = _get_interpolated(self.two_theta, interpolation) + omega_interpolated = _get_interpolated(self.omega, interpolation) + omega_mesh_interpolated, two_theta_mesh_interpolated = np.meshgrid(omega_interpolated, two_theta_interpolated) + qx_mesh_interpolated, qy_mesh_interpolated = _get_q_mesh(omega_mesh_interpolated, two_theta_mesh_interpolated, self.wavelength) + hklx_mesh_interpolated, hkly_mesh_interpolated = _get_hkl_mesh(qx_mesh_interpolated, qy_mesh_interpolated, self.dx, self.dy) + z_mesh_interpolated = self._get_z_mesh_interpolation(two_theta_interpolated, omega_interpolated) + self.angular_mesh = [two_theta_mesh_interpolated, omega_mesh_interpolated, z_mesh_interpolated] + self.hkl_mesh = [hklx_mesh_interpolated, hkly_mesh_interpolated, z_mesh_interpolated] + self.qxqy_mesh = [qx_mesh_interpolated, qy_mesh_interpolated, z_mesh_interpolated] + + def get_dns_map_border(self, mesh_name, switch): two_theta = self.two_theta omega = self.omega dns_path = np.zeros((2 * two_theta.size + 2 * omega.size, 2)) - hkl_path = np.zeros((2 * two_theta.size + 2 * omega.size, 2)) - dns_path[:, 0] = np.concatenate( + angular_border = np.zeros((2 * two_theta.size + 2 * omega.size, 2)) + hkl_border = np.zeros((2 * two_theta.size + 2 * omega.size, 2)) + qxqy_border = np.zeros((2 * two_theta.size + 2 * omega.size, 2)) + # In two_theta-omega plane we set borders of the rectangle, specifying edge sides of the rectangle. + # For a typical measurement at the DNS (two theta: 5...124, omega: 135...304 deg) it would be: + # a) left side two_theta=5; omega=135, 136, ... ,304 + # b) bottom side two_theta=5, 6, ..., 124; omega=135 + # c) right side two_theta=124, omega=135, 136, ... ,304 + # d) upper side two_theta=5, 6, ..., 124; omega=304 + # set first column values (two theta) + angular_border[:, 0] = np.concatenate( (two_theta[0] * np.ones(omega.size), two_theta, two_theta[-1] * np.ones(omega.size), np.flip(two_theta)) + ) # [5,5,..,5(170), 5,6,7,...,124(120), 124,124,..,124(170), 124,123,...,5(120)] + # set second column values (omega) + angular_border[:, 1] = np.concatenate( + (np.flip(omega), omega[0] * np.ones(two_theta.size), omega, omega[-1] * np.ones(two_theta.size)) ) - dns_path[:, 1] = np.concatenate((np.flip(omega), omega[0] * np.ones(two_theta.size), omega, omega[-1] * np.ones(two_theta.size))) - dns_path[:, 0], dns_path[:, 1] = angle_to_q(dns_path[:, 0], dns_path[:, 1], self.wavelength) - if "hkl" in mesh_name: - hkl_path[:, 0] = dns_path[:, 0] * self.dx / 2.0 / np.pi - hkl_path[:, 1] = dns_path[:, 1] * self.dy / 2.0 / np.pi - return path.Path(hkl_path) + # [304,303,...,135(170), 135,135,...,135(120), 135,136,...,304(170), 304,304,..,304(120)] + qxqy_border[:, 0], qxqy_border[:, 1] = angle_to_q(angular_border[:, 0], angular_border[:, 1], self.wavelength) + hkl_border[:, 0], hkl_border[:, 1] = qxqy_border[:, 0] * self.dx / 2.0 / np.pi, qxqy_border[:, 1] * self.dy / 2.0 / np.pi + if "angular" in mesh_name: + dns_path = angular_border + elif "qxqy" in mesh_name: + dns_path = qxqy_border + elif "hkl" in mesh_name: + dns_path = hkl_border + if switch: + dns_path = dns_path[::-1, ::-1] return path.Path(dns_path) - def mask_triangles(self, mesh_name): + def mask_triangles(self, mesh_name, switch): x, y, _z = getattr(self, mesh_name) + if switch: + x, y = y, x x = x.flatten() y = y.flatten() - dns_path = self.get_dns_map_border(mesh_name) + dns_path = self.get_dns_map_border(mesh_name, switch) triangles = self.triangulation.triangles x_y_triangles = np.zeros((len(x[triangles]), 2)) x_y_triangles[:, 0] = np.mean(x[triangles], axis=1) x_y_triangles[:, 1] = np.mean(y[triangles], axis=1) maxi = dns_path.contains_points(x_y_triangles) self.triangulation.set_mask(np.invert(maxi)) - return self.triangulation def get_changing_indexes(self): """ @@ -131,6 +223,11 @@ def get_changing_indexes(self): del basis_indexes[projection_dims.index(min(projection_dims))] return basis_indexes + def get_crystal_axis_names(self): + changing_index = self.get_changing_indexes() + name_dict = {0: "h (r.l.u)", 1: "k (r.l.u)", 2: "l (r.l.u)"} + return name_dict[changing_index[0]], name_dict[changing_index[1]] + def get_changing_hkl_components(self): index = self.get_changing_indexes() hkl1 = get_hkl_float_array(self.hkl1) diff --git a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_helpers.py b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_helpers.py index 5a1e58a6842b..47eeaaefed65 100644 --- a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_helpers.py +++ b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_helpers.py @@ -9,6 +9,7 @@ Helper functions for DNS single crystal elastic calculations. """ +import numpy as np from numpy import asarray, cos, pi, radians, sin @@ -23,3 +24,123 @@ def angle_to_q(two_theta, omega, wavelength): # should work with np arrays qx = (cos(-w) - cos(-w + tt)) * two_pi_over_lambda qy = (sin(-w) - sin(-w + tt)) * two_pi_over_lambda return qx, qy + + +def filter_flattened_meshes(x, y, z, limits): + if x is None: + return [y, y, z] + min_x, max_x, min_y, max_y = limits + x = x.flatten() + y = y.flatten() + z = z.flatten() + filtered = np.logical_and(np.logical_and(min_x < x, x < max_x), np.logical_and(min_y < y, y < max_y)) + x = x[filtered] + y = y[filtered] + z = z[filtered] + return [x, y, z] + + +def get_z_min_max(z, xlim=None, ylim=None, plot_x=None, plot_y=None): + flatten_z = z.flatten() + if xlim is not None and ylim is not None: + flatten_plot_y = plot_y.flatten() + flatten_plot_x = plot_x.flatten() + flatten_z = flatten_z[ + np.logical_and( + np.logical_and(xlim[1] > flatten_plot_x, xlim[0] < flatten_plot_x), + np.logical_and(ylim[1] > flatten_plot_y, ylim[0] < flatten_plot_y), + ) + ] + if flatten_z.size != 0: + z_max = flatten_z.max() + z_min = flatten_z.min() + pz_min = min((i for i in flatten_z if i > 0), default=0) + else: + z_max = 0 + z_min = 0 + pz_min = 0 + return z_min, z_max, pz_min + + +def get_hkl_intensity_from_cursor(single_crystal_map, plot_settings, x, y): + hkl1 = single_crystal_map.hkl1.split(",") + hkl2 = single_crystal_map.hkl2.split(",") + dx = single_crystal_map.dx + dy = single_crystal_map.dy + interpolation_on = bool(plot_settings["interpolate"]) + plot_type = plot_settings["plot_type"] + + if plot_settings["type"] == "angular": # two_theta omega + qx, qy = angle_to_q(two_theta=x, omega=y, wavelength=single_crystal_map.wavelength) + hklx = hkl_to_hklx(hkl1, qx, dx) + hkly = hkl_to_hklx(hkl2, qy, dy) + elif plot_settings["type"] == "qxqy": # qx qy + qx, qy = x, y + hklx = hkl_to_hklx(hkl1, qx, dx) + hkly = hkl_to_hklx(hkl2, qy, dy) + elif plot_settings["type"] == "hkl": # hkl + hklx = hkl_to_hklx(hkl1, x=x) + hkly = hkl_to_hklx(hkl2, x=y) + hkl = hkl_xy_to_hkl(hklx, hkly) + + if plot_type == "triangulation": + z_per_triangle = single_crystal_map.z_face + trifinder = single_crystal_map.triangulation.get_trifinder() + pos_q = trifinder(x, y) + z = z_per_triangle.flatten()[pos_q] + else: # quadmesh or scatterplot + # in case of triangulation, axis switch is already included when mesh is created + if plot_settings["switch"]: + x, y = y, x + if plot_settings["type"] == "angular": # two_theta omega + pos_q = closest_mesh_point(single_crystal_map.angular_mesh[0], single_crystal_map.angular_mesh[1], x, y) + z = single_crystal_map.angular_mesh[2].flatten()[pos_q] + elif plot_settings["type"] == "qxqy": # qx qy + pos_q = closest_mesh_point(single_crystal_map.qxqy_mesh[0], single_crystal_map.qxqy_mesh[1], x, y) + z = single_crystal_map.qxqy_mesh[2].flatten()[pos_q] + elif plot_settings["type"] == "hkl": # hkl + pos_q = closest_mesh_point(single_crystal_map.hkl_mesh[0], single_crystal_map.hkl_mesh[1], x, y) + z = single_crystal_map.hkl_mesh[2].flatten()[pos_q] + + if interpolation_on or plot_type == "triangulation": + # Errors are undefined on interpolated points. In addition, when triangulation is on + # then the original points are not used and the errors cannot be assigned. + error = None + else: + error = single_crystal_map.error_mesh.flatten()[pos_q] + + return [hkl[0], hkl[1], hkl[2], z, error] + + +# functions used by get_hkl_intensity_from_cursor() +def closest_mesh_point(x_mesh, y_mesh, x, y): + closest_point = np.add(np.square(x_mesh - x), np.square(y_mesh - y)).argmin() + return closest_point + + +def hkl_to_hklx(hkl, q=None, d=None, x=None): + if d is None: + return [x * float(a) for a in hkl] + return [q_to_hkl_xy(q, d) * float(a) for a in hkl] + + +def hkl_xy_to_hkl(hklx, hkly): + return [hklx[0] + hkly[0], hklx[1] + hkly[1], hklx[2] + hkly[2]] + + +def hkl_xy_to_q(hkl, d): + return hkl / d * 2.0 * np.pi + + +def q_to_hkl_xy(q, d): + return q * d / 2.0 / np.pi + + +def get_projection(x, z): + # use numpy's implementation of the optimal bin width calculation + bin_edges = np.histogram_bin_edges(x, bins="auto") + numpoints = bin_edges.size - 1 + # use weights to sum up intensity values corresponding to the same bin + i_projection, x_bin_edges = np.histogram(x, numpoints, weights=z) + x_bin_centers = (x_bin_edges[:-1] + x_bin_edges[1:]) / 2.0 + return x_bin_centers, i_projection diff --git a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot.ui b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot.ui index 8d38d662d7e1..57aca7435ec4 100644 --- a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot.ui +++ b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot.ui @@ -30,7 +30,7 @@ - + Navigate up to the next workspace previous dataset @@ -39,6 +39,9 @@ + + Navigate down to the next workspace + next dataset @@ -87,7 +90,7 @@ - Toggle grid lines + Switch between different grid sizes grid @@ -97,7 +100,7 @@ - Select the grid on unit cell principal axes + Switch between orthogonal and crystallographic planes crystal_axes @@ -145,67 +148,145 @@ - + - X + X<sub>min<sub> - - - - 0 - 0 - + + + -360.000000000000000 + + + 360.000000000000000 + + + 0.100000000000000 + + -2.000000000000000 + + + + + - + X<sub>max<sub> - + + + -360.000000000000000 + + + 360.000000000000000 + + + 0.100000000000000 + + + 2.000000000000000 + + + + + - Y + Y<sub>min<sub> - - - - 0 - 0 - + + + -360.000000000000000 + + + 360.000000000000000 + + + 0.100000000000000 + + + -2.000000000000000 + + + + - + Y<sub>max<sub> - + + + -360.000000000000000 + + + 360.000000000000000 + + + 0.100000000000000 + + + 2.000000000000000 + + + + + - Z + I<sub>min<sub> - - - - 0 - 0 - + + + -999.000000000000000 + + + 100000.000000000000000 + + + 1.000000000000000 + + QAbstractSpinBox::AdaptiveDecimalStepType + + + + + - + I<sub>max<sub> + + + + + + + 100000.000000000000000 + + + QAbstractSpinBox::AdaptiveDecimalStepType + + + 100.000000000000000 + + Turn on/off logarithmic scale + log @@ -216,6 +297,9 @@ + + Select colorbar style + 100 @@ -226,6 +310,9 @@ + + Invert selected colormap + invert @@ -236,6 +323,9 @@ + + Change font size of the plot legend + FontSize: diff --git a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_menu.py b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_menu.py index e508828f5bf2..fb507fa81496 100644 --- a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_menu.py +++ b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_menu.py @@ -59,16 +59,38 @@ def __init__(self): self.addMenu(self._menu_axes) self._menu_interpolation = InterpolationMenu(self) self.addMenu(self._menu_interpolation) - self.menus = [self._menu_plot_type, self._menu_axes, self._menu_interpolation] + self._action_gouraud = self.addAction("Gouraud Shading") + self._action_gouraud.setCheckable(True) + self._menu_zoom = ZoomMenu(self) + self.addMenu(self._menu_zoom) + self.menus = [self._menu_plot_type, self._menu_axes, self._menu_interpolation, self._menu_zoom] + # connecting signals + self._action_gouraud.triggered.connect(self._gouraud_changed) sig_replot = Signal(str) + sig_switch_state_changed = Signal() + sig_axes_changed = Signal() - def get_value(self): - plot_type = self._menu_plot_type.get_value() - axis_type = self._menu_axes.get_value() - axis_type["interpolate"] = self._menu_interpolation.get_value() - axis_type["plot_type"] = plot_type - return axis_type + def _get_shading(self): + if self._action_gouraud.isChecked(): + return "gouraud" + return "flat" + + def get_plot_view_settings(self): + axis_dict = self._menu_axes.get_axis_settings() + plot_view_dict = axis_dict + plot_view_dict["shading"] = self._get_shading() + plot_view_dict["plot_type"] = self._menu_plot_type.get_plot_type() + plot_view_dict["interpolate"] = self._menu_interpolation.get_interpolation_setting_index() + plot_view_dict["zoom"] = self._menu_zoom.get_value() + return plot_view_dict + + def _gouraud_changed(self): + self.sig_replot.emit("gouraud") + + def set_interpolation_menu_options(self): + plot_type = self._menu_plot_type.get_plot_type() + self._menu_interpolation.change_interpolation_menu(plot_type) class PlotTypeMenu(QMenu): @@ -77,17 +99,36 @@ def __init__(self, parent): self.parent = parent # adding action action_triangulation_mesh = self.addAction("Triangulation") + action_quad_mesh = self.addAction("Quadmesh (like in dnsplot)") + action_scatter_mesh = self.addAction("Scatter") + # setting checkable action_triangulation_mesh.setCheckable(True) - action_triangulation_mesh.setChecked(True) + action_quad_mesh.setCheckable(True) + action_scatter_mesh.setCheckable(True) + action_quad_mesh.setChecked(True) # action group - p_tag = QActionGroup(self) - p_tag.addAction(action_triangulation_mesh) - self.p_tag = p_tag + plot_type_action_group = QActionGroup(self) + plot_type_action_group.addAction(action_triangulation_mesh) + plot_type_action_group.addAction(action_quad_mesh) + plot_type_action_group.addAction(action_scatter_mesh) + self.plot_type_action_group = plot_type_action_group self.action_triangulation_mesh = action_triangulation_mesh + self.action_quad_mesh = action_quad_mesh + self.action_scatter = action_scatter_mesh + # connecting signals + self.plot_type_action_group.triggered.connect(self._type_changed) - def get_value(self): - index = self.p_tag.actions().index(self.p_tag.checkedAction()) - plot_type_list = {0: "triangulation"} + def _type_changed(self): + self.parent.set_interpolation_menu_options() + self.parent.sig_replot.emit("type") + + def set_type(self, ptype): + plot_type_list = {"triangulation": "action_triangulation_mesh", "quadmesh": "action_quad_mesh", "scatter": "action_scatter"} + getattr(self, plot_type_list[ptype]).setChecked(True) + + def get_plot_type(self): + index = self.plot_type_action_group.actions().index(self.plot_type_action_group.checkedAction()) + plot_type_list = {0: "triangulation", 1: "quadmesh", 2: "scatter"} return plot_type_list[index] @@ -95,23 +136,49 @@ class AxesMenu(QMenu): def __init__(self, parent): super().__init__("Axes") self.parent = parent + action_two_theta_omega = self.addAction("(2\u03b8, \u03c9)") + action_qxqy = self.addAction("(q_x, q_y)") action_hkl = self.addAction("(n_x, n_y)") self.addSeparator() + action_fix_aspect = self.addAction("Fix Aspect Ratio") + action_switch_axis = self.addAction("Switch Axes") # setting checkable and standard option checked + action_two_theta_omega.setCheckable(True) + action_qxqy.setCheckable(True) + action_switch_axis.setCheckable(True) + action_fix_aspect.setCheckable(True) action_hkl.setCheckable(True) action_hkl.setChecked(True) # action group - qag = QActionGroup(self) - qag.addAction(action_hkl) - qag.setExclusive(True) + axes_action_group = QActionGroup(self) + axes_action_group.addAction(action_two_theta_omega) + axes_action_group.addAction(action_qxqy) + axes_action_group.addAction(action_hkl) + axes_action_group.setExclusive(True) # connect Signals - self.qag = qag + self.axes_action_group = axes_action_group + self.action_switch_axis = action_switch_axis + self.action_fix_aspect = action_fix_aspect + self.axes_action_group.triggered.connect(self._axis_type_changed) + self.action_fix_aspect.triggered.connect(self._fix_aspect_changed) + self.action_switch_axis.triggered.connect(self._switch_axis_changed) - def get_value(self): - axis_list = {0: "hkl"} - index = self.qag.actions().index(self.qag.checkedAction()) + def _axis_type_changed(self): + self.parent.sig_axes_changed.emit() + + def _fix_aspect_changed(self): + self.parent.sig_replot.emit("fix_aspect") + + def _switch_axis_changed(self): + self.parent.sig_switch_state_changed.emit() + + def get_axis_settings(self): + axis_list = {0: "angular", 1: "qxqy", 2: "hkl"} + index = self.axes_action_group.actions().index(self.axes_action_group.checkedAction()) axis_type = axis_list[index] - return {"type": axis_type, "switch": False, "fix_aspect": False} + switch = self.action_switch_axis.isChecked() + fix_aspect = self.action_fix_aspect.isChecked() + return {"type": axis_type, "switch": switch, "fix_aspect": fix_aspect} class InterpolationMenu(QMenu): @@ -120,14 +187,73 @@ def __init__(self, parent): self.parent = parent # adding actions action_interpolation_off = self.addAction("Off") + # default values for triangulation interpolation + action_interpolation_1 = self.addAction("1 -> 4") + action_interpolation_2 = self.addAction("1 -> 9 (like in dnsplot)") + action_interpolation_3 = self.addAction("1 -> 16") # setting checkable and check standard option action_interpolation_off.setCheckable(True) - action_interpolation_off.setChecked(True) + action_interpolation_1.setCheckable(True) + action_interpolation_2.setCheckable(True) + action_interpolation_3.setCheckable(True) + action_interpolation_2.setChecked(True) # action group - i_pag = QActionGroup(self) - i_pag.addAction(action_interpolation_off) - i_pag.setExclusive(True) - self.i_pag = i_pag + interpolation_action_group = QActionGroup(self) + interpolation_action_group.addAction(action_interpolation_off) + interpolation_action_group.addAction(action_interpolation_1) + interpolation_action_group.addAction(action_interpolation_2) + interpolation_action_group.addAction(action_interpolation_3) + interpolation_action_group.setExclusive(True) + self.action_interpolation_off = action_interpolation_off + self.action_interpolation_1 = action_interpolation_1 + self.action_interpolation_2 = action_interpolation_2 + self.action_interpolation_3 = action_interpolation_3 + self.interpolation_action_group = interpolation_action_group + self.interpolation_action_group.triggered.connect(self._interpolation_changed) + + def _interpolation_changed(self): + self.parent.sig_replot.emit("interpolation") + + def set_intp(self, interpolation): + getattr(self, f"action_interpolation_{interpolation}").setChecked(True) + + def change_interpolation_menu(self, plot_type): + if plot_type == "scatter": + self.action_interpolation_off.setChecked(True) + self.setEnabled(plot_type != "scatter") + self._set_interpolation_options(plot_type) + + def _set_interpolation_options(self, plot_type): + if plot_type == "triangulation": + labels = ["1 -> 4 (like in dnsplot)", "1 -> 16 (slow)", "1 -> 64 (very slow)"] + else: # quadmesh + labels = ["1 -> 4", "1 -> 9 (like in dnsplot)", "1 -> 16"] + self.action_interpolation_1.setText(labels[0]) + self.action_interpolation_2.setText(labels[1]) + self.action_interpolation_3.setText(labels[2]) + + def get_interpolation_setting_index(self): + return self.interpolation_action_group.actions().index(self.interpolation_action_group.checkedAction()) + + +class ZoomMenu(QMenu): + def __init__(self, parent): + super().__init__("Synchronize Zooming") + self.parent = parent + self.zoom = {"fix_xy": False, "fix_z": False} + self.action_xy_zoom = self.addAction("x and y") + self.action_xy_zoom.setCheckable(True) + self.action_z_zoom = self.addAction("z (intensity)") + self.action_z_zoom.setCheckable(True) + # connect Signals + self.action_xy_zoom.triggered.connect(self._sync_zoom_changed) + self.action_z_zoom.triggered.connect(self._sync_zoom_changed) + + def _sync_zoom_changed(self): + xy = self.action_xy_zoom.isChecked() + z = self.action_z_zoom.isChecked() + self.zoom = {"fix_xy": xy, "fix_z": z} + self.parent.sig_replot.emit("zoom") def get_value(self): - return self.i_pag.actions().index(self.i_pag.checkedAction()) + return self.zoom diff --git a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_model.py b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_model.py index 8d9fccfe518c..65be327a7703 100644 --- a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_model.py +++ b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_model.py @@ -1,3 +1,4 @@ +# ruff: noqa: E741 # Ambiguous variable name # Mantid Repository : https://github.com/mantidproject/mantid # # Copyright © 2023 ISIS Rutherford Appleton Laboratory UKRI, @@ -9,14 +10,19 @@ DNS single crystal elastic plot tab model of DNS reduction GUI. """ +import numpy as np +import mantidqtinterfaces.dns_single_crystal_elastic.plot.elastic_single_crystal_helpers as helper from mantidqtinterfaces.dns_powder_tof.data_structures.dns_obs_model import DNSObsModel from mantidqtinterfaces.dns_single_crystal_elastic.data_structures.dns_single_crystal_map import DNSScMap from mantidqtinterfaces.dns_powder_tof.data_structures.object_dict import ObjectDict +from mantidqtinterfaces.dns_single_crystal_elastic.plot.elastic_single_crystal_helpers import get_hkl_intensity_from_cursor class DNSElasticSCPlotModel(DNSObsModel): """ - Model for DNS plot calculations. + Model for DNS plot calculations. It converts data from (omega, 2theta) + space into (q_x, q_y) and (n_x, n_y). Also, creates hull of DNS data + to filter points. """ def __init__(self, parent): @@ -27,11 +33,13 @@ def __init__(self, parent): self._data.x = None self._data.y = None self._data.z = None + # x_, y_, and z_lims are used for storing full data (default) lims for plotting + self._data.x_lims = None + self._data.y_lims = None + self._data.z_lims = None self._data.z_min = None self._data.z_max = None self._data.pz_min = None - self._data.triang = None - self._data.z_triang = None def create_single_crystal_map(self, data_array, options, initial_values=None): two_theta = data_array["two_theta_array"] @@ -51,32 +59,135 @@ def create_single_crystal_map(self, data_array, options, initial_values=None): self._single_crystal_map = DNSScMap(parameter=parameter, two_theta=two_theta, omega=omega, z_mesh=z_mesh, error_mesh=error) return self._single_crystal_map - def get_interpolated_triangulation(self, interpolate, axis_type, switch): + def has_data(self): + return self._data.x is not None + + def get_projections(self, xlim, ylim): + limits = np.append(xlim, ylim) + x, y, z = helper.filter_flattened_meshes(self._data.x, self._data.y, self._data.z, limits) + x_projection = helper.get_projection(x, z) + y_projection = helper.get_projection(y, z) + return x_projection, y_projection + + def generate_triangulation_mesh(self, interpolate, axis_type, switch): mesh_name = axis_type + "_mesh" self._single_crystal_map.triangulate(mesh_name=mesh_name, switch=switch) - self._single_crystal_map.mask_triangles(mesh_name=mesh_name) - triangulator_refiner, z_refiner = self._single_crystal_map.interpolate_triangulation(interpolate) - self._data.triang = triangulator_refiner - self._data.z_triang = z_refiner - # this is important to get the limits - x, y, z = getattr(self._single_crystal_map, mesh_name) - self._data.x = x - self._data.y = y - self._data.z = z - return triangulator_refiner, z_refiner + self._single_crystal_map.mask_triangles(mesh_name=mesh_name, switch=switch) + self._single_crystal_map.interpolate_triangulation(interpolate) + triangulation_refined = self._single_crystal_map.triangulation + z_refined = self._single_crystal_map.z_mesh + z_face = self._single_crystal_map.z_face + x_face = self._single_crystal_map.x_face + y_face = self._single_crystal_map.y_face + self.set_mesh_data(x_face.flatten(), y_face.flatten(), z_face.flatten()) + return triangulation_refined, z_refined, z_face + + def generate_quad_mesh(self, interpolate, axis_type, switch): + if interpolate: + self._single_crystal_map.interpolate_quad_mesh(interpolate) + x, y, z = getattr(self._single_crystal_map, axis_type + "_mesh") + x, y, z = self.switch_axis(x, y, z, switch) + self.set_mesh_data(x.flatten(), y.flatten(), z.flatten()) + return x, y, z + + def generate_scatter_mesh(self, axis_type, switch): + x, y, z = getattr(self._single_crystal_map, axis_type + "_mesh") + x, y, z = self.switch_axis(x, y, z, switch) + self.set_mesh_data(x.flatten(), y.flatten(), z.flatten()) + return x, y, z + + def get_dx_dy_ratio(self): + return self._single_crystal_map.dx / self._single_crystal_map.dy + + def get_aspect_ratio(self, plot_settings_dict): + if plot_settings_dict["fix_aspect"]: + if plot_settings_dict["type"] == "hkl": + ratio = self.get_dx_dy_ratio() + return ratio + return 1 + return "auto" def get_axis_labels(self, axis_type, crystal_axes, switch=False): + if crystal_axes: + return self._single_crystal_map.get_crystal_axis_names() hkl1 = self._single_crystal_map.hkl1 hkl2 = self._single_crystal_map.hkl2 - axis_labels = {"hkl": [f"[{hkl1}] (r.l.u.)", f"[{hkl2}] (r.l.u.)"]} + axis_labels = { + "angular": ["2\u03b8 (deg)", "\u03c9 (deg)"], + "qxqy": [r"$q_{x} \ (\AA^{-1})$", r"$q_{y} \ (\AA^{-1})$"], + "hkl": [f"[{hkl1}] (r.l.u.)", f"[{hkl2}] (r.l.u.)"], + } labels = axis_labels[axis_type] + if switch: + labels.reverse() return labels def get_changing_hkl_components(self): return self._single_crystal_map.get_changing_hkl_components() + def get_format_coord(self, plot_settings_dict, switch=False): + # adds z and hkl label to cursor position + def format_coord(x, y): + mesh_name = plot_settings_dict["type"] + "_mesh" + border_path = self._single_crystal_map.get_dns_map_border(mesh_name, switch) + h, k, l, z, error = get_hkl_intensity_from_cursor(self._single_crystal_map, plot_settings_dict, x, y) + # ensures empty hover in the region outside the data boundary + if border_path.contains_point((x, y)): + if error is None: + return f"x={x:2.3f}, y={y:2.3f}, hkl=({h:2.2f}, {k:2.2f}, {l:2.2f}), Intensity={z:6.4f}" + return f"x={x:2.3f}, y={y:2.3f}, hkl=({h:2.2f}, {k:2.2f}, {l:2.2f}), Intensity={z:6.4f}±{error:6.4f}" + return f"x={x:2.3f}, y={y:2.3f}, hkl=({h:2.2f}, {k:2.2f}, {l:2.2f})" + + return format_coord + + def get_data_z_min_max(self, xlim=None, ylim=None): + return helper.get_z_min_max(self._data.z, xlim, ylim, self._data.x, self._data.y) + + def get_data_xy_lim(self, switch): + limits = [[min(self._data.x.flatten()), max(self._data.x.flatten())], [min(self._data.y.flatten()), max(self._data.y.flatten())]] + if switch: + limits.reverse() + return limits + + @staticmethod + def switch_axis(x, y, z, switch): + if switch: # switch x and y axes + nx = np.transpose(y) + ny = np.transpose(x) + nz = np.transpose(z) + return nx, ny, nz + return x, y, z + def get_omega_offset(self): return self._single_crystal_map["omega_offset"] def get_dx_dy(self): return self._single_crystal_map["dx"], self._single_crystal_map["dy"] + + def prepare_data_for_saving(self): + x = self._data.x.flatten() + y = self._data.y.flatten() + z = self._data.z.flatten() + data_combined = np.array(list(zip(x, y, z))) + return data_combined + + def set_mesh_data(self, x, y, z): + self._data.x = x + self._data.y = y + self._data.z = z + self.save_default_data_lims() + + def save_default_data_lims(self): + x_lims, y_lims = self.get_data_xy_lim(switch=False) + z_min, z_max, pos_z_min = self.get_data_z_min_max() + # add 5% padding to comply with default plotting settings + x0, x1 = x_lims[0], x_lims[1] + y0, y1 = y_lims[0], y_lims[1] + dx = (x1 - x0) * 0.05 + dy = (y1 - y0) * 0.05 + self._data.x_lims = [x0 - dx, x1 + dx] + self._data.y_lims = [y0 - dy, y1 + dy] + self._data.z_lims = [z_min, z_max] + + def get_default_data_lims(self): + return self._data.x_lims, self._data.y_lims, self._data.z_lims diff --git a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_plot.py b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_plot.py index 980b5e7d5f9c..f4a42506077d 100644 --- a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_plot.py +++ b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_plot.py @@ -9,8 +9,10 @@ DNS single crystal elastic plot tab of DNS reduction GUI. """ +import numpy as np import matplotlib from matplotlib.ticker import AutoMinorLocator, NullLocator +from mpl_toolkits.axes_grid1 import make_axes_locatable from mpl_toolkits.axisartist import Subplot @@ -28,9 +30,6 @@ def __init__(self, parent, figure, grid_helper): self._colorbar = None self._ax_hist = [None, None] - def on_resize(self, _dummy=None): # connected to canvas resize - self._fig.tight_layout(pad=0.3) - @staticmethod def set_fontsize(fontsize): matplotlib.rcParams.update({"font.size": fontsize}) @@ -41,20 +40,21 @@ def create_colorbar(self): def set_norm(self, norm): self._plot.set_norm(norm) + def set_linecolor(self, lines): + lines = ["face", "white", "black"][lines] + if self._plot is not None: + self._plot.set_edgecolor(lines) + + def set_pointsize(self, lines): + if self._plot is not None: + a = np.ones(len(self._plot.get_sizes())) + size = a * [10, 50, 100, 500, 1000][lines] + self._plot.set_sizes(size) + def set_shading(self, shading): if self._plot is not None: self._plot.set_shading(shading) - def set_grid(self, major=False, minor=False): - if minor: - self._ax.xaxis.set_minor_locator(AutoMinorLocator(5)) - else: - self._ax.xaxis.set_minor_locator(NullLocator()) - if major: - self._ax.grid(True, which="both", zorder=1000, linestyle="--") - else: - self._ax.grid(0) - def set_zlim(self, zlim): if self._plot is not None: self._plot.set_clim(zlim[0], zlim[1]) @@ -68,6 +68,89 @@ def set_axis_labels(self, x_label, y_label): self._ax.set_xlabel(x_label) self._ax.set_ylabel(y_label) - def plot_triangulation(self, triang, z, cmap, edge_colors, shading): + def set_format_coord(self, format_coord): + self._ax.format_coord = format_coord + + def set_xlim(self, xlim): + if xlim[0] is None: + self._ax.autoscale(axis="x") + else: + self._ax.set_xlim(left=xlim[0], right=xlim[1], auto=True) + + def set_ylim(self, ylim): + if ylim[0] is None: + self._ax.autoscale(axis="y") + else: + self._ax.set_ylim(bottom=ylim[0], top=ylim[1], auto=True) + + def set_grid(self, major=False, minor=False): + if minor: + self._ax.xaxis.set_minor_locator(AutoMinorLocator(5)) + else: + self._ax.xaxis.set_minor_locator(NullLocator()) + if major: + self._ax.grid(True, which="both", zorder=1000, linestyle="--") + else: + self._ax.grid(0) + + def set_aspect_ratio(self, aspect_ratio): + self._ax.set_aspect(aspect_ratio, anchor="NW") + + # projections + def set_projections(self, x_proj, y_proj, xlim, ylim): + self.remove_projections() + divider = make_axes_locatable(self._ax) + self._ax_hist[0] = divider.append_axes("top", 1.2, pad=0.1, sharex=self._ax) + self._ax_hist[1] = divider.append_axes("right", 1.2, pad=0.1, sharey=self._ax) + self._ax.set_xlim(xlim[0], xlim[1]) + self._ax.set_ylim(ylim[0], ylim[1]) + self._ax.set_xmargin(0) + self._ax.set_ymargin(0) + self._ax_hist[0].set_xmargin(0) + self._ax_hist[1].set_ymargin(0) + self._ax_hist[0].axis["bottom"].major_ticklabels.set_visible(False) + self._ax_hist[1].axis["left"].major_ticklabels.set_visible(False) + self._ax_hist[0].plot(x_proj[0], x_proj[1]) + self._ax_hist[1].plot(y_proj[1], y_proj[0]) + + def remove_projections(self): + if hasattr(self, "_ax_hist") and self._ax_hist[0] is not None: + try: + self._ax_hist[0].remove() + self._ax_hist[1].remove() + except KeyError: + pass + + # getting stuff + def get_active_limits(self): + xlim = self._ax.get_xlim() + ylim = self._ax.get_ylim() + return xlim, ylim + + # plotting + def clear_plot(self): + if self._ax: + self._ax.figure.clear() + + def plot_triangulation(self, triang, z, z_face, cmap, edge_colors, shading): + self._ax.set_visible(True) + if shading == "flat": + self._plot = self._ax.tripcolor(triang, facecolors=z_face, cmap=cmap, edgecolors=edge_colors, shading=shading) + else: # "gouraud" shading + self._plot = self._ax.tripcolor(triang, z, cmap=cmap, edgecolors=edge_colors, shading=shading) + + def plot_quadmesh(self, x, y, z, cmap, edge_colors, shading): + # pylint: disable=too-many-arguments + self._ax.set_visible(True) + self._plot = self._ax.pcolormesh(x, y, z, cmap=cmap, edgecolors=edge_colors, shading=shading) + # set 5% padding to pcolormesh, similar to default values in tripcolor and scatter plots + x0, x1 = self._ax.get_xlim() + y0, y1 = self._ax.get_ylim() + dx = (x1 - x0) * 0.05 + dy = (y1 - y0) * 0.05 + self._ax.set_xlim(x0 - dx, x1 + dx) + self._ax.set_ylim(y0 - dy, y1 + dy) + + def plot_scatter(self, x, y, z, cmap): self._ax.set_visible(True) - self._plot = self._ax.tripcolor(triang, z, cmap=cmap, edgecolors=edge_colors, shading=shading) + self._plot = self._ax.scatter(x, y, c=z, s=100, cmap=cmap, zorder=100) diff --git a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_presenter.py b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_presenter.py index 28a7d0c7ac7c..8b69d3f7c646 100644 --- a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_presenter.py +++ b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_presenter.py @@ -9,10 +9,12 @@ DNS single crystal elastic plot tab presenter of DNS reduction GUI. """ +import numpy as np from mantidqtinterfaces.dns_powder_tof.data_structures.dns_observer import DNSObserver from mantidqtinterfaces.dns_powder_tof.data_structures.object_dict import ObjectDict from mantidqtinterfaces.dns_single_crystal_elastic.plot import mpl_helpers from mantidqtinterfaces.dns_single_crystal_elastic.plot.grid_locator import get_grid_helper +from mantid.simpleapi import logger class DNSElasticSCPlotPresenter(DNSObserver): @@ -27,6 +29,29 @@ def __init__(self, name=None, parent=None, view=None, model=None): self._plot_param.colormap_name = "jet" self._plot_param.font_size = 1 self._plot_param.lines = 0 + self._plot_param.pointsize = 2 + self._plot_param.xlim = [None, None] + self._plot_param.ylim = [None, None] + self._plot_param.zlim = [None, None] + self._plot_param.projections = False + self._plot_param.use_default_lims = True + self._plot_param.set_zlims = True + + def _toggle_projections(self, proj_on): + self._plot_param.projections = proj_on + if proj_on and self.model.has_data(): + x_proj, y_proj = self._calculate_projections() + xlim, ylim, dummy_z = self._get_current_spinners_lims() + self.view.single_crystal_plot.set_projections(x_proj, y_proj, xlim, ylim) + self.view.draw() + else: + self.view.single_crystal_plot.remove_projections() + self._plot(self.view.initial_values) + + def _calculate_projections(self): + xlim, ylim = self.view.single_crystal_plot.get_active_limits() + x_proj, y_proj = self.model.get_projections(xlim, ylim) + return x_proj, y_proj def _datalist_updated(self, workspaces): compare = self.view.datalist.get_datalist() @@ -36,7 +61,10 @@ def _datalist_updated(self, workspaces): ) # check is necessary for simulation def _plot(self, initial_values=None): - axis_type = self.view.get_axis_type() + """ + It is called every time any of the plot view options is changed + or another polarization channel is selected for plotting. + """ plot_list = self.view.datalist.get_checked_plots() if not plot_list: return @@ -45,18 +73,59 @@ def _plot(self, initial_values=None): generated_dict = self.param_dict["elastic_single_crystal_script_generator"] data_array = generated_dict["data_arrays"][self._plot_param.plot_name] options = self.param_dict["elastic_single_crystal_options"] - self.model.create_single_crystal_map(data_array, options, initial_values) + single_crystal_map = self.model.create_single_crystal_map(data_array, options, initial_values) + self._determine_plot_type_options(single_crystal_map) self._change_grid_state(draw=False, change=False) self.view.create_subfigure(self._plot_param.grid_helper) - self._want_plot(axis_type["plot_type"]) + plot_type = self.view.get_plotting_setting("plot_type") + self._want_plot(plot_type) self._set_plotting_grid(self._crystallographical_axes()) + self._set_aspect_ratio() + self._set_ax_formatter() self._set_axis_labels() self.view.single_crystal_plot.create_colorbar() - self.view.single_crystal_plot.on_resize() self._set_initial_omega_offset_dx_dy() + def_xlim, def_ylim, def_zlim = self.model.get_default_data_lims() + xlim, ylim, zlim = self._get_current_spinners_lims() + if self._plot_param.use_default_lims: + self._set_plot_param_lims(def_xlim, def_ylim, def_zlim, include_zlim=self._plot_param.set_zlims) + else: + if self._plot_param.set_zlims: + # use default value for zlim in case of xy-zooming sync + self._set_plot_param_lims(xlim, ylim, def_zlim, include_zlim=self._plot_param.set_zlims) + else: + self._set_plot_param_lims(xlim, ylim, zlim, include_zlim=self._plot_param.set_zlims) + self._set_spinners_lims(include_zlim=self._plot_param.set_zlims) + self._set_plotting_lims(include_zlim=self._plot_param.set_zlims) + if self._plot_param.projections: + self._toggle_projections(proj_on=True) + self._set_log() self.view.canvas.figure.tight_layout() self.view.draw() + def _set_plot_param_lims(self, xlim, ylim, zlim, include_zlim=True): + self._plot_param.xlim = xlim + self._plot_param.ylim = ylim + if include_zlim: + self._plot_param.zlim = zlim + + def _update_plot_param_lims(self, include_zlim=True): + xlim, ylim = self.view.single_crystal_plot.get_active_limits() + self._plot_param.xlim = xlim + self._plot_param.ylim = ylim + if include_zlim: + zlim = self.model.get_data_z_min_max(xlim, ylim) + self._plot_param.zlim = zlim + + def _set_plotting_lims(self, include_zlim=True): + self.view.single_crystal_plot.set_xlim(self._plot_param.xlim) + self.view.single_crystal_plot.set_ylim(self._plot_param.ylim) + if include_zlim: + self.view.single_crystal_plot.set_zlim(self._plot_param.zlim) + + def process_auto_reduction_request(self): + self.view.single_crystal_plot.clear_plot() + def tab_got_focus(self): workspaces = sorted(self.param_dict["elastic_single_crystal_script_generator"]["plot_list"]) if self._datalist_updated(workspaces): @@ -65,6 +134,15 @@ def tab_got_focus(self): self.view.process_events() self.view.datalist.check_first() + def _set_zooming_data(self, include_zlim=False): + self._update_plot_param_lims(include_zlim) + self._set_spinners_lims(include_zlim) + self._set_plotting_lims(include_zlim) + if self._plot_param.projections: + self._toggle_projections(proj_on=True) + # set this flag to false, not to update plot lims on a subsequent call of _plot() + self._plot_param.use_default_lims = False + def _set_initial_omega_offset_dx_dy(self): omega_offset = self.model.get_omega_offset() dx, dy = self.model.get_dx_dy() @@ -93,28 +171,59 @@ def _set_default_dx_dy(self): def _plot_triangulation(self, interpolate, axis_type, switch): color_map, edge_colors, shading = self._get_plot_styles() - triangulation, z = self.model.get_interpolated_triangulation(interpolate, axis_type, switch) - self.view.single_crystal_plot.plot_triangulation(triangulation, z, color_map, edge_colors, shading) + triangulation, z, z_face = self.model.generate_triangulation_mesh(interpolate, axis_type, switch) + self.view.single_crystal_plot.plot_triangulation(triangulation, z, z_face, color_map, edge_colors, shading) + + def _plot_quadmesh(self, interpolate, axis_type, switch): + color_map, edge_colors, shading = self._get_plot_styles() + if shading == "flat": # prevents dropping of line + shading = "nearest" + x, y, z = self.model.generate_quad_mesh(interpolate, axis_type, switch) + self.view.single_crystal_plot.plot_quadmesh(x, y, z, color_map, edge_colors, shading) + + def _plot_scatter(self, axis_type, switch): + color_map = self._get_plot_styles()[0] + x, y, z = self.model.generate_scatter_mesh(axis_type, switch) + self.view.single_crystal_plot.plot_scatter(x, y, z, color_map) def _want_plot(self, plot_type): - axis_type = self.view.get_axis_type() - self._plot_triangulation(axis_type["interpolate"], axis_type["type"], axis_type["switch"]) + plot_settings = self.view.get_plotting_settings_dict() + if plot_type == "triangulation": + self._plot_triangulation(plot_settings["interpolate"], plot_settings["type"], plot_settings["switch"]) + if plot_type == "quadmesh": + self._plot_quadmesh(plot_settings["interpolate"], plot_settings["type"], plot_settings["switch"]) + if plot_type == "scatter": + self._plot_scatter(plot_settings["type"], plot_settings["switch"]) def _get_plot_styles(self): own_dict = self.view.get_state() - shading = "flat" + shading = self.view.get_plotting_setting("shading") edge_colors = ["face", "white", "black"][self._plot_param.lines] colormap_name = own_dict["colormap"] if own_dict["invert_cb"]: colormap_name += "_r" - cmap = mpl_helpers.get_cmap(colormap_name) - return cmap, edge_colors, shading + color_map = mpl_helpers.get_cmap(colormap_name) + return color_map, edge_colors, shading def _set_axis_labels(self): - axis_type = self.view.get_axis_type() - x_label, y_label = self.model.get_axis_labels(axis_type["type"], self._crystallographical_axes()) + axis_type = self.view.get_plotting_setting("type") + switch = self.view.get_plotting_setting("switch") + x_label, y_label = self.model.get_axis_labels(axis_type, self._crystallographical_axes()) + if switch: + x_label, y_label = y_label, x_label self.view.single_crystal_plot.set_axis_labels(x_label, y_label) + def _set_aspect_ratio(self): + """ + If aspect ratio tick mark is selected: + a) sets aspect ratio to 1 for two_theta-omega and qx-qy planes; + b) sets aspect ratio to dx/dy for n_x-n_y plane. Useful when + the crystallographic plane is additionally activated. + """ + plot_settings = self.view.get_plotting_settings_dict() + ratio = self.model.get_aspect_ratio(plot_settings) + self.view.single_crystal_plot.set_aspect_ratio(ratio) + def _change_crystal_axes_grid(self): current_state = self._plot_param.grid_state % 4 if current_state == 0: @@ -150,18 +259,77 @@ def _change_crystal_axes(self): self._plot_param.grid_state = 1 else: self._plot_param.grid_state = 0 + self._plot(self.view.initial_values) + + def _change_line_style(self): + plot_type = self.view.get_plotting_setting("plot_type") + if plot_type == "scatter": + self._plot_param.pointsize = (self._plot_param.pointsize + 1) % 5 + self.view.single_crystal_plot.set_pointsize(self._plot_param.pointsize) + else: + self._plot_param.lines = (self._plot_param.lines + 1) % 3 + self.view.single_crystal_plot.set_linecolor(self._plot_param.lines) + self.view.draw() + + def _set_ax_formatter(self): + switch = self.view.get_plotting_setting("switch") + plot_settings = self.view.get_plotting_settings_dict() + format_coord = self.model.get_format_coord(plot_settings, switch) + self.view.single_crystal_plot.set_format_coord(format_coord) + + def _manual_lim_changed(self): + xlim, ylim, zlim = self._get_current_spinners_lims() + self._set_plot_param_lims(xlim, ylim, zlim) + self._set_plotting_lims() + self.view.draw() + # set this flag to false, not to update plot lims on a subsequent call of _plot() + self._plot_param.use_default_lims = False + + def _home_button_clicked(self): + self._plot_param.use_default_lims = True + self._plot_param.set_zlims = True self._plot() + def _save_data(self): + export_dir = self.param_dict["paths"]["export_dir"] + displayed_ws_name = self._plot_param.plot_name + export_file_name = f"{export_dir}/user_export_{displayed_ws_name}.csv" + displayed_data = self.model.prepare_data_for_saving() + column_headers = self._get_column_headers() + data_table = np.concatenate((column_headers, displayed_data), axis=0) + np.savetxt(export_file_name, data_table, delimiter=",", fmt="%s") + self.view.show_status_message(f"Displayed data have been saved to: {export_file_name}", 10, clear=True) + + def _get_column_headers(self): + axis_labels = self.view.get_plotting_settings_dict()["type"] + if axis_labels == "qxqy": + column_headers = np.array([["q_x (1/A)", "q_y (1/A)", "Intensity"]]) + elif axis_labels == "hkl": + column_headers = np.array([["n_x", "n_y", "Intensity"]]) + elif axis_labels == "angular": + column_headers = np.array([["2\u03b8 (deg)", "\u03c9 (deg)", "Intensity"]]) + return column_headers + def _create_grid_helper(self): - axis_type = self.view.get_axis_type() + switch = self.view.get_plotting_setting("switch") a, b, c, d = self.model.get_changing_hkl_components() - return get_grid_helper(self._plot_param.grid_helper, self._plot_param.grid_state, a, b, c, d, axis_type["switch"]) + return get_grid_helper(self._plot_param.grid_helper, self._plot_param.grid_state, a, b, c, d, switch) def _set_colormap(self): cmap = self._get_plot_styles()[0] self.view.single_crystal_plot.set_cmap(cmap) self.view.draw() + def _set_log(self): + log = self.view.get_state()["log_scale"] + xlim, ylim, zlim = self._get_current_spinners_lims() + dz_min, dz_max, dpz_min = self.model.get_data_z_min_max(xlim, ylim) + if log and zlim[0] < 0: + zlim[0] = dpz_min + norm = mpl_helpers.get_log_norm(log, zlim) + self.view.single_crystal_plot.set_norm(norm) + self.view.draw() + def _change_font_size(self, draw=True): own_dict = self.view.get_state() font_size = own_dict["fontsize"] @@ -169,20 +337,102 @@ def _change_font_size(self, draw=True): self._plot_param.font_size = font_size self.view.single_crystal_plot.set_fontsize(font_size) if draw: - self._plot() + self._plot(self.view.initial_values) def _crystallographical_axes(self): own_dict = self.view.get_state() return own_dict["crystal_axes"] + def _determine_plot_type_options(self, sc_map): + if not sc_map.rectangular_grid: + self.view.views_menu.menus[0].action_quad_mesh.setEnabled(False) + if self.view.views_menu.menus[0].action_quad_mesh.isChecked(): + logger.warning( + "Warning: quadmesh only possible on a rectangular grid. Selected data do not compose " + "a rectangular grid. The selected plot type is changed to triangulation." + ) + self.view.views_menu.menus[0].action_triangulation_mesh.setChecked(True) + else: + self.view.views_menu.menus[0].action_quad_mesh.setEnabled(True) + axes_type = self.view.get_plotting_setting("type") + if axes_type == "angular": + self.view._map["crystal_axes"].setChecked(False) + self.view._map["crystal_axes"].setEnabled(False) + self.view.views_menu.menus[1].action_fix_aspect.setChecked(False) + self.view.views_menu.menus[1].action_fix_aspect.setEnabled(False) + else: + self.view._map["crystal_axes"].setEnabled(True) + self.view.views_menu.menus[1].action_fix_aspect.setEnabled(True) + + def _get_current_spinners_lims(self): + own_dict = self.view.get_state() + xlim = [own_dict["x_min"], own_dict["x_max"]] + ylim = [own_dict["y_min"], own_dict["y_max"]] + zlim = [own_dict["z_min"], own_dict["z_max"]] + return xlim, ylim, zlim + + def _set_spinners_lims(self, include_zlim=True): + self.view._map["x_min"].setValue(self._plot_param.xlim[0]) + self.view._map["x_max"].setValue(self._plot_param.xlim[1]) + self.view._map["y_min"].setValue(self._plot_param.ylim[0]) + self.view._map["y_max"].setValue(self._plot_param.ylim[1]) + if include_zlim: + self.view._map["z_min"].setValue(self._plot_param.zlim[0]) + self.view._map["z_max"].setValue(self._plot_param.zlim[1]) + + def _switch_spinners_lims(self): + xlim, ylim, dummy = self._get_current_spinners_lims() + self.view._map["x_min"].setValue(ylim[0]) + self.view._map["x_max"].setValue(ylim[1]) + self.view._map["y_min"].setValue(xlim[0]) + self.view._map["y_max"].setValue(xlim[1]) + self._plot() + if self._plot_param.projections: + self._toggle_projections(proj_on=True) + + def _change_axes(self): + self._plot_param.use_default_lims = True + self._plot() + if self._plot_param.projections: + self._toggle_projections(proj_on=True) + + def _change_ws(self): + zoom_state_dict = self.view.get_plotting_setting("zoom") + z_sync = zoom_state_dict["fix_z"] + xy_sync = zoom_state_dict["fix_xy"] + no_sync = (not z_sync) and (not xy_sync) + if no_sync: + self._plot_param.use_default_lims = True + self._plot_param.set_zlims = True + elif xy_sync and z_sync: + self._plot_param.use_default_lims = False + self._plot_param.set_zlims = False + elif z_sync: + self._plot_param.use_default_lims = True + self._plot_param.set_zlims = False + else: # xy_sync + self._plot_param.use_default_lims = False + self._plot_param.set_zlims = True + self._plot() + def _attach_signal_slots(self): self.view.sig_plot.connect(self._plot) self.view.sig_update_omega_offset.connect(self._update_omega_offset) self.view.sig_restore_default_omega_offset.connect(self._set_default_omega_offset) self.view.sig_update_dxdy.connect(self._update_dx_dy) self.view.sig_restore_default_dxdy.connect(self._set_default_dx_dy) + self.view.sig_calculate_projection.connect(self._toggle_projections) + self.view.sig_save_data.connect(self._save_data) self.view.sig_change_colormap.connect(self._set_colormap) + self.view.sig_change_log.connect(self._set_log) + self.view.sig_change_linestyle.connect(self._change_line_style) + self.view.sig_manual_lim_changed.connect(self._manual_lim_changed) self._plotted_script_number = 0 self.view.sig_change_grid.connect(self._change_grid_state) self.view.sig_change_crystal_axes.connect(self._change_crystal_axes) self.view.sig_change_font_size.connect(self._change_font_size) + self.view.sig_home_button_clicked.connect(self._home_button_clicked) + self.view.sig_plot_zoom_updated.connect(self._set_zooming_data) + self.view.sig_switch_changed.connect(self._switch_spinners_lims) + self.view.sig_axes_changed.connect(self._change_axes) + self.view.sig_change_data_ws.connect(self._change_ws) diff --git a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_view.py b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_view.py index b29eb636c021..3f7099fa0ae4 100644 --- a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_view.py +++ b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_view.py @@ -45,8 +45,15 @@ def __init__(self, parent): "grid": _content.tB_grid, "linestyle": _content.tB_linestyle, "crystal_axes": _content.tB_crystal_axes, + "x_min": _content.sB_x_min, + "x_max": _content.sB_x_max, + "y_min": _content.sB_y_min, + "y_max": _content.sB_y_max, + "z_min": _content.sB_z_min, + "z_max": _content.sB_z_max, "colormap": _content.combB_colormap, "projections": _content.tB_projections, + "log_scale": _content.tB_log, "invert_cb": _content.tB_invert_cb, "save_data": _content.tB_save_data, "fontsize": _content.sB_fontsize, @@ -60,7 +67,7 @@ def __init__(self, parent): self.datalist = DNSDatalist(self, self._map["datalist"]) self._map["down"].clicked.connect(self.datalist.down) self._map["up"].clicked.connect(self.datalist.up) - self.datalist.sig_datalist_changed.connect(self._plot) + self.datalist.sig_datalist_changed.connect(self._change_ws) # connecting signals self._attach_signal_slots() @@ -73,13 +80,16 @@ def __init__(self, parent): self.menus = [] self.menus.append(self.views_menu) self.menus.append(self.options_menu) + self.views_menu.sig_switch_state_changed.connect(self._switch_changed) + self.views_menu.sig_axes_changed.connect(self._axes_changed) self.views_menu.sig_replot.connect(self._plot) - canvas = FigureCanvas(Figure()) - canvas.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) - toolbar = NavigationToolbar(canvas, self) - _content.plot_head_layout.insertWidget(5, toolbar) - _content.plot_layout.insertWidget(2, canvas) - self.canvas = canvas + self.canvas = FigureCanvas(Figure()) + self.canvas.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + self.toolbar = NavigationToolbar(self.canvas, self) + self.toolbar.actions()[0].triggered.connect(self._home_button_clicked) + _content.plot_head_layout.insertWidget(5, self.toolbar) + _content.plot_layout.insertWidget(2, self.canvas) + self.canvas.mpl_connect("button_release_event", self._zooming_event) self.single_crystal_plot = DNSScPlot(self, self.canvas.figure, None) self.initial_values = None @@ -89,15 +99,46 @@ def __init__(self, parent): sig_restore_default_dxdy = Signal() sig_update_omega_offset = Signal(float) sig_update_dxdy = Signal(float, float) + sig_calculate_projection = Signal(bool) + sig_save_data = Signal() sig_change_grid = Signal() sig_change_crystal_axes = Signal(bool) sig_change_colormap = Signal() + sig_change_log = Signal() sig_change_font_size = Signal() + sig_change_linestyle = Signal() + sig_manual_lim_changed = Signal() + sig_home_button_clicked = Signal() + sig_plot_zoom_updated = Signal() + sig_switch_changed = Signal() + sig_axes_changed = Signal() + sig_change_data_ws = Signal() # emitting custom signals for presenter + def _switch_changed(self): + self.sig_switch_changed.emit() + + def _axes_changed(self): + self.sig_axes_changed.emit() + + def _home_button_clicked(self): + self.sig_home_button_clicked.emit() + + def _manual_lim_changed(self): + self.sig_manual_lim_changed.emit() + + def _change_linestyle(self): + self.sig_change_linestyle.emit() + + def _toggle_projections(self, set_proj): + self.sig_calculate_projection.emit(set_proj) + def _change_font_size(self): self.sig_change_font_size.emit() + def _change_log(self): + self.sig_change_log.emit() + def _change_colormap(self): self.sig_change_colormap.emit() @@ -107,9 +148,15 @@ def _change_crystal_axes(self, pressed): def _plot(self): self.sig_plot.emit() + def _change_ws(self): + self.sig_change_data_ws.emit() + def _change_grid(self): self.sig_change_grid.emit() + def _save_data(self): + self.sig_save_data.emit() + def set_plot_view_menu_visibility(self, visible): self.views_menu.menuAction().setVisible(visible) @@ -128,11 +175,19 @@ def change_omega_offset(self): omega_offset_dialog.exec_() # gui options + def _zooming_event(self, event): + # event.button = 1 for zooming in, = 3 for zooming out + if (event.button == 1 or event.button == 3) and self.toolbar.mode == "zoom rect": + self.sig_plot_zoom_updated.emit() + def create_subfigure(self, grid_helper=None): self.single_crystal_plot = DNSScPlot(self, self.canvas.figure, grid_helper) - def get_axis_type(self): - return self.views_menu.get_value() + def get_plotting_settings_dict(self): + return self.views_menu.get_plot_view_settings() + + def get_plotting_setting(self, setting_key): + return self.views_menu.get_plot_view_settings()[setting_key] def set_initial_omega_offset_dx_dy(self, off, dx, dy): self.initial_values = {"omega_offset": off, "dx": dx, "dy": dy} @@ -143,6 +198,16 @@ def draw(self): def _attach_signal_slots(self): self._map["fontsize"].editingFinished.connect(self._change_font_size) self._map["grid"].clicked.connect(self._change_grid) + self._map["log_scale"].clicked.connect(self._change_log) + self._map["linestyle"].clicked.connect(self._change_linestyle) + self._map["projections"].clicked.connect(self._toggle_projections) + self._map["save_data"].clicked.connect(self._save_data) + self._map["x_min"].editingFinished.connect(self._manual_lim_changed) + self._map["x_max"].editingFinished.connect(self._manual_lim_changed) + self._map["y_min"].editingFinished.connect(self._manual_lim_changed) + self._map["y_max"].editingFinished.connect(self._manual_lim_changed) + self._map["z_min"].editingFinished.connect(self._manual_lim_changed) + self._map["z_max"].editingFinished.connect(self._manual_lim_changed) self._map["colormap"].currentIndexChanged.connect(self._change_colormap) self._map["crystal_axes"].clicked.connect(self._change_crystal_axes) self._map["invert_cb"].clicked.connect(self._change_colormap) diff --git a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/mpl_helpers.py b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/mpl_helpers.py index d0306478d7ac..e1222675d8a0 100644 --- a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/mpl_helpers.py +++ b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/mpl_helpers.py @@ -6,6 +6,13 @@ # SPDX - License - Identifier: GPL - 3.0 + import matplotlib as mpl +from matplotlib import colors + + +def get_log_norm(log, zlim): + if log: + return colors.SymLogNorm(linthresh=0.001, vmin=zlim[0], vmax=zlim[1]) + return colors.Normalize(vmin=zlim[0], vmax=zlim[1]) def get_cmap(colormap_name): diff --git a/qt/python/mantidqtinterfaces/test/dns_single_crystal_elastic/data_structures/dns_single_crystal_map_test.py b/qt/python/mantidqtinterfaces/test/dns_single_crystal_elastic/data_structures/dns_single_crystal_map_test.py index 2355f37120ad..c9af0ec2e453 100644 --- a/qt/python/mantidqtinterfaces/test/dns_single_crystal_elastic/data_structures/dns_single_crystal_map_test.py +++ b/qt/python/mantidqtinterfaces/test/dns_single_crystal_elastic/data_structures/dns_single_crystal_map_test.py @@ -62,10 +62,10 @@ def test___init__(self): self.assertEqual(self.map.hkl2, "2,3,4") self.assertEqual(self.map.wavelength, 4.74) self.assertEqual(len(self.map.hkl_mesh), 3) - self.assertTrue(np.allclose(self.map.hklx_mesh, [0.0, 0.0, -0.00022479, -0.00028889, -0.0003854, -0.00051368])) - self.assertTrue(np.allclose(self.map.hkly_mesh, [0.0, 0.0, -0.00735043, -0.00734146, -0.01470759, -0.01469189])) - self.assertTrue((self.map.z_mesh == np.array([8.0, 11.0, 9.0, 12.0, 10.0, 13.0])).all()) - self.assertTrue((self.map.error_mesh == np.array([14.0, 17.0, 15.0, 18.0, 16.0, 19.0])).all()) + self.assertTrue(np.allclose(self.map.hklx_mesh, [[0.0, 0.0], [-0.00022479, -0.00028889], [-0.0003854, -0.00051368]])) + self.assertTrue(np.allclose(self.map.hkly_mesh, [[0.0, 0.0], [-0.00735043, -0.00734146], [-0.01470759, -0.01469189]])) + self.assertTrue((self.map.z_mesh == np.array([[8.0, 11.0], [9.0, 12.0], [10.0, 13.0]])).all()) + self.assertTrue((self.map.error_mesh == np.array([[14.0, 17.0], [15.0, 18.0], [16.0, 19.0]])).all()) def test__get_mesh(self): data_array = get_fake_elastic_single_crystal_dataset() @@ -107,26 +107,49 @@ def test__get_hkl_mesh(self): @patch("mantidqtinterfaces.dns_single_crystal_elastic.data_structures.dns_single_crystal_map.tri") def test_triangulate(self, mock_tri): self.map.hkl_mesh = [np.asarray([0, 1]), np.asarray([2, 3]), np.asarray([4, 5])] - test_v = self.map.triangulate("hkl_mesh", switch=False) + self.map.triangulate("hkl_mesh", switch=False) mock_tri.Triangulation.assert_called_once() self.assertTrue((mock_tri.Triangulation.call_args_list[0][0][0] == np.asarray([0, 1])).all()) self.assertTrue((mock_tri.Triangulation.call_args_list[0][0][1] == np.asarray([2, 3])).all()) - self.assertEqual(test_v, mock_tri.Triangulation.return_value) + self.assertEqual(self.map.triangulation, mock_tri.Triangulation.return_value) mock_tri.reset_mock() - def test_interpolate_triangulation(self): - z_mock = mock.Mock() - self.map.triangulation = None - self.map.z_mesh = z_mock - self.assertIsNone(self.map.interpolate_triangulation()) - self.map.triangulation = 1 - test_v = self.map.interpolate_triangulation() - self.assertEqual(test_v[0], 1) - self.assertEqual(test_v[1], z_mock.flatten.return_value) + @patch("mantidqtinterfaces.dns_single_crystal_elastic.data_structures.dns_single_crystal_map.LinearTriInterpolator") + @patch("mantidqtinterfaces.dns_single_crystal_elastic.data_structures.dns_single_crystal_map.UniformTriRefiner") + def test_interpolate_triangulation(self, mock_refiner, mock_interpolator): + triangulation = mock.Mock() + self.map.triangulation = triangulation + z_mesh = np.array([1.0, 2.0, 3.0]) + self.map.z_mesh = z_mesh + refined_x = np.array([0.0, 0.5, 0.25]) + refined_y = np.array([0.0, 0.0, 0.5]) + refined_z = np.array([1.0, 1.5, 2.0]) + refined_triangles = np.array([[0, 1, 2]]) + mock_refined_triang = mock.Mock() + mock_refined_triang.x = refined_x + mock_refined_triang.y = refined_y + mock_refined_triang.triangles = refined_triangles + mock_refiner_instance = mock.Mock() + mock_refiner_instance.refine_field.return_value = (mock_refined_triang, refined_z) + mock_refiner.return_value = mock_refiner_instance + self.map.interpolate_triangulation(interpolation=2) + + actual_args, _ = mock_interpolator.call_args + self.assertEqual(actual_args[0], triangulation) + self.assertTrue((actual_args[1] == z_mesh.flatten()).all()) + mock_refiner.assert_called_once_with(triangulation) + self.assertTrue(self.map.triangulation == mock_refined_triang) + self.assertTrue((self.map.z_mesh == refined_z).all()) + self.assertTrue((self.map.z_tri == refined_z[refined_triangles]).all()) + self.assertTrue((self.map.z_face == refined_z[refined_triangles].mean(axis=1)).all()) + self.assertTrue((self.map.x_tri == refined_x[refined_triangles]).all()) + self.assertTrue((self.map.y_tri == refined_y[refined_triangles]).all()) + self.assertTrue((self.map.x_face == refined_x[refined_triangles].mean(axis=1)).all()) + self.assertTrue((self.map.y_face == refined_y[refined_triangles].mean(axis=1)).all()) @patch("mantidqtinterfaces.dns_single_crystal_elastic.data_structures.dns_single_crystal_map.path") def test_get_dns_map_border(self, mock_path): - test_v = self.map.get_dns_map_border("qxqy") + test_v = self.map.get_dns_map_border("qxqy", False) self.assertEqual(test_v, mock_path.Path.return_value) test_array = np.array( [ @@ -144,7 +167,7 @@ def test_get_dns_map_border(self, mock_path): ) self.assertTrue(np.allclose(mock_path.Path.call_args_list[0][0][0], test_array)) mock_path.reset_mock() - self.map.get_dns_map_border("hkl") + self.map.get_dns_map_border("hkl", False) test_array = np.array( [ [0.0, 0.0], @@ -165,8 +188,7 @@ def test_mask_triangles(self): mock_triang = mock.Mock() self.map.triangulation = mock_triang self.map.triangulation.triangles = np.array([[5, 4, 2], [2, 4, 0], [5, 2, 3], [3, 2, 0]]) - test_v = self.map.mask_triangles("hkl_mesh") - self.assertEqual(test_v, mock_triang) + self.map.mask_triangles("hkl_mesh", False) mock_triang.set_mask.assert_called_once() carg = mock_triang.set_mask.call_args_list[0][0][0] self.assertTrue((carg == np.array([False, True, False, False])).all()) diff --git a/qt/python/mantidqtinterfaces/test/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_model_test.py b/qt/python/mantidqtinterfaces/test/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_model_test.py index 16f9efd48fef..fa185574c926 100644 --- a/qt/python/mantidqtinterfaces/test/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_model_test.py +++ b/qt/python/mantidqtinterfaces/test/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_model_test.py @@ -9,7 +9,7 @@ from unittest import mock from unittest.mock import patch - +import numpy as np from mantidqtinterfaces.dns_powder_tof.data_structures.dns_obs_model import DNSObsModel from mantidqtinterfaces.dns_single_crystal_elastic.plot.elastic_single_crystal_plot_model import DNSElasticSCPlotModel from mantidqtinterfaces.dns_powder_tof.helpers.helpers_for_testing import ( @@ -28,8 +28,11 @@ def setUpClass(cls): def setUp(self): self.model._single_crystal_map = mock.Mock() - self.model._single_crystal_map.hkl_mesh_intp = [1, 2, 3] - self.model._single_crystal_map.hkl_mesh = [0, 1, 2] + self.model._single_crystal_map.triangulation = mock.Mock() + self.model._single_crystal_map.x_face = np.array([10, 11, 12]) + self.model._single_crystal_map.y_face = np.array([13, 14, 15]) + self.model._single_crystal_map.z_face = np.array([7, 8, 9]) + self.model._single_crystal_map.z_mesh = np.array([4, 5, 6]) def test___init__(self): self.assertIsInstance(self.model, DNSElasticSCPlotModel) @@ -39,11 +42,12 @@ def test___init__(self): self.assertTrue(hasattr(self.model._data, "x")) self.assertTrue(hasattr(self.model._data, "y")) self.assertTrue(hasattr(self.model._data, "z")) + self.assertTrue(hasattr(self.model._data, "x_lims")) + self.assertTrue(hasattr(self.model._data, "y_lims")) + self.assertTrue(hasattr(self.model._data, "z_lims")) self.assertTrue(hasattr(self.model._data, "z_min")) self.assertTrue(hasattr(self.model._data, "z_max")) self.assertTrue(hasattr(self.model._data, "pz_min")) - self.assertTrue(hasattr(self.model._data, "triang")) - self.assertTrue(hasattr(self.model._data, "z_triang")) @patch("mantidqtinterfaces.dns_single_crystal_elastic.plot.elastic_single_crystal_plot_model.DNSScMap") def test_create_single_crystal_map(self, mock_dns_scd_map): @@ -54,21 +58,36 @@ def test_create_single_crystal_map(self, mock_dns_scd_map): mock_dns_scd_map.assert_called_once() self.assertEqual(self.model._single_crystal_map, 123) - def test_get_interpolated_triangulation(self): - self.model._single_crystal_map.interpolate_triangulation.return_value = [1, 2] - test_v = self.model.get_interpolated_triangulation(True, "hkl", False) - self.assertEqual(self.model._data.x, 0) - self.assertEqual(self.model._data.y, 1) - self.assertEqual(self.model._data.z, 2) - self.model._single_crystal_map.triangulate.assert_called_once_with(mesh_name="hkl_mesh", switch=False) - self.model._single_crystal_map.mask_triangles.assert_called_once_with(mesh_name="hkl_mesh") - self.model._single_crystal_map.interpolate_triangulation.assert_called_once_with(True) - self.assertEqual(test_v, (1, 2)) - self.model._single_crystal_map.triangulate.reset_mock() - self.model._single_crystal_map.interpolate_triangulation.reset_mock() - self.model.get_interpolated_triangulation(False, "hkl", True) - self.model._single_crystal_map.triangulate.assert_called_once_with(mesh_name="hkl_mesh", switch=True) - self.model._single_crystal_map.interpolate_triangulation.assert_called_once_with(False) + @patch("mantidqtinterfaces.dns_single_crystal_elastic.plot.elastic_single_crystal_plot_model.helper.get_projection") + @patch("mantidqtinterfaces.dns_single_crystal_elastic.plot.elastic_single_crystal_plot_model.helper.filter_flattened_meshes") + def test_get_projections(self, mock_ffm, mock_gpj): + mock_ffm.return_value = [1, 2, 3] + mock_gpj.return_value = 2 + test_v = self.model.get_projections([0, 1], [2, 3]) + mock_ffm.assert_called_once() + self.assertEqual(mock_gpj.call_count, 2) + self.assertEqual(mock_gpj.mock_calls, [mock.call(1, 3), mock.call(2, 3)]) + self.assertEqual(test_v[1], 2) + self.assertEqual(test_v[0], 2) + + def test_generate_triangulation_mesh(self): + result = self.model.generate_triangulation_mesh(True, "hkl", True) + self.assertIsNotNone(result) + self.assertIsInstance(result, tuple) + self.assertEqual(len(result), 3) + self.assertIsInstance(result[0], type(self.model._single_crystal_map.triangulation)) + self.assertTrue(np.all(result[1] == np.array([4, 5, 6]))) + self.assertTrue(np.all(result[2] == np.array([7, 8, 9]))) + + def test_generate_quad_mesh(self): + self.model._single_crystal_map.interpolate_quad_mesh = mock.Mock() + self.model._single_crystal_map.test_mesh = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + self.model.switch_axis = mock.Mock(return_value=np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])) + self.model.set_mesh_data = mock.Mock() + self.model.generate_quad_mesh(2, "test", False) + self.model._single_crystal_map.interpolate_quad_mesh.assert_called_once_with(2) + self.model.switch_axis.assert_called_once() + self.model.set_mesh_data.assert_called_once() def test_get_axis_labels(self): self.model._single_crystal_map.hkl1 = "abc" @@ -82,6 +101,24 @@ def test_get_changing_hkl_components(self): test_v = self.model.get_changing_hkl_components() self.assertEqual(test_v, 123) + @patch("mantidqtinterfaces.dns_single_crystal_elastic.plot.elastic_single_crystal_plot_model.helper.get_z_min_max") + def test_get_data_z_min_max(self, mock_get_z_min_max): + mock_get_z_min_max.return_value = 123 + self.model._data.x = 0 + self.model._data.y = 1 + self.model._data.z = 2 + test_v = self.model.get_data_z_min_max(xlim=[1, 2], ylim=[2, 3]) + mock_get_z_min_max.assert_called_once_with(2, [1, 2], [2, 3], 0, 1) + self.assertEqual(test_v, 123) + + def test_get_data_xy_lim(self): + self.model._data.x = np.array([[1, 2], [3, 4]]) + self.model._data.y = np.array([[7, 8], [3, 4]]) + test_v = self.model.get_data_xy_lim(False) + self.assertEqual(test_v, [[1, 4], [3, 8]]) + test_v = self.model.get_data_xy_lim(True) + self.assertEqual(test_v, [[3, 8], [1, 4]]) + def test_get_omega_offset(self): self.model._single_crystal_map = {"omega_offset": 123} test_v = self.model.get_omega_offset() diff --git a/qt/python/mantidqtinterfaces/test/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_plot_test.py b/qt/python/mantidqtinterfaces/test/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_plot_test.py index 4aed6a998e0e..cd8c04161b5a 100644 --- a/qt/python/mantidqtinterfaces/test/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_plot_test.py +++ b/qt/python/mantidqtinterfaces/test/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_plot_test.py @@ -23,10 +23,6 @@ def setUp(self): patcher_subplot.start() self.plot = DNSScPlot(parent=None, figure=self.mock_fig, grid_helper=None) - def test_on_resize(self): - self.plot.on_resize() - self.mock_fig.tight_layout.assert_called_once_with(pad=0.3) - @staticmethod def test_set_fontsize(): with patch.object(matplotlib.rcParams, "update") as mock_update: @@ -76,11 +72,18 @@ def test_set_axis_labels(self): self.plot._ax.set_xlabel.assert_called_once_with("x") self.plot._ax.set_ylabel.assert_called_once_with("y") - def test_plot_triangulation(self): + def test_plot_triangulation_non_flat(self): + self.plot._ax.tripcolor.return_value = "plotobj" + self.plot.plot_triangulation("triang", [0, 1, 2], [1, 2, 3], "cmap", "edges", "shade") + self.plot._ax.set_visible.assert_called_with(True) + self.plot._ax.tripcolor.assert_called_once_with("triang", [0, 1, 2], cmap="cmap", edgecolors="edges", shading="shade") + self.assertEqual(self.plot._plot, "plotobj") + + def test_plot_triangulation_flat(self): self.plot._ax.tripcolor.return_value = "plotobj" - self.plot.plot_triangulation("triang", "z", "cmap", "edges", "shade") + self.plot.plot_triangulation("triang", [0, 1, 2], [1, 2, 3], "cmap", "edges", "flat") self.plot._ax.set_visible.assert_called_with(True) - self.plot._ax.tripcolor.assert_called_once_with("triang", "z", cmap="cmap", edgecolors="edges", shading="shade") + self.plot._ax.tripcolor.assert_called_once_with("triang", facecolors=[1, 2, 3], cmap="cmap", edgecolors="edges", shading="flat") self.assertEqual(self.plot._plot, "plotobj") diff --git a/qt/python/mantidqtinterfaces/test/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_presenter_test.py b/qt/python/mantidqtinterfaces/test/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_presenter_test.py index be72151161ca..31bff8147eef 100644 --- a/qt/python/mantidqtinterfaces/test/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_presenter_test.py +++ b/qt/python/mantidqtinterfaces/test/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_presenter_test.py @@ -28,14 +28,24 @@ def setUpClass(cls): cls.view.single_crystal_plot = mock.create_autospec(DNSScPlot) cls.view.datalist = mock.create_autospec(DNSDatalist) cls.view.sig_plot.connect = mock.Mock() - cls.view.sig_restore_default_omega_offset = mock.Mock() - cls.view.sig_restore_default_dxdy = mock.Mock() cls.view.sig_update_omega_offset.connect = mock.Mock() + cls.view.sig_restore_default_omega_offset = mock.Mock() cls.view.sig_update_dxdy.connect = mock.Mock() + cls.view.sig_restore_default_dxdy = mock.Mock() + cls.view.sig_calculate_projection = mock.Mock() + cls.view.sig_save_data = mock.Mock() cls.view.sig_change_colormap.connect = mock.Mock() + cls.view.sig_change_log.connect = mock.Mock() + cls.view.sig_change_linestyle.connect = mock.Mock() + cls.view.sig_manual_lim_changed.connect = mock.Mock() cls.view.sig_change_grid.connect = mock.Mock() cls.view.sig_change_crystal_axes.connect = mock.Mock() cls.view.sig_change_font_size.connect = mock.Mock() + cls.view.sig_home_button_clicked.connect = mock.Mock() + cls.view.sig_plot_zoom_updated.connect = mock.Mock() + cls.view.sig_switch_changed.connect = mock.Mock() + cls.view.sig_axes_changed.connect = mock.Mock() + cls.view.sig_change_data_ws.connect = mock.Mock() cls.model = mock.create_autospec(DNSElasticSCPlotModel) cls.presenter = DNSElasticSCPlotPresenter(view=cls.view, model=cls.model, parent=parent) @@ -43,7 +53,7 @@ def setUpClass(cls): def setUp(self): self.view.reset_mock() self.model.reset_mock() - self.view.get_axis_type.return_value = { + self.view.get_plotting_settings_dict.return_value = { "plot_type": "quasmesh", "type": "hkl", "switch": False, @@ -69,6 +79,12 @@ def setUp(self): self.presenter._plot_param.colormap_name = "jet" self.presenter._plot_param.font_size = 1 self.presenter._plot_param.lines = 0 + self.presenter._plot_param.xlim = None + self.presenter._plot_param.ylim = None + self.presenter._plot_param.zlim = None + self.presenter._plot_param.projections = False + self.presenter._plot_param.use_default_lims = True + self.presenter._plot_param.set_zlims = True def test___init__(self): self.assertIsInstance(self.presenter, DNSElasticSCPlotPresenter) @@ -80,6 +96,43 @@ def test___init__(self): self.assertTrue(hasattr(self.presenter._plot_param, "colormap_name")) self.assertTrue(hasattr(self.presenter._plot_param, "font_size")) self.assertTrue(hasattr(self.presenter._plot_param, "lines")) + self.assertTrue(hasattr(self.presenter._plot_param, "xlim")) + self.assertTrue(hasattr(self.presenter._plot_param, "ylim")) + self.assertTrue(hasattr(self.presenter._plot_param, "zlim")) + self.assertTrue(hasattr(self.presenter._plot_param, "projections")) + + @patch("mantidqtinterfaces.dns_single_crystal_elastic.plot.elastic_single_crystal_plot_presenter.DNSElasticSCPlotPresenter._plot") + @patch( + "mantidqtinterfaces.dns_single_crystal_elastic.plot." + "elastic_single_crystal_plot_presenter." + "DNSElasticSCPlotPresenter._calculate_projections" + ) + @patch( + "mantidqtinterfaces.dns_single_crystal_elastic.plot." + "elastic_single_crystal_plot_presenter." + "DNSElasticSCPlotPresenter._get_current_spinners_lims" + ) + def test__toggle_projections(self, mock_get_current_spinners_lims, mock_calculate_proj, mock_plot): + self.presenter = DNSElasticSCPlotPresenter(view=self.view, model=self.model) + mock_calculate_proj.return_value = (1, 2) + mock_get_current_spinners_lims.return_value = ([3, 4], [5, 6], [7, 8]) + self.view.initial_values = mock.Mock() + self.presenter._toggle_projections(False) + self.view.single_crystal_plot.remove_projections.assert_called_once() + mock_plot.assert_called_once() + self.presenter._toggle_projections(True) + mock_calculate_proj.assert_called_once() + mock_get_current_spinners_lims.assert_called_once() + self.view.draw.assert_called_once() + self.view.single_crystal_plot.set_projections.assert_called_once_with(1, 2, [3, 4], [5, 6]) + + def test__calculate_projections(self): + self.view.single_crystal_plot.get_active_limits.return_value = (1, 2) + self.model.get_projections.return_value = (3, 4) + test_v = self.presenter._calculate_projections() + self.model.get_projections.assert_called_once_with(1, 2) + self.assertEqual(test_v, (3, 4)) + self.model.get_projections.reset_mock() def test__datalist_updated(self): self.view.datalist.get_datalist.return_value = None @@ -95,7 +148,7 @@ def test__datalist_updated(self): self.assertTrue(test_v) def test__plot(self): - self.view.get_axis_type.return_value = {"plot_type": "type1"} + self.view.get_plotting_setting.side_effect = {"plot_type": "type1", "switch": False}.__getitem__ self.view.datalist.get_checked_plots.return_value = ["plot1"] self.presenter.param_dict = { "elastic_single_crystal_script_generator": {"data_arrays": {"plot1": [1, 2, 3]}}, @@ -108,25 +161,58 @@ def test__plot(self): self.presenter._crystallographical_axes = mock.Mock(return_value="grid_axes") self.presenter._set_axis_labels = mock.Mock() self.presenter._set_initial_omega_offset_dx_dy = mock.Mock() + self.presenter._manual_lim_changed = mock.Mock() + self.presenter._determine_plot_type_options = mock.Mock() + self.presenter._get_current_spinners_lims = mock.Mock(return_value=([1, 2], [3, 4], [5, 6])) + self.presenter._set_spinners_lims = mock.Mock() + self.presenter._set_plotting_lims = mock.Mock() + self.presenter._set_log = mock.Mock() self.view.single_crystal_plot.create_colorbar = mock.Mock() - self.view.single_crystal_plot.on_resize = mock.Mock() self.view.single_crystal_plot = mock.Mock() self.view.canvas = mock.Mock() self.view.canvas.figure = mock.Mock() self.view.canvas.figure.tight_layout = mock.Mock() self.view.draw = mock.Mock() + self.model.get_default_data_lims = mock.Mock(return_value=([1, 2], [3, 4], [5, 6])) + self.presenter._plot(initial_values={"omega_offset": 1}) self.presenter._change_font_size.assert_called_once_with(draw=False) self.model.create_single_crystal_map.assert_called_once_with([1, 2, 3], {"options"}, {"omega_offset": 1}) self.presenter._want_plot.assert_called_once_with("type1") self.view.create_subfigure.assert_called_once_with(None) self.view.single_crystal_plot.create_colorbar.assert_called_once() - self.view.single_crystal_plot.on_resize.assert_called_once() self.view.canvas.figure.tight_layout.assert_called_once() self.view.draw.assert_called_once() + def test_set_plot_param_lims(self): + self.presenter = DNSElasticSCPlotPresenter(view=self.view, model=self.model) + self.presenter._plot_param = mock.Mock() + xlim, ylim, zlim = [-1, 2], [3, -4], [5, 6] + self.presenter._set_plot_param_lims(xlim, ylim, zlim, include_zlim=False) + self.assertEqual(self.presenter._plot_param.xlim, [-1, 2]) + self.assertEqual(self.presenter._plot_param.ylim, [3, -4]) + self.assertFalse(self.presenter._plot_param.zlim.called) + self.presenter._plot_param.reset_mock() + self.presenter._set_plot_param_lims(xlim, ylim, zlim, include_zlim=True) + self.assertEqual(self.presenter._plot_param.xlim, [-1, 2]) + self.assertEqual(self.presenter._plot_param.ylim, [3, -4]) + self.assertEqual(self.presenter._plot_param.zlim, [5, 6]) + + def test_update_plot_param_lims(self): + self.presenter = DNSElasticSCPlotPresenter(view=self.view, model=self.model) + self.view.single_crystal_plot.get_active_limits.return_value = ([10, 20], [30, 40]) + self.model.get_data_z_min_max.return_value = [-1, -2] + self.presenter._update_plot_param_lims(include_zlim=False) + self.assertEqual(self.presenter._plot_param.xlim, [10, 20]) + self.assertEqual(self.presenter._plot_param.ylim, [30, 40]) + # default values for zlim + self.assertEqual(self.presenter._plot_param.zlim, [None, None]) + # if include_zlim is not provided, True should be used by default + self.presenter._update_plot_param_lims() + self.assertEqual(self.presenter._plot_param.zlim, [-1, -2]) + def test___set_initial_omega_offset_dx_dy(self): self.model.get_omega_offset.return_value = 3 self.model.get_dx_dy.return_value = (1, 2) @@ -134,7 +220,6 @@ def test___set_initial_omega_offset_dx_dy(self): self.model.get_omega_offset.assert_called_once() self.model.get_dx_dy.assert_called_once() self.view.set_initial_omega_offset_dx_dy.assert_called_once_with(3, 1, 2) - # oof = self.model.get_omega_offset() @patch( "mantidqtinterfaces.dns_single_crystal_elastic.plot.elastic_single_crystal_plot_presenter." @@ -177,25 +262,26 @@ def test__update_dx_dy(self, mock_plot): ) def test_plot_triangulation(self, mock_get_plot_styles): mock_get_plot_styles.return_value = ("jet", False, "flat") - self.model.get_interpolated_triangulation.return_value = (1, 2) + self.model.generate_triangulation_mesh.return_value = (1, 2, 3) self.presenter._plot_triangulation(False, {}, True) - self.model.get_interpolated_triangulation.assert_called_once_with(False, {}, True) - self.view.single_crystal_plot.plot_triangulation.assert_called_once_with(1, 2, "jet", False, "flat") + self.model.generate_triangulation_mesh.assert_called_once_with(False, {}, True) + self.view.single_crystal_plot.plot_triangulation.assert_called_once_with(1, 2, 3, "jet", False, "flat") @patch( "mantidqtinterfaces.dns_single_crystal_elastic.plot.elastic_single_crystal_plot_presenter.DNSElasticSCPlotPresenter._plot_triangulation" ) def test__want_plot(self, mock_plot_triangulation): self.mock_view = mock.Mock() - self.mock_view.get_axis_type = mock.Mock() - self.mock_view.get_axis_type.return_value = {"type": "hkl", "interpolate": "triang", "switch": False} + self.mock_view.get_plotting_settings_dict = mock.Mock() + self.mock_view.get_plotting_settings_dict.return_value = {"type": "hkl", "interpolate": 0, "switch": False} self.presenter = DNSElasticSCPlotPresenter(name="test", parent=None, view=self.mock_view, model=mock.Mock()) - self.presenter._want_plot("plot_type_value") - self.mock_view.get_axis_type.assert_called_once_with() - mock_plot_triangulation.assert_called_once_with("triang", "hkl", False) + self.presenter._want_plot("triangulation") + self.mock_view.get_plotting_settings_dict.assert_called_once_with() + mock_plot_triangulation.assert_called_once_with(0, "hkl", False) def test__get_plot_styles(self): self.presenter._plot_param.lines = 0 + self.view.get_plotting_setting.side_effect = {"shading": "flat"}.__getitem__ test_v = self.presenter._get_plot_styles() self.view.get_state.assert_called_once() self.assertIsInstance(test_v[0], LinearSegmentedColormap) @@ -208,6 +294,7 @@ def test__get_plot_styles(self): ) def test__set_axis_labels(self, mock_crystallographical_axes): mock_crystallographical_axes.return_value = True + self.view.get_plotting_setting.side_effect = {"type": "hkl", "switch": False}.__getitem__ self.model.get_axis_labels.return_value = ("x", "y") self.presenter = DNSElasticSCPlotPresenter(name="test", parent=None, view=self.view, model=self.model) self.presenter._set_axis_labels() @@ -291,6 +378,7 @@ def test__change_grid_state(self, mock_cg, mock_ng): @patch("mantidqtinterfaces.dns_single_crystal_elastic.plot.elastic_single_crystal_plot_presenter.DNSElasticSCPlotPresenter._plot") def test__change_crystal_axes(self, mock_plot): + self.view.initial_values = mock.Mock() self.presenter._change_crystal_axes() self.assertEqual(self.presenter._plot_param.grid_state, 0) mock_plot.assert_called_once() @@ -300,8 +388,9 @@ def test__create_grid_helper(self, mock_get_grid_helper): self.presenter._plot_param.grid_helper = None self.presenter._plot_param.grid_state = 0 self.model.get_changing_hkl_components.return_value = 1, 2, 3, 4 + self.view.get_plotting_setting.return_value = False self.presenter._create_grid_helper() - self.view.get_axis_type.assert_called_once() + self.view.get_plotting_setting.assert_called_once() mock_get_grid_helper.assert_called_once_with(None, 0, 1, 2, 3, 4, False) @patch( @@ -316,6 +405,23 @@ def test__set_colormap(self, mock_get_plot_styles): self.view.single_crystal_plot.set_cmap.assert_called_once_with("jet") self.view.draw.assert_called_once() + @patch("mantidqtinterfaces.dns_single_crystal_elastic.plot.elastic_single_crystal_plot_presenter.mpl_helpers.get_log_norm") + @patch( + "mantidqtinterfaces.dns_single_crystal_elastic.plot.elastic_single_crystal_plot_presenter.DNSElasticSCPlotPresenter._get_current_spinners_lims" + ) + def test__set_log(self, mock_get_current_spinners_lims, mock_get_log_norm): + self.presenter = DNSElasticSCPlotPresenter(view=self.view, model=self.model) + mock_get_log_norm.return_value = 1 + mock_get_current_spinners_lims.return_value = ([1, 2], [3, 4], [5, 6]) + self.view.get_state.return_value["log_scale"] = True + self.model.get_data_z_min_max.return_value = (10, 20, 30) + self.presenter._set_log() + mock_get_current_spinners_lims.assert_called_once() + self.assertEqual(self.view.get_state.call_count, 1) + self.view.single_crystal_plot.set_norm.assert_called_once_with(1) + self.view.draw.assert_called_once() + mock_get_log_norm.assert_called_once_with(True, [5, 6]) + @patch("mantidqtinterfaces.dns_single_crystal_elastic.plot.elastic_single_crystal_plot_presenter.DNSElasticSCPlotPresenter._plot") def test__change_font_size(self, mock_plot): self.presenter._plot_param.font_size = 0 @@ -334,6 +440,41 @@ def test__change_font_size(self, mock_plot): self.presenter._change_font_size(draw=False) mock_plot.assert_not_called() + def test__set_ax_formatter(self): + self.model.get_format_coord.return_value = 123 + self.presenter._set_ax_formatter() + self.view.get_plotting_settings_dict.assert_called_once() + self.model.get_format_coord.assert_called_once() + self.view.single_crystal_plot.set_format_coord.assert_called_once_with(123) + + @patch( + "mantidqtinterfaces.dns_single_crystal_elastic.plot." + "elastic_single_crystal_plot_presenter." + "DNSElasticSCPlotPresenter._get_current_spinners_lims" + ) + def test__manual_lim_changed(self, mock_get_current_spinners_lims): + mock_get_current_spinners_lims.return_value = ([1, 2], [3, 4], [1, 1]) + self.presenter._manual_lim_changed() + self.view.single_crystal_plot.set_xlim.assert_called_once_with([1, 2]) + self.view.single_crystal_plot.set_ylim.assert_called_once_with([3, 4]) + self.assertFalse(self.presenter._plot_param.use_default_lims) + + @patch("mantidqtinterfaces.dns_single_crystal_elastic.plot.elastic_single_crystal_plot_presenter.DNSElasticSCPlotPresenter._plot") + def test__home_button_clicked(self, mock_plot): + self.presenter._home_button_clicked() + self.assertTrue(self.presenter._plot_param.use_default_lims) + self.assertTrue(self.presenter._plot_param.set_zlims) + mock_plot.assert_called_once() + + def test__get_current_spinners_lims(self): + self.mock_view = mock.Mock() + self.mock_view.get_state.return_value = {"x_min": 1, "x_max": 2, "y_min": 3, "y_max": 4, "z_min": -5, "z_max": 5} + self.presenter = DNSElasticSCPlotPresenter(view=self.mock_view, model=self.model) + xlim, ylim, zlim = self.presenter._get_current_spinners_lims() + self.assertEqual(xlim, [1, 2]) + self.assertEqual(ylim, [3, 4]) + self.assertEqual(zlim, [-5, 5]) + if __name__ == "__main__": unittest.main()