diff --git a/docs/source/cutting.rst b/docs/source/cutting.rst index 5f2812d65..3f55640bd 100644 --- a/docs/source/cutting.rst +++ b/docs/source/cutting.rst @@ -30,10 +30,9 @@ integrate over [0,3], [3,6], [6,9] and [9,10] respectively. Cuts with the same range from multiple datasets can be plotted by first selecting multiple workspaces in the left panel. -There are two different methods to compute cuts: ``Rebin`` and ``Integration``. -**NOTE: for mantid major releases from** ``v6.40``\ **, the default cutting algorithm has been changed from** ``Rebin`` -**to** ``Integration``\ **. For more detail on this change, and cutting algorithms in general, see the** *Cutting Algorithms* -**section below**. +There are two different methods to compute cuts: ``Rebin`` and ``Integration``, which can be selected from the +``Cut Algorithm`` drop down menu. The difference between these methods are described in the :ref:`Cutting_Algorithms` +section below and in more detail in the :ref:`Mathematical_Reference`. Clicking on the ``Norm to 1`` check box will cause the resulting cut data to be normalised such that the maximum of the data of each cut is unity. @@ -92,12 +91,33 @@ that tab. When MSlice is used as a Mantid interface ``MD Histo`` type workspaces can also be saved to Mantid Workbench by clicking the ``Save to Workbench`` button either on the ``MD Histo`` or the ``Cut`` tab. +.. _Cutting_Algorithms: + Cutting Algorithms ------------------ -There are two different methods used to compute cuts. ``Rebin`` uses the basic rebinning algorithms directly and effectively averages -the counts in the integration range, whilst ``Integration`` sums the counts in the integration range. -For mantid major releases from ``v6.40``, the default cutting algorithm has been changed from ``Rebin`` to ``Integration``. This change -has been made because the ``Integration`` method can be used for both absolute and non-absolute units measurements. Conversely, for -absolute units measurements the ``Rebin`` method will give incorrect and misleading values. -As a result of this change, it is expected that values calculanced henceforth will differ from those calculated historically, if the -default integration method has been used. + +There are two different methods used to compute cuts: + +- ``Integration`` sums the (signal :math:`\times` bin width) in the integration range. +- ``Rebin`` averages the signal in the integration range. + +The two methods are described in more detail in the :ref:`Mathematical_Reference`, +but in short, there is a bin-dependent conversion factor between the two types of +cuts which depends on the data coverage in the integration range of that bin. +That is, if the integration range does not include regions without data +(e.g. due to kinematic constraints), then the two cuts will be equivalent except +for a constant scaling factor (proportional to the integration range). +However, if the integration range overlaps regions without data, +then the two cuts will give markedly different results. + +The default method is ``Rebin`` and is more suitable for DOS-types cuts which +integrate over :math:`|Q|` whilst if you are interested in cross-sections and +are integrating over energy transfer, it is recommended to use ``Integration``. + +There is an option in the ``Cut`` tab to change the cut algorithm from ``Rebin`` +to ``Integration`` or vice versa and this setting will be saved for subsequent +similar cuts on the same workspace. + +You can also change the default using the ``Options`` menu, ``Cut algorithm default`` +entry. This will change the default cut algorithm *for this session of MSlice* +(the default algorithm will revert to ``Rebin`` if you restart MSlice). diff --git a/docs/source/images/math_ref/rebin_cuts.png b/docs/source/images/math_ref/rebin_cuts.png new file mode 100644 index 000000000..94279ca61 Binary files /dev/null and b/docs/source/images/math_ref/rebin_cuts.png differ diff --git a/docs/source/images/math_ref/rebin_grids.svg b/docs/source/images/math_ref/rebin_grids.svg new file mode 100644 index 000000000..a0b72faa1 --- /dev/null +++ b/docs/source/images/math_ref/rebin_grids.svg @@ -0,0 +1,725 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/source/index.rst b/docs/source/index.rst index 7cb4b79a9..74412db5c 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -18,6 +18,7 @@ to report any bugs or suggest any improvements to the program. cutting slicing cli + math_ref * :ref:`search` diff --git a/docs/source/math_ref.rst b/docs/source/math_ref.rst new file mode 100644 index 000000000..173068513 --- /dev/null +++ b/docs/source/math_ref.rst @@ -0,0 +1,210 @@ +.. _Mathematical_Reference: + +Mathematical Reference +====================== + +This page describes the mathematical operations behind the ``Slice`` and ``Cut`` operations of MSlice + +In general, MSlice handles "reduced" (processed) inelastic neutron scattering data which has been +binned (histogrammed) in energy transfer for each detector / position-sensitive-detector (PSD) element. +We will use the terminology of the `Horace `_ program and +refer to these energy-detector-element bins as "pixels". + +Since the detector elements are in the laboratory coordinates, but we often want to plot the data +in reciprocal space, a coordinate transformation is needed. +This means that the input and output binning grids of the data will not be axis aligned, and will +instead look something like this: + +.. _grid_figure: + +.. image:: images/math_ref/rebin_grids.svg + :width: 600 + +Where the square red grid represents the target (output) bins, and the slanted green grids +(parallelograms) represents the original bins ("pixels"). +As discussed in the :ref:`PSD_and_non-PSD_modes` section, MSlice makes a distinction between +**PSD** (fine green grid) and **non-PSD** (coarser green grid) data. + +A ``Slice`` refers to a rebin into a two dimensional output, whilst a ``Cut`` is a rebin _or_ +integration into a one dimensional output. +For each type of data (**PSD** and **non-PSD**) we will describe each operation in turn. + + +PSD Slice +--------- + +For **PSD** data, MSlice uses *centre-point rebinning*, treating each input bin ("pixel") as a +point and summing the full signal of each pixel whose centres lie within an output bin +(illustrated in the image by the darker green shading in top left, with dots marking centres). +Thus the output signal in the :math:`(i,j)`\ th bin, :math:`Y_{ij}`, is: + +.. math:: + Y_{ij} = \frac{1}{N_{kl}} \sum_{kl} y_{kl} + +where :math:`y_{kl}` is the input signal in the input :math:`(k,l)`\ th bin +and the sum runs over the :math:`N_{kl}` number of bins whose centres lie within the +boundaries of the :math:`(i,j)`\ th output bin. + +The above expression uses the ``NumEventsNormalization`` convention of Mantid which is the +same as that adopted by the `Horace `_ program. +The error values are considered to be standard deviations and are summed in quadrature. + +PSD Cut +------- + +Since MSlice allows users to specify bins in the non-integrating direction which are not +necessarily aligned with respect to the original data, a rebinning step as described above +is needed for the ``Cut`` operation too. + +This leads to the two types of behaviour ("algorithms") for the ``Cut`` operation described +in the :ref:`Cutting_Algorithms` section: + +- The default ``Rebin`` method just uses the rebinning described above with one axis having + only a single bin. +- The ``Integration`` method first rebins the data as described above with the integration axis + divided into :math:`N_{\mathrm{int}} =` 100 bins. + It then calls the relevant Mantid algorithm + (`IntegrateMDHistoWorkspace `_\ ) + to integrate (sums the signal) in those 100 bins. + +Taking the :math:`j` index to be over the integration axis, the integrated ``Cut`` signal +:math:`C_i` is then given by: + +.. math:: + C_i^{\mathrm{integration}} = w_i \sum_{j \in \mathcal{D}} Y_{ij} + +where the index :math:`j` only runs over regions with data :math:`\mathcal{D}`, and the width +:math:`w_i = \sum_{j \in \mathcal{D}} w_{ij}` where :math:`w_{ij}` is the width in the +:math:`j`\ th direction of the :math:`(i,j)`\ th bin. + +Note that the equivalent expression for a ``Rebin`` cut is simply: + +.. math:: + C_i^{\mathrm{rebin}} = \sum_{j \in \mathcal{F}} Y_{ij} + +where now the index :math:`j` runs over the full integration range :math:`\mathcal{F}`. +The difference is thus a coordinate-dependent weighting factor :math:`w_i`. +If the integration range *does not include regions without data* (e.g. :math:`\mathcal{D} \equiv \mathcal{F}`) +then all the :math:`w_i` will be equal to the full integration width and the difference between +:math:`C_i^{\mathrm{integration}}` and :math:`C_i^{\mathrm{rebin}}` is a constant. + +However, if the integration range covers region with no data (e.g. beyond the kinematic limits) +then the two cuts *may* look very different because :math:`C_i^{\mathrm{integration}}` will weight +regions with data more heavily than regions without data. + + +Non-PSD Slice +------------- + +For **non-PSD** data, MSlice uses *fractional rebinning*, where it first calculates the +overlap area between the input and output bins, and then sums only the fraction of the +signal of the input bins which overlaps the output bin. + +The output signal is computed as: + +.. math:: + Y_{ij} = \left. \sum_{kl} y_{kl} f_{kl} \right/ \sum_{kl} f_{kl} + +and the output uncertainty as: + +.. math:: + E_{ij} = \left. \sqrt{\sum_{kl} e^2_{kl} f_{kl}} \right/ \sum_{kl} f_{kl} + +where :math:`f_{kl}` is the fractional overlap of the input :math:`(k,l)`\ th bin with +the output :math:`(i,j)`\ th bin. + +This is illustrated in the :ref:`figure ` at the start of the page by the square on the right +hand side with blue triangular and orange quadrilateral shaded regions. +The blue and orange shading illustrates the fractional overlap areas which weights +the signal in the top left and top right input bins (large parallelograms) respectively. + +Non-PSD Cuts +------------ + +Like for **PSD** data, there are two ``Cut`` "algorithms" for **non-PSD** data also. + +The ``Rebin`` cut algorithm performs the same operation described in the previous section +but with a single bin in the integration axis, yielding + +.. math:: + C_i^{\mathrm{rebin}} = \left. \sum_{j \in \mathcal{F}} Y_{ij} \right/ \sum_{j \in \mathcal{F}} F_{ij} + +where :math:`F_{ij} = \sum_{kl} f_{kl}`, and :math:`\mathcal{F}` indicates the full integration range. + +In order to support rebinning in the non-integration axis, the ``Integration`` algorithm +first rebins the data into the desired bins in the non-integration axis, +and 100 bins in the integration axis and then sums them as: + +.. math:: + C_i^{\mathrm{integration}} = \left. N_i \sum_{j \in \mathcal{F}} Y_{ij} w_{ij} \right/ \sum_{j \in \mathcal{F}} F_{ij} + +where :math:`N_i = \sum_{j \in \mathcal{D}} 1` is the number of :math:`j` bins at a given +:math:`i` *with non-zero fraction* (e.g. if the integration contains only regions with data +then :math:`N_i` = 100, otherwise :math:`N_i` will be less), +and :math:`w_{ij}` is the width along the :math:`j`\ th axis of the :math:`(i,j)`\ th bin. +The :math:`N_i` normalisation is needed because in the limiting case where all the fractions +:math:`F_{ij}` are unity, the denominator would be :math:`N_i`, so we recover the usual +expression for integrating over a distribution. +Note that as previously, :math:`\mathcal{D}` indicates the region within the integration +range with data (in this case equivalent to regions with non-zero fractions). + +Like with the **PSD** case there is thus an :math:`i` dependent scaling factor :math:`N_i w` +(assuming all the bins have the same width) between ``Cuts`` computed using the ``Rebin`` or +``Integration`` algorithm. +This scaling factor is a constant if the integration range includes only regions with data +(e.g. :math:`\mathcal{D} \equiv \mathcal{F}`), but will not be constant if the integration +overlaps regions without data. + +The difference is illustrated below: + +.. image:: images/math_ref/rebin_cuts.png + +The cuts have been normalised to the peak intensity so that the constant scaling factor between +the two algorithms factorises out. +In the top cut, integrating over :math:`6 \leq |Q| < 8 \mathrm{\AA}^{-1}` there are no regions +without data so the two cuts are equivalent except for a constant scaling factor. +In the bottom cut, integrating over :math:`8 \leq |Q| < 10 \mathrm{\AA}^{-1}` there is a large +region with no data, so now cuts from the two techniques differ markedly. +At :math:`E<0` meV Where the data covers the full integration range, we have :math:`N_i` = 100 +and the two cuts are equivalent. +As :math:`E` increases, :math:`N_i` decreases until at around 20 meV, it is :math:`N_i` = 50, +and we see that at that point the (normalised) ``Integration`` cut is half the intensity of +the (normalised) ``Rebin`` cut. + +A note on units +--------------- + +One advantage of inelastic neutron scattering over other techniques is that it is (relatively) +easy to normalise the measured data to absolute units. +At the ISIS Neutron and Muon Source if this normalisation is done, then the signal will be in +units of [milibarns per steradian per meV per formula unit] or [mb/sr/meV/f.u.]. + +An ``Integration`` over energy would then yield a differential cross-section in [mb/sr/f.u.], +whereas a ``Rebin`` over energy would leave the units unchanged at [mb/sr/meV/f.u.]. + +However, an ``Integration`` over :math:`|Q|` instead of energy will yield units of +[mb/Å/sr/meV/f.u.] rather [mb/meV/f.u.] and as such it may be more useful to perform an +average ``Rebin`` which will leave the units unchanged. + +Unfortunately, the input files read by MSlice do not indicate if the signal values saved +are in absolute units or not, so MSlice cannot automatically display the correct units on plots +- this is left to the user. + +A note on the regions of validity of the two algorithms +------------------------------------------------------- + +As can be seen in the example above, the ``Integration`` cut algorithm will produce low signals +where there is less data, whereas the ``Rebin`` cut algorithm will amplify the signals in such +regions. In effect, it assumes that the signal is constant across the integration range and +can be extrapolated over regions without data (so the only manifestation of the lack of data +are larger errorbars associated with these bins). + +This assumption *may* be valid for density-of-states (DOS) type cuts where one would expect +that the signal is approximately constant over :math:`|Q|` and only varies in energy. +Thus for these applications, it may be suitable to chose the ``Rebin`` algorithm, and to +extrapolate the high-energy, high-:math:`|Q|` regions which are kinematically inaccessible. + +Conversely, for integration over energy - for example over the elastic line to compute a +differential cross-section or over a finite energy crystal field excitation to obtain a +magnetic cross-section, the ``Integration`` algorithm should be chosen else the signal +in the cut will vary with the integration range and would not be a cross-section. diff --git a/mslice/app/mainwindow.py b/mslice/app/mainwindow.py index f63042b8a..249726740 100644 --- a/mslice/app/mainwindow.py +++ b/mslice/app/mainwindow.py @@ -263,9 +263,15 @@ def is_energy_conversion_allowed(self): def print_startup_notifications(self): #if notifications are required to be printed on mslice start up, add to list. - print_list = ["WARNING: The default cut algorithm in mslice has been changed from 'Rebin (average counts)' \ -to 'Intergration (summed counts)'. This is expected to result in different output values to those obtained historically. \n\ -For more information, please refer to documentation at: https://mantidproject.github.io/mslice/cutting.html"] + + # Disable this notification pending decision on changing the default + #print_list = ["\nWARNING: The default cut algorithm in mslice has been changed " \ + # "from 'Rebin (average counts)' to 'Intergration (summed counts)'.\n" \ + # "This is expected to result in different output values to those obtained historically.\n" \ + # "For more information, please refer to documentation at: " \ + # "https://mantidproject.github.io/mslice/cutting.html"] + print_list = [] for item in print_list: - print(item) + for strn in item.split('\n'): + self._console.execute(f'print("{strn}")', hidden=True) diff --git a/mslice/app/mainwindow.ui b/mslice/app/mainwindow.ui index dabf6997b..082ebd9e6 100644 --- a/mslice/app/mainwindow.ui +++ b/mslice/app/mainwindow.ui @@ -466,9 +466,6 @@ true - - true - Integration (Sum Counts) @@ -477,6 +474,9 @@ true + + true + Rebin (Averages Counts) diff --git a/mslice/cli/_mslice_commands.py b/mslice/cli/_mslice_commands.py index 26156b9bd..f63278855 100644 --- a/mslice/cli/_mslice_commands.py +++ b/mslice/cli/_mslice_commands.py @@ -167,7 +167,7 @@ def Slice(InputWorkspace, Axis1=None, Axis2=None, NormToOne=False): return get_slice_plotter_presenter().create_slice(workspace, x_axis, y_axis, None, None, NormToOne, DEFAULT_CMAP) -def Cut(InputWorkspace, CutAxis=None, IntegrationAxis=None, NormToOne=False, Algorithm='Integration'): +def Cut(InputWorkspace, CutAxis=None, IntegrationAxis=None, NormToOne=False, Algorithm='Rebin'): """ Cuts workspace. :param InputWorkspace: Workspace to cut. The parameter can be either a python @@ -183,6 +183,7 @@ def Cut(InputWorkspace, CutAxis=None, IntegrationAxis=None, NormToOne=False, Alg ', , , , cm-1', or ', , , meV' Recognised energy units are 'meV' (default) and 'cm-1' :param NormToOne: if True the cut will be normalized to one. + :param Algorithm: the cut algorithm to use. Either 'Rebin' (default) or 'Integration' :return: """ from mslice.app.presenters import get_cut_plotter_presenter diff --git a/mslice/cli/plotfunctions.py b/mslice/cli/plotfunctions.py index 95489e742..9781aba07 100644 --- a/mslice/cli/plotfunctions.py +++ b/mslice/cli/plotfunctions.py @@ -71,7 +71,7 @@ def errorbar(axes, workspace, *args, **kwargs): if not cur_canvas.manager.has_plot_handler(): cur_canvas.manager.add_cut_plot(presenter, workspace.name) cur_fig.canvas.draw() - cur_canvas.manager.update_axes(axes) + cur_canvas.manager.update_axes(axes, plot_over) cut = Cut(cut_axis, int_axis, intensity_min, intensity_max, workspace.norm_to_one, width='', algorithm=workspace.algorithm) diff --git a/mslice/filename.py b/mslice/filename.py new file mode 100644 index 000000000..e69de29bb diff --git a/mslice/models/cut/cut.py b/mslice/models/cut/cut.py index eac4ea5b1..0d5e6853d 100644 --- a/mslice/models/cut/cut.py +++ b/mslice/models/cut/cut.py @@ -2,7 +2,7 @@ class Cut(object): """Groups parameters needed to cut and validates them""" def __init__(self, cut_axis, integration_axis, intensity_start, intensity_end, norm_to_one=False, width=None, - algorithm='Integration'): + algorithm='Rebin'): self.cut_axis = cut_axis self.integration_axis = integration_axis self.intensity_start = intensity_start diff --git a/mslice/models/cut/cut_functions.py b/mslice/models/cut/cut_functions.py index 33ca55234..54eb8e443 100644 --- a/mslice/models/cut/cut_functions.py +++ b/mslice/models/cut/cut_functions.py @@ -13,7 +13,7 @@ def output_workspace_name(selected_workspace, integration_start, integration_end integration_end) + ")" -def compute_cut(workspace, cut_axis, integration_axis, is_norm, algo='Integration', store=True): +def compute_cut(workspace, cut_axis, integration_axis, is_norm, algo='Rebin', store=True): out_ws_name = output_workspace_name(workspace.name, integration_axis.start, integration_axis.end) cut = mantid_algorithms.Cut(OutputWorkspace=out_ws_name, store=store, InputWorkspace=workspace, CutAxis=cut_axis.to_dict(), IntegrationAxis=integration_axis.to_dict(), diff --git a/mslice/plotting/plot_window/cut_plot.py b/mslice/plotting/plot_window/cut_plot.py index 22a32a2c6..292ef8a45 100644 --- a/mslice/plotting/plot_window/cut_plot.py +++ b/mslice/plotting/plot_window/cut_plot.py @@ -34,6 +34,7 @@ def __init__(self, figure_manager, cut_plotter_presenter, workspace_name): self.plot_window = figure_manager.window self._canvas = self.plot_window.canvas self._cut_plotter_presenter = cut_plotter_presenter + self._plot_options_view = None self._lines_visible = {} self._legends_shown = True self._legends_visible = [] @@ -46,6 +47,8 @@ def __init__(self, figure_manager, cut_plotter_presenter, workspace_name): self._waterfall_cache = {} self._is_icut = False self._powder_lines = {} + self._datum_dirty = True + self._datum_cache = 0 def save_default_options(self): self.default_options = { @@ -112,7 +115,8 @@ def window_closing(self): self.plot_window.close() def plot_options(self): - CutPlotOptionsPresenter(CutPlotOptions(redraw_signal=self.plot_window.redraw), self) + self._plot_options_view = CutPlotOptions(self.plot_window, redraw_signal=self.plot_window.redraw) + return CutPlotOptionsPresenter(self._plot_options_view, self) def plot_clicked(self, x, y): bounds = self.calc_figure_boundaries() @@ -158,6 +162,7 @@ def update_legend(self, line_data=None): def change_axis_scale(self, xy_config): current_axis = self._canvas.figure.gca() + orig_y_scale_log = self.y_log if xy_config['x_log']: xmin = xy_config['x_range'][0] xdata = [ll.get_xdata() for ll in current_axis.get_lines()] @@ -189,6 +194,9 @@ def change_axis_scale(self, xy_config): self.x_range = xy_config['x_range'] self.y_range = xy_config['y_range'] + if xy_config['y_log'] or (xy_config['y_log'] != orig_y_scale_log): + self.update_bragg_peaks(refresh=True) + def get_line_options(self, line): index = self._get_line_index(line) if index >= 0: @@ -281,12 +289,15 @@ def set_line_options_by_index(self, line_index, line_options): def remove_line_by_index(self, line_index): containers = self._canvas.figure.gca().containers - container = containers[line_index] - container[0].remove() - for i in range(2): - for line in container[i + 1]: + if line_index < len(containers): + container = containers[line_index] + container[0].remove() + for line in container[1] + container[2]: line.remove() - containers.remove(container) + containers.remove(container) + + self._datum_dirty = True + self.update_bragg_peaks(refresh=True) def toggle_errorbar(self, line_index, line_options): container = self._canvas.figure.gca().containers[line_index] @@ -322,15 +333,38 @@ def set_is_icut(self, is_icut): def is_icut(self): return self._is_icut - def update_bragg_peaks(self): + def _get_overplot_datum(self): + if self._datum_dirty: + if not self.waterfall: + self._datum_cache = np.nanmedian([line.get_ydata() for line in self._canvas.figure.gca().get_lines() + if not self._cut_plotter_presenter.is_overplot(line)]) + else: + for line in self._canvas.figure.gca().get_lines(): + if not self._cut_plotter_presenter.is_overplot(line): + self._datum_cache = np.nanmedian([line.get_ydata()]) + break + + self._datum_dirty = False + + return self._datum_cache + + def update_bragg_peaks(self, refresh=False): if self.plot_window.action_aluminium.isChecked(): - self._cut_plotter_presenter.add_overplot_line(self.ws_name, 'Aluminium', False, cif=None) + refresh and self._cut_plotter_presenter.hide_overplot_line(None, 'Aluminium') + self._cut_plotter_presenter.add_overplot_line(self.ws_name, 'Aluminium', False, None, self.y_log, + self._get_overplot_datum()) if self.plot_window.action_copper.isChecked(): - self._cut_plotter_presenter.add_overplot_line(self.ws_name, 'Copper', False, cif=None) + refresh and self._cut_plotter_presenter.hide_overplot_line(None, 'Copper') + self._cut_plotter_presenter.add_overplot_line(self.ws_name, 'Copper', False, None, self.y_log, + self._get_overplot_datum()) if self.plot_window.action_niobium.isChecked(): - self._cut_plotter_presenter.add_overplot_line(self.ws_name, 'Niobium', False, cif=None) + refresh and self._cut_plotter_presenter.hide_overplot_line(None, 'Niobium') + self._cut_plotter_presenter.add_overplot_line(self.ws_name, 'Niobium', False, None, self.y_log, + self._get_overplot_datum()) if self.plot_window.action_tantalum.isChecked(): - self._cut_plotter_presenter.add_overplot_line(self.ws_name, 'Tantalum', False, cif=None) + refresh and self._cut_plotter_presenter.hide_overplot_line(None, 'Tantalum') + self._cut_plotter_presenter.add_overplot_line(self.ws_name, 'Tantalum', False, None, self.y_log, + self._get_overplot_datum()) self.update_legend() def save_icut(self): @@ -410,6 +444,9 @@ def toggle_waterfall(self): self._apply_offset(self.plot_window.waterfall_x, self.plot_window.waterfall_y) else: self._apply_offset(0., 0.) + + self._datum_dirty = True + self.update_bragg_peaks(refresh=True) self._canvas.draw() def _cache_line(self, line): @@ -431,14 +468,14 @@ def _apply_offset(self, x, y): path.vertices = np.add(self._waterfall_cache[line][index], np.array([[ind * x, ind * y], [ind * x, ind * y]])) - def on_newplot(self, ax): + def on_newplot(self, ax, plot_over): # This callback should be activated by a call to errorbar new_line = False line_containers = self._canvas.figure.gca().containers num_lines = len(line_containers) self.plot_window.action_waterfall.setEnabled(num_lines > 1) self.plot_window.toggle_waterfall_edit() - if not self._is_icut: + if not self._is_icut and not plot_over: self.plot_window.action_aluminium.setChecked(False) self.plot_window.action_copper.setChecked(False) self.plot_window.action_niobium.setChecked(False) @@ -456,6 +493,9 @@ def on_newplot(self, ax): if new_line and num_lines > 1: self.toggle_waterfall() + self._datum_dirty = True + self.update_bragg_peaks(refresh=True) + def generate_script(self, clipboard=False): try: generate_script(self.ws_name, None, self, self.plot_window, clipboard) diff --git a/mslice/plotting/plot_window/overplot_interface.py b/mslice/plotting/plot_window/overplot_interface.py index 08df43dbb..71390644c 100644 --- a/mslice/plotting/plot_window/overplot_interface.py +++ b/mslice/plotting/plot_window/overplot_interface.py @@ -34,7 +34,8 @@ def toggle_overplot_line(plot_handler, plotter_presenter, key, recoil, checked, plot_handler.manager.report_as_current() if checked: - plotter_presenter.add_overplot_line(plot_handler.ws_name, key, recoil, cif_file) + plotter_presenter.add_overplot_line(plot_handler.ws_name, key, recoil, cif_file, plot_handler.y_log, + plot_handler._get_overplot_datum()) else: plotter_presenter.hide_overplot_line(plot_handler.ws_name, key) diff --git a/mslice/plotting/plot_window/plot_figure_manager.py b/mslice/plotting/plot_window/plot_figure_manager.py index 1453d06f2..1857c5ab0 100644 --- a/mslice/plotting/plot_window/plot_figure_manager.py +++ b/mslice/plotting/plot_window/plot_figure_manager.py @@ -40,6 +40,7 @@ def __init__(self, number, current_figs): self._current_figs = current_figs self.window = PlotWindow(manager=weakref.proxy(self)) + self.window.setAttribute(Qt.WA_DeleteOnClose, True) self.window.resize(800, 600) self.plot_handler = None @@ -273,9 +274,9 @@ def update_grid(self): if self._ygrid: self.canvas.figure.gca().grid(True, axis='y') - def update_axes(self, ax): + def update_axes(self, ax, plot_over): if self.plot_handler is not None: - self.plot_handler.on_newplot(ax) + self.plot_handler.on_newplot(ax, plot_over) def move_window(self, x, y): center = QtWidgets.QDesktopWidget().screenGeometry().center() diff --git a/mslice/plotting/plot_window/plot_options.py b/mslice/plotting/plot_window/plot_options.py index 0a3b048f4..4e86ba219 100644 --- a/mslice/plotting/plot_window/plot_options.py +++ b/mslice/plotting/plot_window/plot_options.py @@ -2,13 +2,14 @@ from numpy import arange as np_arange from six import iteritems -import functools import qtpy.QtWidgets as QtWidgets from qtpy.QtCore import Signal from mslice.models.colors import named_cycle_colors, color_to_name from mslice.util.qt import load_ui +from mantidqt.utils.qt.line_edit_double_validator import LineEditDoubleValidator + class PlotOptionsDialog(QtWidgets.QDialog): @@ -21,10 +22,19 @@ class PlotOptionsDialog(QtWidgets.QDialog): yGridEdited = Signal() ok_clicked = Signal() - def __init__(self, parent=None, redraw_signal=None): + def __init__(self, parent, redraw_signal=None): QtWidgets.QDialog.__init__(self, parent) load_ui(__file__, 'plot_options.ui', self) + self.x_min_validator = LineEditDoubleValidator(self.lneXMin, 0.0) + self.lneXMin.setValidator(self.x_min_validator) + self.x_max_validator = LineEditDoubleValidator(self.lneXMax, 0.0) + self.lneXMax.setValidator(self.x_max_validator) + self.y_min_validator = LineEditDoubleValidator(self.lneYMin, 0.0) + self.lneYMin.setValidator(self.y_min_validator) + self.y_max_validator = LineEditDoubleValidator(self.lneYMax, 0.0) + self.lneYMax.setValidator(self.y_max_validator) + self.lneFigureTitle.editingFinished.connect(self.titleEdited) self.lneXAxisLabel.editingFinished.connect(self.xLabelEdited) self.lneYAxisLabel.editingFinished.connect(self.yLabelEdited) @@ -130,13 +140,18 @@ class SlicePlotOptions(PlotOptionsDialog): cRangeEdited = Signal() cLogEdited = Signal() - def __init__(self, redraw_signal=None): - super(SlicePlotOptions, self).__init__(redraw_signal=redraw_signal) + def __init__(self, parent, redraw_signal=None): + super(SlicePlotOptions, self).__init__(parent, redraw_signal=redraw_signal) self.chkXLog.hide() self.chkYLog.hide() self.cut_options.hide() self.setMaximumWidth(350) + self.c_min_validator = LineEditDoubleValidator(self.lneCMin, 0.0) + self.lneCMin.setValidator(self.c_min_validator) + self.c_max_validator = LineEditDoubleValidator(self.lneCMax, 0.0) + self.lneCMax.setValidator(self.c_max_validator) + self.lneCMin.editingFinished.connect(self.cRangeEdited) self.lneCMax.editingFinished.connect(self.cRangeEdited) self.chkLogarithmic.stateChanged.connect(self.cLogEdited) @@ -175,8 +190,8 @@ class CutPlotOptions(PlotOptionsDialog): showLegendsEdited = Signal() removed_line = Signal(int) - def __init__(self, redraw_signal=None): - super(CutPlotOptions, self).__init__(redraw_signal=redraw_signal) + def __init__(self, parent, redraw_signal=None): + super(CutPlotOptions, self).__init__(parent, redraw_signal=redraw_signal) self._line_widgets = [] self.groupBox_4.hide() @@ -186,9 +201,9 @@ def __init__(self, redraw_signal=None): self.showLegendsEdited.connect(self.disable_show_legend) def set_line_options(self, line_options): - for i in range(len(line_options)): - line_widget = LegendAndLineOptionsSetter(line_options[i], self.color_validator, self.show_legends) - line_widget.destroyed.connect(functools.partial(self.remove_line_widget, line_widget, i)) + for line_option in line_options: + line_widget = LegendAndLineOptionsSetter(line_option, self.color_validator, self.show_legends, + self.remove_line_widget) self.verticalLayout_legend.addWidget(line_widget) self._line_widgets.append(line_widget) @@ -215,7 +230,8 @@ def color_validator(self, selected): msg_box.exec_() return False - def remove_line_widget(self, selected, index): + def remove_line_widget(self, selected): + index = self._line_widgets.index(selected) self._line_widgets.remove(selected) self.removed_line.emit(index) @@ -263,9 +279,11 @@ class LegendAndLineOptionsSetter(QtWidgets.QWidget): inverse_styles = {v: k for k, v in iteritems(styles)} inverse_markers = {v: k for k, v in iteritems(markers)} - def __init__(self, line_options, color_validator, show_legends): + def __init__(self, line_options, color_validator, show_legends, remove_line_callback=None): super(LegendAndLineOptionsSetter, self).__init__() + self._deletion_callback = remove_line_callback + self.legend_text_label = QtWidgets.QLabel("Plot") self.legendText = QtWidgets.QLineEdit(self) self.legendText.setText(line_options['label']) @@ -362,6 +380,7 @@ def __init__(self, line_options, color_validator, show_legends): self.line_color.currentIndexChanged.connect(lambda selected: self.color_valid(selected)) self.delete_button = QtWidgets.QPushButton("Delete Line", self) row5.addWidget(self.delete_button) + self.delete_button.clicked.connect(self.deletion_callback) self.delete_button.clicked.connect(self.deleteLater) separator = QtWidgets.QFrame() @@ -369,6 +388,11 @@ def __init__(self, line_options, color_validator, show_legends): separator.setFrameShadow(QtWidgets.QFrame.Sunken) layout.addWidget(separator) + def deletion_callback(self): + if self._deletion_callback is not None: + self._deletion_callback(self) + self._deletion_callback = None + def color_valid(self, index): if self.color_validator is None: return diff --git a/mslice/plotting/plot_window/quick_options.py b/mslice/plotting/plot_window/quick_options.py index 8f92c734b..e07686c46 100644 --- a/mslice/plotting/plot_window/quick_options.py +++ b/mslice/plotting/plot_window/quick_options.py @@ -1,5 +1,7 @@ from mslice.plotting.plot_window.plot_options import LegendAndLineOptionsSetter +from mantidqt.utils.qt.line_edit_double_validator import LineEditDoubleValidator + from qtpy import QtWidgets from qtpy.QtCore import Signal @@ -8,8 +10,8 @@ class QuickOptions(QtWidgets.QDialog): ok_clicked = Signal() - def __init__(self): - super(QuickOptions, self).__init__() + def __init__(self, parent=None): + super(QuickOptions, self).__init__(parent) self.layout = QtWidgets.QVBoxLayout() self.setLayout(self.layout) self.ok_button = QtWidgets.QPushButton("OK", self) @@ -24,16 +26,22 @@ def __init__(self): class QuickAxisOptions(QuickOptions): - def __init__(self, target, existing_values, font_size, grid, log, redraw_signal): - super(QuickAxisOptions, self).__init__() + def __init__(self, parent, target, existing_values, font_size, grid, log, redraw_signal): + super(QuickAxisOptions, self).__init__(parent) self.setWindowTitle("Edit " + target) self.log = log self.min_label = QtWidgets.QLabel("Min:") self.min = QtWidgets.QLineEdit() self.min.setText(str(existing_values[0])) + self.min_validator = LineEditDoubleValidator(self.min, str(existing_values[0])) + self.min.setValidator(self.min_validator) + self.max_label = QtWidgets.QLabel("Max:") self.max = QtWidgets.QLineEdit() self.max.setText(str(existing_values[1])) + self.max_validator = LineEditDoubleValidator(self.max, str(existing_values[1])) + self.max.setValidator(self.max_validator) + self.font_size_label = QtWidgets.QLabel("Font Size:") self.font_size = QtWidgets.QDoubleSpinBox() self.decimals = 1 @@ -91,13 +99,14 @@ def grid_state(self): def is_kept_open(self): return self.keep_open.isChecked() + class QuickLabelOptions(QuickOptions): ok_clicked = Signal() cancel_clicked = Signal() - def __init__(self, label, redraw_signal): - super(QuickLabelOptions, self).__init__() + def __init__(self, parent, label, redraw_signal): + super(QuickLabelOptions, self).__init__(parent) self.setWindowTitle("Edit " + label.get_text()) self.line_edit = QtWidgets.QLineEdit() self.line_edit.setText(label.get_text()) @@ -131,13 +140,14 @@ def _ok_clicked(self): self.redraw_signal.emit() self.accept() + class QuickLineOptions(QuickOptions): ok_clicked = Signal() cancel_clicked = Signal() - def __init__(self, line_options, show_legends): - super(QuickLineOptions, self).__init__() + def __init__(self, parent, line_options, show_legends): + super(QuickLineOptions, self).__init__(parent) self.setWindowTitle("Edit line") self.line_widget = LegendAndLineOptionsSetter(line_options, None, show_legends) self.layout.addWidget(self.line_widget) diff --git a/mslice/plotting/plot_window/slice_plot.py b/mslice/plotting/plot_window/slice_plot.py index def82c68e..ed15a1378 100644 --- a/mslice/plotting/plot_window/slice_plot.py +++ b/mslice/plotting/plot_window/slice_plot.py @@ -156,7 +156,7 @@ def window_closing(self): pass def plot_options(self): - SlicePlotOptionsPresenter(SlicePlotOptions(redraw_signal=self.plot_window.redraw), self) + SlicePlotOptionsPresenter(SlicePlotOptions(self.plot_window, redraw_signal=self.plot_window.redraw), self) def plot_clicked(self, x, y): bounds = self.calc_figure_boundaries() @@ -174,7 +174,6 @@ def object_clicked(self, target): return elif isinstance(target, Text): quick_options(target, self, redraw_signal=self.plot_window.redraw) - else: quick_options(target, self) self.update_legend() @@ -575,3 +574,11 @@ def is_changed(self, item): if self.default_options is None: return False return self.default_options[item] != getattr(self, item) + + @property + def y_log(self): # needed for interface consistency with cut plot + return False + + @staticmethod + def _get_overplot_datum(): # needed for interface consistency with cut plot + return 0 diff --git a/mslice/presenters/cut_plotter_presenter.py b/mslice/presenters/cut_plotter_presenter.py index d8d39db85..df3fd21cf 100644 --- a/mslice/presenters/cut_plotter_presenter.py +++ b/mslice/presenters/cut_plotter_presenter.py @@ -11,6 +11,8 @@ from mslice.models.powder.powder_functions import compute_powder_line import warnings +BRAGG_SIZE_ON_AXES = 0.15 + class CutPlotterPresenter(PresenterUtility): @@ -97,8 +99,25 @@ def hide_overplot_line(self, workspace, key): line = cache.pop(key) remove_line(line) - def add_overplot_line(self, workspace_name, key, recoil, cif=None): - recoil = False + @staticmethod + def _get_log_bragg_y_coords(size, portion_of_axes, datum): + datum = 0.001 if datum == 0 else datum + y1, y2 = plt.gca().get_ylim() + if (y2 > 0 and y1 > 0) or (y2 < 0 and y1 < 0): + total_steps = np.log10(y2 / y1) + elif y1 < 0: + total_steps_up = np.log10(y2) + 1 if abs(y2) >= 1 else abs(y2) + total_steps_down = np.log10(-y1) + 1 if abs(y1) >= 1 else abs(y1) + total_steps = total_steps_up + total_steps_down + else: + y1 = 1 if y1 == 0 else y1 + y2 = 1 if y2 == 0 else y2 + total_steps = np.log10(y2 / y1) + 1 + + adj_factor = total_steps * portion_of_axes / 2 + return np.resize(np.array([10 ** adj_factor, 10 ** (-adj_factor), np.nan]), size) * datum + + def add_overplot_line(self, workspace_name, key, recoil, cif=None, y_has_logarithmic=None, datum=0): cache = self._cut_cache_dict[plt.gca()][0] cache.rotated = not is_twotheta(cache.cut_axis.units) and not is_momentum(cache.cut_axis.units) try: @@ -115,9 +134,13 @@ def add_overplot_line(self, workspace_name, key, recoil, cif=None): q_axis = cache.cut_axis x, y = compute_powder_line(workspace_name, q_axis, key, cif_file=cif) try: - y = np.array(y) * (scale_fac / np.nanmax(y)) + if not y_has_logarithmic: + y = np.array(y) * scale_fac / np.nanmax(y) + datum + else: + y = self._get_log_bragg_y_coords(len(y), BRAGG_SIZE_ON_AXES, datum) + self._overplot_cache[key] = plot_overplot_line(x, y, key, recoil, cache) - except ValueError: + except (ValueError, IndexError): warnings.warn("No Bragg peak found.") def store_icut(self, icut): @@ -137,3 +160,6 @@ def update_main_window(self): def workspace_selection_changed(self): pass + + def is_overplot(self, line): + return line in self._overplot_cache.values() diff --git a/mslice/presenters/data_loader_presenter.py b/mslice/presenters/data_loader_presenter.py index e8fde8bc4..77b636ddb 100644 --- a/mslice/presenters/data_loader_presenter.py +++ b/mslice/presenters/data_loader_presenter.py @@ -59,15 +59,16 @@ def _load_ws(self, file_paths, ws_names, force_overwrite): allChecked = True else: load(filename=file_paths[i], output_workspace=ws_name) + + if not allChecked: + allChecked = self.check_efixed(ws_name, multi) + else: + apply_fixed_final_energy_to_a_valid_workspace(ws_name, self._EfCache) except (ValueError, TypeError) as e: self._view.error_loading_workspace(e) except RuntimeError: not_opened.append(ws_name) else: - if not allChecked: - allChecked = self.check_efixed(ws_name, multi) - else: - apply_fixed_final_energy_to_a_valid_workspace(ws_name, self._EfCache) if self._main_presenter is not None: self._main_presenter.show_workspace_manager_tab() self._main_presenter.show_tab_for_workspace(get_workspace_handle(ws_name)) diff --git a/mslice/presenters/quick_options_presenter.py b/mslice/presenters/quick_options_presenter.py index cdfb5d3eb..a3b722328 100644 --- a/mslice/presenters/quick_options_presenter.py +++ b/mslice/presenters/quick_options_presenter.py @@ -7,33 +7,34 @@ def quick_options(target, model, has_logarithmic=None, redraw_signal=None): """Find which quick_options to use based on type of target""" if isinstance(target, text.Text): - quick_label_options(target, redraw_signal) + quick_label_options(model.plot_window, target, redraw_signal) elif isinstance(target, string_types): - return quick_axis_options(target, model, has_logarithmic, redraw_signal) + return quick_axis_options(model.plot_window, target, model, has_logarithmic, redraw_signal) else: - quick_line_options(target, model) + quick_line_options(model.plot_window, target, model) -def quick_label_options(target, redraw_signal=None): - view = QuickLabelOptions(target, redraw_signal) +def quick_label_options(parent, target, redraw_signal=None): + view = QuickLabelOptions(parent, target, redraw_signal) view.ok_clicked.connect(lambda: _set_label_options(view, target)) view.show() return view -def quick_axis_options(target, model, has_logarithmic=None, redraw_signal=None): +def quick_axis_options(parent, target, model, has_logarithmic=None, redraw_signal=None): if target[:1] == 'x' or target[:1] == 'y': grid = getattr(model, target[:-5] + 'grid') else: grid = None - view = QuickAxisOptions(target, getattr(model, target), getattr(model, target + '_font_size'), grid, has_logarithmic, redraw_signal) + view = QuickAxisOptions(parent, target, getattr(model, target), getattr(model, target + '_font_size'), grid, + has_logarithmic, redraw_signal) view.ok_clicked.connect(lambda: _set_axis_options(view, target, model, has_logarithmic, grid)) view.show() return view -def quick_line_options(target, model): - view = QuickLineOptions(model.get_line_options(target), model.show_legends) +def quick_line_options(parent, target, model): + view = QuickLineOptions(parent, model.get_line_options(target), model.show_legends) _run_quick_options(view, _set_line_options, model, target) diff --git a/mslice/presenters/slice_plotter_presenter.py b/mslice/presenters/slice_plotter_presenter.py index ecfa5d318..7826e9e7a 100644 --- a/mslice/presenters/slice_plotter_presenter.py +++ b/mslice/presenters/slice_plotter_presenter.py @@ -92,7 +92,7 @@ def hide_overplot_line(self, workspace, key): line = cache.overplot_lines.pop(key) remove_line(line) - def add_overplot_line(self, workspace_name, key, recoil, cif=None): + def add_overplot_line(self, workspace_name, key, recoil, cif=None, y_has_logarithmic=None, datum=None): cache = self._slice_cache[workspace_name] if recoil: x, y = compute_recoil_line(workspace_name, cache.momentum_axis, key) diff --git a/mslice/scripting/helperfunctions.py b/mslice/scripting/helperfunctions.py index 07d517369..b33159675 100644 --- a/mslice/scripting/helperfunctions.py +++ b/mslice/scripting/helperfunctions.py @@ -176,7 +176,7 @@ def add_cut_lines_with_width(errorbars, script_lines, cuts): cut_start, cut_end = integration_start, min(integration_start + cut.width, integration_end) intensity_range = (cut.intensity_start, cut.intensity_end) norm_to_one = cut.norm_to_one - algo_str = '' if 'Integration' in cut.algorithm else ', Algorithm="{}"'.format(cut.algorithm) + algo_str = '' if 'Rebin' in cut.algorithm else ', Algorithm="{}"'.format(cut.algorithm) while cut_start != cut_end and index < len(errorbars): cut.integration_axis.start = cut_start diff --git a/mslice/tests/command_line_test.py b/mslice/tests/command_line_test.py index 736f43b28..286956d1a 100644 --- a/mslice/tests/command_line_test.py +++ b/mslice/tests/command_line_test.py @@ -148,13 +148,13 @@ def test_cut_non_psd(self, get_cpp, is_gui): get_cpp.return_value = CutPlotterPresenter() workspace = self.create_workspace('test_workspace_cut_cli') #test rebin - rebin_result = Cut(workspace, Algorithm='Rebin') + rebin_result = Cut(workspace) rebin_signal = rebin_result.get_signal() self.assertEqual(type(rebin_result), HistogramWorkspace) self.assertAlmostEqual(1.129, rebin_signal[5], 2) self.assertAlmostEqual(1.375, rebin_signal[8], 2) #test integration - int_result = Cut(workspace) + int_result = Cut(workspace, Algorithm='Integration') int_signal = int_result.get_signal() self.assertAlmostEqual(2.258, int_signal[5], 2) self.assertAlmostEqual(1.375, int_signal[8], 2) @@ -178,14 +178,14 @@ def test_cut_psd(self, get_cpp, is_gui): get_cpp.return_value = CutPlotterPresenter() workspace = self.create_pixel_workspace('test_workspace_cut_psd_cli') #test rebin - rebin_result = Cut(workspace, Algorithm='Rebin') + rebin_result = Cut(workspace) rebin_signal = rebin_result.get_signal() self.assertEqual(type(rebin_result), HistogramWorkspace) self.assertEqual(128, rebin_signal[0]) self.assertEqual(192, rebin_signal[29]) self.assertEqual(429, rebin_signal[15]) #test Integration - int_result = Cut(workspace) + int_result = Cut(workspace, Algorithm='Integration') int_signal = int_result.get_signal() self.assertEqual(type(int_result), HistogramWorkspace) self.assertAlmostEqual(64, int_signal[0], 2) diff --git a/mslice/tests/cut_plot_test.py b/mslice/tests/cut_plot_test.py index ba2096370..2b4dfbfed 100644 --- a/mslice/tests/cut_plot_test.py +++ b/mslice/tests/cut_plot_test.py @@ -57,7 +57,7 @@ def test_change_scale_log(self): self.axes.get_lines = MagicMock(return_value=[line]) self.canvas.figure.gca = MagicMock(return_value=self.axes) xy_config = {'x_log': True, 'y_log': True, 'x_range': (0, 20), 'y_range': (1, 7)} - + self.cut_plot.update_bragg_peaks = MagicMock() self.cut_plot.change_axis_scale(xy_config) if LooseVersion(mpl_version) < LooseVersion('3.3'): @@ -67,6 +67,7 @@ def test_change_scale_log(self): self.axes.set_xscale.assert_called_once_with('symlog', linthresh=10.0) self.axes.set_yscale.assert_called_once_with('symlog', linthresh=1.0) + self.cut_plot.update_bragg_peaks.assert_called_once_with(refresh=True) self.assertEqual(self.cut_plot.x_range, (0, 20)) self.assertEqual(self.cut_plot.y_range, (1, 7)) @@ -99,6 +100,7 @@ def test_update_legend(self): def test_waterfall(self): self.cut_plot._apply_offset = MagicMock() + self.cut_plot.update_bragg_peaks = MagicMock() self.cut_plot.waterfall = True self.cut_plot.waterfall_x = 1 self.cut_plot.waterfall_y = 2 @@ -107,3 +109,4 @@ def test_waterfall(self): self.cut_plot.waterfall = False self.cut_plot.toggle_waterfall() self.cut_plot._apply_offset.assert_called_with(0, 0) + self.cut_plot.update_bragg_peaks.assert_called_with(refresh=True) diff --git a/mslice/tests/quick_options_test.py b/mslice/tests/quick_options_test.py index 87b36bc34..4469ac791 100644 --- a/mslice/tests/quick_options_test.py +++ b/mslice/tests/quick_options_test.py @@ -28,6 +28,7 @@ class QuickOptionsTest(unittest.TestCase): def setUp(self): self.model = MagicMock() + self.model.plot_window = None @patch.object(QuickLabelOptions, '__init__', lambda x, y: None) @patch.object(QuickLabelOptions, 'exec_', lambda x: None) @@ -35,7 +36,7 @@ def setUp(self): def test_label(self, label_options_mock): target = Mock(spec=text.Text) quick_options(target, self.model) - label_options_mock.assert_called_once_with(target, None) + label_options_mock.assert_called_once_with(None, target, None) @patch.object(QuickLineOptions, '__init__', lambda x, y, z: None) @patch.object(QuickLineOptions, 'exec_', lambda x: None) @@ -43,7 +44,7 @@ def test_label(self, label_options_mock): def test_line(self, line_options_mock): target = Line2D([], [], 3.0, '-', 'red', 'o', label='label1') quick_options(target, self.model) - line_options_mock.assert_called_once_with(target, self.model) + line_options_mock.assert_called_once_with(None, target, self.model) @patch.object(QuickLineOptions, '__init__', lambda x, y, z: None) @patch.object(QuickLineOptions, 'exec_', lambda x: None) @@ -51,7 +52,7 @@ def test_line(self, line_options_mock): def test_axis(self, axis_options_mock): target = "x_axis" quick_options(target, self.model) - axis_options_mock.assert_called_once_with(target, self.model, None, None) + axis_options_mock.assert_called_once_with(None, target, self.model, None, None) @patch('mslice.plotting.plot_window.cut_plot.CutPlot.show_legends', new_callable=PropertyMock(return_value=True)) @patch('mslice.presenters.quick_options_presenter.QuickLineOptions') @@ -63,13 +64,13 @@ def test_line_slice(self, qlo_mock, show_legends): window.canvas = canvas slice_plotter = MagicMock() model = SlicePlot(plot_figure, slice_plotter, 'workspace') + model.plot_window = None qlo_mock, target = setup_line_values(qlo_mock) quick_options(target, model) # check view is called with existing line parameters - qlo_mock.assert_called_with( - {'shown': None, 'color': '#d62728', 'label': u'label1', 'style': '-', 'width': '3.0', - 'marker': 'o', 'legend': None, 'error_bar': None}, True) + qlo_mock.assert_called_with(None, {'shown': None, 'color': '#d62728', 'label': u'label1', 'style': '-', + 'width': '3.0', 'marker': 'o', 'legend': None, 'error_bar': None}, True) # check model is updated with parameters from view self.assertDictEqual(model.get_line_options(target), {'shown': None, 'color': '#1f77b4', 'label': u'label2', @@ -86,6 +87,7 @@ def test_line_cut(self, qlo_mock, show_legends): window.canvas = canvas cut_plotter = MagicMock() model = CutPlot(plot_figure, cut_plotter, 'workspace') + type(model).plot_window = PropertyMock(return_value=None) qlo_mock, target = setup_line_values(qlo_mock) container = ErrorbarContainer([target], has_yerr=True, label='label1') @@ -96,15 +98,14 @@ def test_line_cut(self, qlo_mock, show_legends): quick_options(target, model) # check view is called with existing line parameters - qlo_mock.assert_called_with( - {'shown': True, 'color': '#d62728', 'label': u'label1', 'style': '-', 'width': '3.0', - 'marker': 'o', 'legend': True, 'error_bar': False}, True) + qlo_mock.assert_called_with(None, {'shown': True, 'color': '#d62728', 'label': u'label1', 'style': '-', + 'width': '3.0', 'marker': 'o', 'legend': True, 'error_bar': False}, True) # check model is updated with parameters from view self.assertDictEqual(model.get_line_options(target), {'shown': True, 'color': '#1f77b4', 'label': u'label2', 'style': '--', 'width': '5.0', 'marker': '.', 'legend': True, 'error_bar': False}) - @patch.object(QuickAxisOptions, '__init__', lambda t, u, v, w, x, y, z: None) + @patch.object(QuickAxisOptions, '__init__', lambda p, t, u, v, w, x, y, z: None) @patch.object(QuickAxisOptions, 'range_min', PropertyMock(return_value='0')) @patch.object(QuickAxisOptions, 'range_max', PropertyMock(return_value='10')) @patch.object(QuickAxisOptions, 'grid_state', PropertyMock(return_value=True)) @@ -129,6 +130,7 @@ def setUp(self): self.model.canvas.draw = MagicMock() x_grid = PropertyMock(return_value=False) self.model.x_grid = x_grid + self.model.plot_window = None range_min = PropertyMock(return_value=5) type(self.view).range_min = range_min @@ -139,7 +141,7 @@ def setUp(self): def test_accept(self, quick_axis_options_view): quick_axis_options_view.return_value = self.view - qopt = quick_axis_options('x_range', self.model) + qopt = quick_axis_options(None, 'x_range', self.model) qopt.redraw_signal = PropertyMock() qopt.ok_clicked.connect.call_args[0][0]() # Call the connected signal directly self.assertEquals(self.model.x_range, (5, 10)) @@ -148,7 +150,7 @@ def test_accept(self, quick_axis_options_view): def test_reject(self, quick_axis_options_view): quick_axis_options_view.return_value = self.view self.view.set_range = Mock() - qopt = quick_axis_options('x_range', self.model) + qopt = quick_axis_options(None, 'x_range', self.model) qopt.redraw_signal = PropertyMock() qopt.reject() self.view.set_range.assert_not_called() @@ -160,7 +162,7 @@ def test_colorbar(self, quick_axis_options_view): colorbar_log = PropertyMock() type(self.model).colorbar_log = colorbar_log self.view.log_scale.isChecked = Mock() - qopt = quick_axis_options('colorbar_range', self.model, True) + qopt = quick_axis_options(None, 'colorbar_range', self.model, True) qopt.redraw_signal = PropertyMock() qopt.ok_clicked.connect.call_args[0][0]() # Call the connected signal directly self.view.log_scale.isChecked.assert_called_once() @@ -182,7 +184,7 @@ def setUp(self): @patch('mslice.presenters.quick_options_presenter._set_font_size') def test_accept(self, set_font_size, set_label, quick_label_options_view): quick_label_options_view.return_value = self.view - qopt = quick_label_options('label') + qopt = quick_label_options(None, 'label') qopt.redraw_signal = PropertyMock() qopt.ok_clicked.connect.call_args[0][0]() # Call the connected signal directly assert set_label.called @@ -191,7 +193,7 @@ def test_accept(self, set_font_size, set_label, quick_label_options_view): def test_reject(self, quick_label_options_view): quick_label_options_view.return_value = self.view self.view.set_range = Mock() - qopt = quick_label_options('label') + qopt = quick_label_options(None, 'label') qopt.redraw_signal = PropertyMock() qopt.reject() self.target.set_text.assert_not_called() @@ -213,6 +215,7 @@ def setUp(self): self.model = MagicMock() self.target = MagicMock() self.model.canvas.draw = MagicMock() + self.model.plot_window = None self.target.set_color = MagicMock() self.target.set_linestyle = MagicMock() self.target.set_linewidth = MagicMock() diff --git a/mslice/tests/slice_plot_test.py b/mslice/tests/slice_plot_test.py index 65fcef24f..2aef4aff1 100644 --- a/mslice/tests/slice_plot_test.py +++ b/mslice/tests/slice_plot_test.py @@ -81,7 +81,7 @@ def test_arbitrary_recoil_line(self, qt_get_int_mock): self.plot_figure.action_arbitrary_nuclei.isChecked = MagicMock(return_value=True) self.slice_plot.arbitrary_recoil_line() - self.slice_plotter.add_overplot_line.assert_called_once_with('workspace', 5, True, None) + self.slice_plotter.add_overplot_line.assert_called_once_with('workspace', 5, True, None, False, 0) @patch('mslice.plotting.plot_window.slice_plot.QtWidgets.QInputDialog.getInt') def test_arbitrary_recoil_line_cancelled(self, qt_get_int_mock): diff --git a/mslice/tests/workspace_algorithms_test.py b/mslice/tests/workspace_algorithms_test.py new file mode 100644 index 000000000..775795cf3 --- /dev/null +++ b/mslice/tests/workspace_algorithms_test.py @@ -0,0 +1,45 @@ +from __future__ import (absolute_import, division, print_function) + +import unittest + +from mslice.models.workspacemanager.workspace_algorithms import process_limits +from mslice.util.mantid.mantid_algorithms import AppendSpectra, CreateSimulationWorkspace, CreateWorkspace + +from mantid.api import AnalysisDataService + + +class WorkspaceAlgorithmsTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.direct_workspace = CreateSimulationWorkspace(OutputWorkspace='MAR_workspace', Instrument='MAR', + BinParams=[-10, 1, 10], UnitX='DeltaE') + cls.direct_workspace.e_mode = "Direct" + cls.direct_workspace.e_fixed = 1.1 + + cls.indirect_workspace = CreateSimulationWorkspace(OutputWorkspace='OSIRIS_workspace', Instrument='OSIRIS', + BinParams=[-10, 1, 10], UnitX='DeltaE') + cls.indirect_workspace.e_mode = "Indirect" + cls.indirect_workspace.e_fixed = 1.1 + + CreateWorkspace(DataX=cls.indirect_workspace.raw_ws.readX(0), DataY=cls.indirect_workspace.raw_ws.readY(0), + ParentWorkspace='OSIRIS_workspace', UnitX='DeltaE', OutputWorkspace="extra_spectra_ws") + + cls.extended_workspace = AppendSpectra(InputWorkspace1='OSIRIS_workspace', InputWorkspace2='extra_spectra_ws', + OutputWorkspace='extended_workspace') + cls.extended_workspace.e_mode = "Indirect" + cls.extended_workspace.e_fixed = 1.1 + + @classmethod + def tearDownClass(cls) -> None: + AnalysisDataService.clear() + + def test_process_limits_does_not_fail_for_direct_data(self): + process_limits(self.direct_workspace) + + def test_process_limits_does_not_fail_for_indirect_data(self): + process_limits(self.indirect_workspace) + + def test_process_limits_will_raise_a_runtime_error_for_a_workspace_where_not_every_histogram_has_a_detector(self): + with self.assertRaises(RuntimeError): + process_limits(self.extended_workspace) diff --git a/mslice/widgets/cut/cut.py b/mslice/widgets/cut/cut.py index fcafa22e6..26bb19e01 100644 --- a/mslice/widgets/cut/cut.py +++ b/mslice/widgets/cut/cut.py @@ -46,8 +46,8 @@ def __init__(self, parent=None, *args, **kwargs): self.set_validators() self._en = EnergyUnits('meV') self._en_default = 'meV' - self._cut_alg_default = 'Integration' - self.set_cut_algorithm('Integration') + self._cut_alg_default = 'Rebin' + self.set_cut_algorithm('Rebin') self.cmbCutEUnits.currentIndexChanged.connect(self._changed_unit) self.cmbCutAlg.currentIndexChanged.connect(self.cut_algorithm_changed)