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()