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)