diff --git a/Framework/PythonInterface/mantid/dataobjects/CMakeLists.txt b/Framework/PythonInterface/mantid/dataobjects/CMakeLists.txt
index 4cb6b56c17b4dda52d2cebfe8ab01c326c6a8055..c7c1d77df86ccc388ac54a2dbafb07e6f87f3c2f 100644
--- a/Framework/PythonInterface/mantid/dataobjects/CMakeLists.txt
+++ b/Framework/PythonInterface/mantid/dataobjects/CMakeLists.txt
@@ -10,6 +10,7 @@ set ( MODULE_TEMPLATE src/dataobjects.cpp.in )
 set ( EXPORT_FILES
   src/Exports/EventList.cpp
   src/Exports/EventWorkspace.cpp
+  src/Exports/EventWorkspaceProperty.cpp
   src/Exports/Workspace2D.cpp
   src/Exports/RebinnedOutput.cpp
   src/Exports/SpecialWorkspace2D.cpp
diff --git a/Framework/PythonInterface/mantid/dataobjects/src/Exports/EventWorkspaceProperty.cpp b/Framework/PythonInterface/mantid/dataobjects/src/Exports/EventWorkspaceProperty.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..18f1de77c03eabd85ac0b5f8af6a19fcb2779541
--- /dev/null
+++ b/Framework/PythonInterface/mantid/dataobjects/src/Exports/EventWorkspaceProperty.cpp
@@ -0,0 +1,14 @@
+#include "MantidPythonInterface/api/WorkspacePropertyExporter.h"
+#include "MantidPythonInterface/kernel/GetPointer.h"
+#include "MantidDataObjects/EventWorkspace.h"
+
+using Mantid::DataObjects::EventWorkspace;
+using Mantid::API::WorkspaceProperty;
+
+GET_POINTER_SPECIALIZATION(WorkspaceProperty<EventWorkspace>)
+
+void export_EventWorkspaceProperty() {
+  using Mantid::PythonInterface::WorkspacePropertyExporter;
+
+  WorkspacePropertyExporter<EventWorkspace>::define("EventWorkspaceProperty");
+}
\ No newline at end of file
diff --git a/Framework/PythonInterface/plugins/algorithms/WorkflowAlgorithms/SANS/SANSConvertToQ.py b/Framework/PythonInterface/plugins/algorithms/WorkflowAlgorithms/SANS/SANSConvertToQ.py
new file mode 100644
index 0000000000000000000000000000000000000000..89595d544000e6f35fa751938ebfb3fe3f54f04b
--- /dev/null
+++ b/Framework/PythonInterface/plugins/algorithms/WorkflowAlgorithms/SANS/SANSConvertToQ.py
@@ -0,0 +1,247 @@
+# pylint: disable=too-few-public-methods
+
+""" Converts a workspace from wavelengths to momentum transfer."""
+from __future__ import (absolute_import, division, print_function)
+from mantid.kernel import (Direction, PropertyManagerProperty, CompositeValidator)
+from mantid.api import (DataProcessorAlgorithm, MatrixWorkspaceProperty, AlgorithmFactory, PropertyMode,
+                        WorkspaceUnitValidator)
+
+from sans.common.constants import EMPTY_NAME
+from sans.common.enums import (ReductionDimensionality, RangeStepType)
+from sans.common.general_functions import (create_unmanaged_algorithm, append_to_sans_file_tag)
+from sans.state.state_base import create_deserialized_sans_state_from_property_manager
+from sans.algorithm_detail.q_resolution_calculator import QResolutionCalculatorFactory
+
+
+class SANSConvertToQ(DataProcessorAlgorithm):
+    def category(self):
+        return 'SANS\\ConvertToQ'
+
+    def summary(self):
+        return 'Converts a SANS workspace to momentum transfer.'
+
+    def PyInit(self):
+        # ----------
+        # INPUT
+        # ----------
+        # State
+        self.declareProperty(PropertyManagerProperty('SANSState'),
+                             doc='A property manager which fulfills the SANSState contract.')
+
+        # Main workspace
+        workspace_validator = CompositeValidator()
+        workspace_validator.add(WorkspaceUnitValidator("Wavelength"))
+        self.declareProperty(MatrixWorkspaceProperty("InputWorkspace", '',
+                                                     optional=PropertyMode.Mandatory, direction=Direction.Input,
+                                                     validator=workspace_validator),
+                             doc='The main input workspace.')
+
+        # Adjustment workspaces
+        self.declareProperty(MatrixWorkspaceProperty("InputWorkspaceWavelengthAdjustment", '',
+                                                     optional=PropertyMode.Optional, direction=Direction.Input,
+                                                     validator=workspace_validator),
+                             doc='The workspace which contains only wavelength-specific adjustments, ie which affects '
+                                 'all spectra equally.')
+        self.declareProperty(MatrixWorkspaceProperty("InputWorkspacePixelAdjustment", '',
+                                                     optional=PropertyMode.Optional, direction=Direction.Input),
+                             doc='The workspace which contains only pixel-specific adjustments, ie which affects '
+                                 'all bins within a spectrum equally.')
+        self.declareProperty(MatrixWorkspaceProperty("InputWorkspaceWavelengthAndPixelAdjustment", '',
+                                                     optional=PropertyMode.Optional, direction=Direction.Input,
+                                                     validator=workspace_validator),
+                             doc='The workspace which contains wavelength- and pixel-specific adjustments.')
+
+        self.declareProperty('OutputParts', defaultValue=False,
+                             direction=Direction.Input,
+                             doc='Set to true to output two additional workspaces which will have the names '
+                                 'OutputWorkspace_sumOfCounts OutputWorkspace_sumOfNormFactors. The division '
+                                 'of _sumOfCounts and _sumOfNormFactors equals the workspace returned by the '
+                                 'property OutputWorkspace (default is false).')
+
+        # ----------
+        # Output
+        # ----------
+        self.declareProperty(MatrixWorkspaceProperty("OutputWorkspace", '',
+                                                     optional=PropertyMode.Mandatory, direction=Direction.Output),
+                             doc="The reduced workspace")
+
+    def PyExec(self):
+        # Read the state
+        state_property_manager = self.getProperty("SANSState").value
+        state = create_deserialized_sans_state_from_property_manager(state_property_manager)
+
+        # Perform either a 1D reduction or a 2D reduction
+        convert_to_q = state.convert_to_q
+        reduction_dimensionality = convert_to_q.reduction_dimensionality
+        if reduction_dimensionality is ReductionDimensionality.OneDim:
+            output_workspace, sum_of_counts_workspace, sum_of_norms_workspace = self._run_q_1d(state)
+        else:
+            output_workspace, sum_of_counts_workspace, sum_of_norms_workspace = self._run_q_2d(state)
+
+        # Set the output
+        append_to_sans_file_tag(output_workspace, "_convertq")
+        self.setProperty("OutputWorkspace", output_workspace)
+        if sum_of_counts_workspace and sum_of_norms_workspace:
+            self._set_partial_workspaces(sum_of_counts_workspace, sum_of_norms_workspace)
+
+    def _run_q_1d(self, state):
+        data_workspace = self.getProperty("InputWorkspace").value
+        wavelength_adjustment_workspace = self.getProperty("InputWorkspaceWavelengthAdjustment").value
+        pixel_adjustment_workspace = self.getProperty("InputWorkspacePixelAdjustment").value
+        wavelength_and_pixel_adjustment_workspace = self.getProperty("InputWorkspaceWavelengthAndPixelAdjustment").value
+
+        # Get QResolution
+        convert_to_q = state.convert_to_q
+        q_resolution_factory = QResolutionCalculatorFactory()
+        q_resolution_calculator = q_resolution_factory.create_q_resolution_calculator(state)
+        q_resolution_workspace = q_resolution_calculator.get_q_resolution_workspace(convert_to_q, data_workspace)
+
+        output_parts = self.getProperty("OutputParts").value
+
+        # Extract relevant settings
+        q_binning = convert_to_q.q_1d_rebin_string
+        use_gravity = convert_to_q.use_gravity
+        gravity_extra_length = convert_to_q.gravity_extra_length
+        radius_cutoff = convert_to_q.radius_cutoff * 1000.  # Q1D2 expects the radius cutoff to be in mm
+        wavelength_cutoff = convert_to_q.wavelength_cutoff
+
+        q1d_name = "Q1D"
+        q1d_options = {"DetBankWorkspace": data_workspace,
+                       "OutputWorkspace": EMPTY_NAME,
+                       "OutputBinning": q_binning,
+                       "AccountForGravity": use_gravity,
+                       "RadiusCut": radius_cutoff,
+                       "WaveCut": wavelength_cutoff,
+                       "OutputParts": output_parts,
+                       "ExtraLength": gravity_extra_length}
+        if wavelength_adjustment_workspace:
+            q1d_options.update({"WavelengthAdj": wavelength_adjustment_workspace})
+        if pixel_adjustment_workspace:
+            q1d_options.update({"PixelAdj": pixel_adjustment_workspace})
+        if wavelength_and_pixel_adjustment_workspace:
+            q1d_options.update({"WavePixelAdj": wavelength_and_pixel_adjustment_workspace})
+        if q_resolution_workspace:
+            q1d_options.update({"QResolution": q_resolution_workspace})
+
+        q1d_alg = create_unmanaged_algorithm(q1d_name, **q1d_options)
+        q1d_alg.execute()
+        reduced_workspace = q1d_alg.getProperty("OutputWorkspace").value
+
+        # Get the partial workspaces
+        sum_of_counts_workspace, sum_of_norms_workspace = self._get_partial_output(output_parts, q1d_alg,
+                                                                                   do_clean=False)
+
+        return reduced_workspace, sum_of_counts_workspace, sum_of_norms_workspace
+
+    def _run_q_2d(self, state):
+        """
+        This method performs a 2D data reduction on our workspace.
+
+        Note that it does not perform any q resolution calculation, nor any wavelength-and-pixel adjustment. The
+        output workspace contains two numerical axes.
+        @param state: a SANSState object
+        @return: the reduced workspace, the sum of counts workspace, the sum of norms workspace or
+                 the reduced workspace, None, None
+        """
+        data_workspace = self.getProperty("InputWorkspace").value
+        wavelength_adjustment_workspace = self.getProperty("InputWorkspaceWavelengthAdjustment").value
+        pixel_adjustment_workspace = self.getProperty("InputWorkspacePixelAdjustment").value
+
+        output_parts = self.getProperty("OutputParts").value
+
+        # Extract relevant settings
+        convert_to_q = state.convert_to_q
+        max_q_xy = convert_to_q.q_xy_max
+        delta_q_prefix = -1 if convert_to_q.q_xy_step_type is RangeStepType.Log else 1
+        delta_q = delta_q_prefix*convert_to_q.q_xy_step
+        radius_cutoff = convert_to_q.radius_cutoff / 1000.  # Qxy expects the radius cutoff to be in mm
+        wavelength_cutoff = convert_to_q.wavelength_cutoff
+        use_gravity = convert_to_q.use_gravity
+        gravity_extra_length = convert_to_q.gravity_extra_length
+
+        qxy_name = "Qxy"
+        qxy_options = {"InputWorkspace": data_workspace,
+                       "OutputWorkspace": EMPTY_NAME,
+                       "MaxQxy": max_q_xy,
+                       "DeltaQ": delta_q,
+                       "AccountForGravity": use_gravity,
+                       "RadiusCut": radius_cutoff,
+                       "WaveCut": wavelength_cutoff,
+                       "OutputParts": output_parts,
+                       "ExtraLength": gravity_extra_length}
+        if wavelength_adjustment_workspace:
+            qxy_options.update({"WavelengthAdj": wavelength_adjustment_workspace})
+        if pixel_adjustment_workspace:
+            qxy_options.update({"PixelAdj": pixel_adjustment_workspace})
+
+        qxy_alg = create_unmanaged_algorithm(qxy_name, **qxy_options)
+        qxy_alg.execute()
+
+        reduced_workspace = qxy_alg.getProperty("OutputWorkspace").value
+        reduced_workspace = self._replace_special_values(reduced_workspace)
+
+        # Get the partial workspaces
+        sum_of_counts_workspace, sum_of_norms_workspace = self._get_partial_output(output_parts, qxy_alg, do_clean=True)
+
+        return reduced_workspace, sum_of_counts_workspace, sum_of_norms_workspace
+
+    def _get_partial_output(self, output_parts, alg, do_clean=False):
+        if output_parts:
+            sum_of_counts_workspace = alg.getProperty("SumOfCounts").value
+            sum_of_norms_workspace = alg.getProperty("sumOfNormFactors").value
+            if do_clean:
+                sum_of_counts_workspace = self._replace_special_values(sum_of_counts_workspace)
+                sum_of_norms_workspace = self._replace_special_values(sum_of_norms_workspace)
+        else:
+            sum_of_counts_workspace = None
+            sum_of_norms_workspace = None
+        return sum_of_counts_workspace, sum_of_norms_workspace
+
+    def _set_partial_workspaces(self, sum_of_counts_workspace, sum_of_norms_workspace):
+        """
+        Sets the partial output, ie the sum of the counts workspace and the sum of the normalization workspace
+        @param sum_of_counts_workspace: the sum of the counts workspace
+        @param sum_of_norms_workspace: the sum of the normalization workspace
+        """
+        self.declareProperty(MatrixWorkspaceProperty("SumOfCounts", '',
+                                                     optional=PropertyMode.Optional, direction=Direction.Output),
+                             doc="The sum of the counts workspace.")
+        self.declareProperty(MatrixWorkspaceProperty("SumOfNormFactors", '',
+                                                     optional=PropertyMode.Optional, direction=Direction.Output),
+                             doc="The sum of the normalizations workspace.")
+
+        output_name = self.getProperty("OutputWorkspace").name
+        sum_of_counts_workspace_name = output_name + "_sumOfCounts"
+        sum_of_norms_workspace_name = output_name + "_sumOfNormFactors"
+
+        self.setPropertyValue("SumOfCounts", sum_of_counts_workspace_name)
+        self.setPropertyValue("SumOfNormFactors", sum_of_norms_workspace_name)
+
+        self.setProperty("SumOfCounts", sum_of_counts_workspace)
+        self.setProperty("SumOfNormFactors", sum_of_norms_workspace)
+
+    def _replace_special_values(self, workspace):
+        replace_name = "ReplaceSpecialValues"
+        replace_options = {"InputWorkspace": workspace,
+                           "OutputWorkspace": EMPTY_NAME,
+                           "NaNValue": 0.,
+                           "InfinityValue": 0.}
+        replace_alg = create_unmanaged_algorithm(replace_name, **replace_options)
+        replace_alg.execute()
+        return replace_alg.getProperty("OutputWorkspace").value
+
+    def validateInputs(self):
+        errors = dict()
+        # Check that the input can be converted into the right state object
+        state_property_manager = self.getProperty("SANSState").value
+        try:
+            state = create_deserialized_sans_state_from_property_manager(state_property_manager)
+            state.property_manager = state_property_manager
+            state.validate()
+        except ValueError as err:
+            errors.update({"SANSSConvertToQ": str(err)})
+        return errors
+
+
+# Register algorithm with Mantid
+AlgorithmFactory.subscribe(SANSConvertToQ)
diff --git a/Framework/PythonInterface/plugins/algorithms/WorkflowAlgorithms/SANS/SANSConvertToWavelength.py b/Framework/PythonInterface/plugins/algorithms/WorkflowAlgorithms/SANS/SANSConvertToWavelength.py
new file mode 100644
index 0000000000000000000000000000000000000000..c57fb73620cda10231028b369682c284ced40cca
--- /dev/null
+++ b/Framework/PythonInterface/plugins/algorithms/WorkflowAlgorithms/SANS/SANSConvertToWavelength.py
@@ -0,0 +1,73 @@
+# pylint: disable=too-few-public-methods
+
+""" SANSConvertToWavelength converts to wavelength units """
+from __future__ import (absolute_import, division, print_function)
+from mantid.kernel import (Direction, PropertyManagerProperty)
+from mantid.api import (DataProcessorAlgorithm, MatrixWorkspaceProperty, AlgorithmFactory, PropertyMode)
+from sans.common.constants import EMPTY_NAME
+from sans.common.general_functions import (create_unmanaged_algorithm, append_to_sans_file_tag,
+                                           get_input_workspace_as_copy_if_not_same_as_output_workspace)
+from sans.common.enums import (RangeStepType, RebinType)
+from sans.state.state_base import create_deserialized_sans_state_from_property_manager
+
+
+class SANSConvertToWavelength(DataProcessorAlgorithm):
+    def category(self):
+        return 'SANS\\Wavelength'
+
+    def summary(self):
+        return 'Convert the units of a SANS workspace to wavelength'
+
+    def PyInit(self):
+        # State
+        self.declareProperty(PropertyManagerProperty('SANSState'),
+                             doc='A property manager which fulfills the SANSState contract.')
+
+        self.declareProperty(MatrixWorkspaceProperty("InputWorkspace", '',
+                                                     optional=PropertyMode.Mandatory, direction=Direction.Input),
+                             doc='The workspace which is to be converted to wavelength')
+
+        self.declareProperty(MatrixWorkspaceProperty('OutputWorkspace', '',
+                                                     optional=PropertyMode.Mandatory, direction=Direction.Output),
+                             doc='The output workspace.')
+
+    def PyExec(self):
+        # State
+        state_property_manager = self.getProperty("SANSState").value
+        state = create_deserialized_sans_state_from_property_manager(state_property_manager)
+        wavelength_state = state.wavelength
+
+        # Input workspace
+        workspace = get_input_workspace_as_copy_if_not_same_as_output_workspace(self)
+
+        wavelength_name = "SANSConvertToWavelengthAndRebin"
+        wavelength_options = {"InputWorkspace": workspace,
+                              "WavelengthLow": wavelength_state.wavelength_low,
+                              "WavelengthHigh": wavelength_state.wavelength_high,
+                              "WavelengthStep": wavelength_state.wavelength_step,
+                              "WavelengthStepType": RangeStepType.to_string(
+                                  wavelength_state.wavelength_step_type),
+                              "RebinMode": RebinType.to_string(wavelength_state.rebin_type)}
+        wavelength_alg = create_unmanaged_algorithm(wavelength_name, **wavelength_options)
+        wavelength_alg.setPropertyValue("OutputWorkspace", EMPTY_NAME)
+        wavelength_alg.setProperty("OutputWorkspace", workspace)
+        wavelength_alg.execute()
+        converted_workspace = wavelength_alg.getProperty("OutputWorkspace").value
+        append_to_sans_file_tag(converted_workspace, "_wavelength")
+        self.setProperty("OutputWorkspace", converted_workspace)
+
+    def validateInputs(self):
+        errors = dict()
+        # Check that the input can be converted into the right state object
+        state_property_manager = self.getProperty("SANSState").value
+        try:
+            state = create_deserialized_sans_state_from_property_manager(state_property_manager)
+            state.property_manager = state_property_manager
+            state.validate()
+        except ValueError as err:
+            errors.update({"SANSSMove": str(err)})
+        return errors
+
+
+# Register algorithm with Mantid
+AlgorithmFactory.subscribe(SANSConvertToWavelength)
diff --git a/Framework/PythonInterface/plugins/algorithms/WorkflowAlgorithms/SANS/SANSConvertToWavelengthAndRebin.py b/Framework/PythonInterface/plugins/algorithms/WorkflowAlgorithms/SANS/SANSConvertToWavelengthAndRebin.py
new file mode 100644
index 0000000000000000000000000000000000000000..c9faf23419a2406677835eb67ea38963fa168fb7
--- /dev/null
+++ b/Framework/PythonInterface/plugins/algorithms/WorkflowAlgorithms/SANS/SANSConvertToWavelengthAndRebin.py
@@ -0,0 +1,146 @@
+# pylint: disable=too-few-public-methods
+
+""" SANSConvertToWavelengthAndRebin algorithm converts to wavelength units and performs a rebin."""
+
+from __future__ import (absolute_import, division, print_function)
+from mantid.kernel import (Direction, StringListValidator, Property)
+from mantid.dataobjects import EventWorkspace
+from mantid.api import (DataProcessorAlgorithm, MatrixWorkspaceProperty, AlgorithmFactory, PropertyMode, Progress)
+from sans.common.constants import EMPTY_NAME
+from sans.common.general_functions import (create_unmanaged_algorithm, append_to_sans_file_tag,
+                                           get_input_workspace_as_copy_if_not_same_as_output_workspace)
+from sans.common.enums import (RebinType, RangeStepType)
+
+
+class SANSConvertToWavelengthAndRebin(DataProcessorAlgorithm):
+    def category(self):
+        return 'SANS\\Wavelength'
+
+    def summary(self):
+        return 'Convert the units of time-of-flight workspace to units of wavelength and performs a rebin.'
+
+    def PyInit(self):
+        # Workspace which is to be masked
+        self.declareProperty(MatrixWorkspaceProperty("InputWorkspace", '',
+                                                     optional=PropertyMode.Mandatory, direction=Direction.Input),
+                             doc='The workspace which is to be converted to wavelength')
+
+        self.declareProperty('WavelengthLow', defaultValue=Property.EMPTY_DBL, direction=Direction.Input,
+                             doc='The low value of the wavelength binning.')
+        self.declareProperty('WavelengthHigh', defaultValue=Property.EMPTY_DBL, direction=Direction.Input,
+                             doc='The high value of the wavelength binning.')
+        self.declareProperty('WavelengthStep', defaultValue=Property.EMPTY_DBL, direction=Direction.Input,
+                             doc='The step size of the wavelength binning.')
+
+        # Step type
+        allowed_step_types = StringListValidator([RangeStepType.to_string(RangeStepType.Log),
+                                                  RangeStepType.to_string(RangeStepType.Lin)])
+        self.declareProperty('WavelengthStepType', RangeStepType.to_string(RangeStepType.Lin),
+                             validator=allowed_step_types, direction=Direction.Input,
+                             doc='The step type for rebinning.')
+
+        # Rebin type
+        allowed_rebin_methods = StringListValidator([RebinType.to_string(RebinType.Rebin),
+                                                     RebinType.to_string(RebinType.InterpolatingRebin)])
+        self.declareProperty("RebinMode", RebinType.to_string(RebinType.Rebin),
+                             validator=allowed_rebin_methods, direction=Direction.Input,
+                             doc="The method which is to be applied to the rebinning.")
+
+        self.declareProperty(MatrixWorkspaceProperty('OutputWorkspace', '',
+                                                     optional=PropertyMode.Mandatory, direction=Direction.Output),
+                             doc='The output workspace.')
+
+    def PyExec(self):
+        workspace = get_input_workspace_as_copy_if_not_same_as_output_workspace(self)
+
+        progress = Progress(self, start=0.0, end=1.0, nreports=3)
+
+        # Convert the units into wavelength
+        progress.report("Converting workspace to wavelength units.")
+        workspace = self._convert_units_to_wavelength(workspace)
+
+        # Get the rebin option
+        rebin_type = RebinType.from_string(self.getProperty("RebinMode").value)
+        rebin_string = self._get_rebin_string(workspace)
+        if rebin_type is RebinType.Rebin:
+            rebin_options = {"InputWorkspace": workspace,
+                             "PreserveEvents": True,
+                             "Params": rebin_string}
+        else:
+            rebin_options = {"InputWorkspace": workspace,
+                             "Params": rebin_string}
+
+        # Perform the rebin
+        progress.report("Performing rebin.")
+        workspace = self._perform_rebin(rebin_type, rebin_options, workspace)
+
+        append_to_sans_file_tag(workspace, "_toWavelength")
+        self.setProperty("OutputWorkspace", workspace)
+        progress.report("Finished converting to wavelength.")
+
+    def validateInputs(self):
+        errors = dict()
+        # Check the wavelength
+        wavelength_low = self.getProperty("WavelengthLow").value
+        wavelength_high = self.getProperty("WavelengthHigh").value
+        if wavelength_low is not None and wavelength_high is not None and wavelength_low > wavelength_high:
+            errors.update({"WavelengthLow": "The lower wavelength setting needs to be smaller "
+                                            "than the higher wavelength setting."})
+
+        if wavelength_low is not None and wavelength_low < 0:
+            errors.update({"WavelengthLow": "The wavelength cannot be smaller than 0."})
+
+        if wavelength_high is not None and wavelength_high < 0:
+            errors.update({"WavelengthHigh": "The wavelength cannot be smaller than 0."})
+
+        wavelength_step = self.getProperty("WavelengthStep").value
+        if wavelength_step is not None and wavelength_step < 0:
+            errors.update({"WavelengthStep": "The wavelength step cannot be smaller than 0."})
+
+        # Check the workspace
+        workspace = self.getProperty("InputWorkspace").value
+        rebin_type = RebinType.from_string(self.getProperty("RebinMode").value)
+        if rebin_type is RebinType.InterpolatingRebin and isinstance(workspace, EventWorkspace):
+            errors.update({"RebinMode": "An interpolating rebin cannot be applied to an EventWorkspace."})
+        return errors
+
+    def _convert_units_to_wavelength(self, workspace):
+        convert_name = "ConvertUnits"
+        convert_options = {"InputWorkspace": workspace,
+                           "Target": "Wavelength"}
+        convert_alg = create_unmanaged_algorithm(convert_name, **convert_options)
+        convert_alg.setPropertyValue("OutputWorkspace", EMPTY_NAME)
+        convert_alg.setProperty("OutputWorkspace", workspace)
+        convert_alg.execute()
+        return convert_alg.getProperty("OutputWorkspace").value
+
+    def _get_rebin_string(self, workspace):
+        wavelength_low = self.getProperty("WavelengthLow").value
+        wavelength_high = self.getProperty("WavelengthHigh").value
+
+        # If the wavelength has not been specified, then get it from the workspace. Only the first spectrum is checked
+        # The lowest wavelength value is to be found in the spectrum located at workspaces index 0 is a very
+        # strong assumption, but it existed in the previous implementation.
+        if wavelength_low is None or wavelength_low == Property.EMPTY_DBL:
+            wavelength_low = min(workspace.readX(0))
+
+        if wavelength_high is None or wavelength_high == Property.EMPTY_DBL:
+            wavelength_high = max(workspace.readX(0))
+
+        wavelength_step = self.getProperty("WavelengthStep").value
+        step_type = RangeStepType.from_string(self.getProperty("WavelengthStepType").value)
+        pre_factor = -1 if step_type == RangeStepType.Log else 1
+        wavelength_step *= pre_factor
+        return str(wavelength_low) + "," + str(wavelength_step) + "," + str(wavelength_high)
+
+    def _perform_rebin(self, rebin_type, rebin_options, workspace):
+        rebin_name = "Rebin" if rebin_type is RebinType.Rebin else "InterpolatingRebin"
+        rebin_alg = create_unmanaged_algorithm(rebin_name, **rebin_options)
+        rebin_alg.setPropertyValue("OutputWorkspace", EMPTY_NAME)
+        rebin_alg.setProperty("OutputWorkspace", workspace)
+        rebin_alg.execute()
+        return rebin_alg.getProperty("OutputWorkspace").value
+
+
+# Register algorithm with Mantid
+AlgorithmFactory.subscribe(SANSConvertToWavelengthAndRebin)
diff --git a/Framework/PythonInterface/plugins/algorithms/WorkflowAlgorithms/SANS/SANSSave.py b/Framework/PythonInterface/plugins/algorithms/WorkflowAlgorithms/SANS/SANSSave.py
new file mode 100644
index 0000000000000000000000000000000000000000..a3fa9121a8378898af8d6d15dbcab46395797783
--- /dev/null
+++ b/Framework/PythonInterface/plugins/algorithms/WorkflowAlgorithms/SANS/SANSSave.py
@@ -0,0 +1,171 @@
+
+
+# pylint: disable=invalid-name
+
+""" SANSSave algorithm performs saving of SANS reduction data."""
+
+from __future__ import (absolute_import, division, print_function)
+from mantid.kernel import (Direction)
+from mantid.api import (DataProcessorAlgorithm, MatrixWorkspaceProperty, AlgorithmFactory, PropertyMode,
+                        FileProperty, FileAction, Progress)
+from sans.common.enums import (SaveType)
+from sans.algorithm_detail.save_workspace import (save_to_file, get_zero_error_free_workspace, file_format_with_append)
+
+
+class SANSSave(DataProcessorAlgorithm):
+    def category(self):
+        return 'SANS\\Save'
+
+    def summary(self):
+        return 'Performs saving of reduced SANS data.'
+
+    def PyInit(self):
+        self.declareProperty(MatrixWorkspaceProperty("InputWorkspace", '',
+                                                     optional=PropertyMode.Mandatory, direction=Direction.Input),
+                             doc='The workspace which is to be saved.'
+                                 ' This workspace needs to be the result of a SANS reduction,'
+                                 ' i.e. it can only be 1D or 2D if the second axis is numeric.')
+
+        self.declareProperty(FileProperty("Filename", '', action=FileAction.Save,
+                                          extensions=[]),
+                             doc="The name of the file which needs to be stored. Note that "
+                                 "the actual file type is selected below.")
+
+        self.declareProperty("Nexus", False, direction=Direction.Input,
+                             doc="Save as nexus format. "
+                                 "Note that if file formats of the same type, e.g. .xml are chosen, then the "
+                                 "file format is appended to the file name.")
+        self.declareProperty("CanSAS", False, direction=Direction.Input,
+                             doc="Save as CanSAS xml format."
+                                 "Note that if file formats of the same type, e.g. .xml are chosen, then the "
+                                 "file format is appended to the file name.")
+        self.declareProperty("NXcanSAS", False, direction=Direction.Input,
+                             doc="Save as NXcanSAS format."
+                                 "Note that if file formats of the same type, e.g. .xml are chosen, then the "
+                                 "file format is appended to the file name.")
+        self.declareProperty("NistQxy", False, direction=Direction.Input,
+                             doc="Save as Nist Qxy format."
+                                 "Note that if file formats of the same type, e.g. .xml are chosen, then the "
+                                 "file format is appended to the file name.")
+        self.declareProperty("RKH", False, direction=Direction.Input,
+                             doc="Save as RKH format."
+                                 "Note that if file formats of the same type, e.g. .xml are chosen, then the "
+                                 "file format is appended to the file name.")
+        self.declareProperty("CSV", False, direction=Direction.Input,
+                             doc="Save as CSV format."
+                                 "Note that if file formats of the same type, e.g. .xml are chosen, then the "
+                                 "file format is appended to the file name.")
+
+        self.setPropertyGroup("Nexus", 'FileFormats')
+        self.setPropertyGroup("CanSAS", 'FileFormats')
+        self.setPropertyGroup("NXCanSAS", 'FileFormats')
+        self.setPropertyGroup("NistQxy", 'FileFormats')
+        self.setPropertyGroup("RKH", 'FileFormats')
+        self.setPropertyGroup("CSV", 'FileFormats')
+
+        self.declareProperty("UseZeroErrorFree", True, direction=Direction.Input,
+                             doc="This allows the user to artificially inflate zero error values.")
+
+    def PyExec(self):
+        use_zero_error_free = self.getProperty("UseZeroErrorFree").value
+        file_formats = self._get_file_formats()
+        file_name = self.getProperty("Filename").value
+        workspace = self.getProperty("InputWorkspace").value
+
+        if use_zero_error_free:
+            workspace = get_zero_error_free_workspace(workspace)
+        progress = Progress(self, start=0.0, end=1.0, nreports=len(file_formats) + 1)
+        for file_format in file_formats:
+            progress_message = "Saving to {0}.".format(SaveType.to_string(file_format.file_format))
+            progress.report(progress_message)
+            save_to_file(workspace, file_format, file_name)
+        progress.report("Finished saving workspace to files.")
+
+    def validateInputs(self):
+        errors = dict()
+        # The workspace must be 1D or 2D if the second axis is numeric
+        workspace = self.getProperty("InputWorkspace").value
+        number_of_histograms = workspace.getNumberHistograms()
+        axis1 = workspace.getAxis(1)
+        is_first_axis_numeric = axis1.isNumeric()
+        if not is_first_axis_numeric and number_of_histograms > 1:
+            errors.update({"InputWorkspace": "The input data seems to be 2D. In this case all "
+                                             "axes need to be numeric."})
+
+        # Make sure that at least one file format is selected
+        file_formats = self._get_file_formats()
+        if not file_formats:
+            errors.update({"Nexus": "At least one data format needs to be specified."})
+            errors.update({"CanSAS": "At least one data format needs to be specified."})
+            errors.update({"NXcanSAS": "At least one data format needs to be specified."})
+            errors.update({"NistQxy": "At least one data format needs to be specified."})
+            errors.update({"RKH": "At least one data format needs to be specified."})
+            errors.update({"CSV": "At least one data format needs to be specified."})
+
+        # NistQxy cannot be used with 1D data
+        is_nistqxy_selected = self.getProperty("NistQxy").value
+        if is_nistqxy_selected and number_of_histograms == 1 and not is_first_axis_numeric:
+            errors.update({"NistQxy": "Attempting to save a 1D workspace with NistQxy. NistQxy can store 2D data"
+                                      " only. This requires all axes to be numeric."})
+        return errors
+
+    def _get_file_formats(self):
+        file_types = []
+        self._check_file_types(file_types, "Nexus", SaveType.Nexus)
+        self._check_file_types(file_types, "CanSAS", SaveType.CanSAS)
+        self._check_file_types(file_types, "NXcanSAS", SaveType.NXcanSAS)
+        self._check_file_types(file_types, "NistQxy", SaveType.NistQxy)
+        self._check_file_types(file_types, "RKH", SaveType.RKH)
+        self._check_file_types(file_types, "CSV", SaveType.CSV)
+
+        # Now check if several file formats were selected which save into the same file
+        # type, e.g. as Nexus and NXcanSAS do. If this is the case then we want to mark these file formats
+        file_formats = []
+
+        # SaveNexusProcessed clashes with SaveNXcanSAS, but we only alter NXcanSAS data
+        self.add_file_format_with_appended_name_requirement(file_formats, SaveType.Nexus, file_types, [])
+
+        # SaveNXcanSAS clashes with SaveNexusProcessed
+        self.add_file_format_with_appended_name_requirement(file_formats, SaveType.NXcanSAS, file_types,
+                                                            [SaveType.Nexus])
+
+        # SaveNISTDAT clashes with SaveRKH, both can save to .dat
+        self.add_file_format_with_appended_name_requirement(file_formats, SaveType.NistQxy, file_types, [SaveType.RKH])
+
+        # SaveRKH clashes with SaveNISTDAT, but we alter SaveNISTDAT
+        self.add_file_format_with_appended_name_requirement(file_formats, SaveType.RKH, file_types, [])
+
+        # SaveCanSAS1D does not clash with anyone
+        self.add_file_format_with_appended_name_requirement(file_formats, SaveType.CanSAS, file_types, [])
+
+        # SaveCSV does not clash with anyone
+        self.add_file_format_with_appended_name_requirement(file_formats, SaveType.CSV, file_types, [])
+        return file_formats
+
+    def add_file_format_with_appended_name_requirement(self, file_formats, file_format, file_types,
+                                                       clashing_file_formats):
+        """
+        This function adds a file format to the file format list. It checks if there are other selected file formats
+        which clash with the current file format, e.g. both SaveNexusProcessed and SaveNXcanSAS produce .nxs files
+
+        :param file_formats: A list of file formats
+        :param file_format: The file format to be added
+        :param file_types: The pure file types
+        :param clashing_file_formats: The other file types which might be clashing with the current file type.
+        :return:
+        """
+        if file_format in file_types:
+            append_name = False
+            for clashing_file_format in clashing_file_formats:
+                if clashing_file_format in file_types:
+                    append_name = True
+            file_formats.append(file_format_with_append(file_format=file_format,
+                                                        append_file_format_name=append_name))
+
+    def _check_file_types(self, file_formats, key, to_add):
+        if self.getProperty(key).value:
+            file_formats.append(to_add)
+
+
+# Register algorithm with Mantid
+AlgorithmFactory.subscribe(SANSSave)
diff --git a/Framework/PythonInterface/plugins/algorithms/WorkflowAlgorithms/SANS/SANSScale.py b/Framework/PythonInterface/plugins/algorithms/WorkflowAlgorithms/SANS/SANSScale.py
new file mode 100644
index 0000000000000000000000000000000000000000..b1c77dd643f73fb8d11ac6f9ca4a70679b98cf80
--- /dev/null
+++ b/Framework/PythonInterface/plugins/algorithms/WorkflowAlgorithms/SANS/SANSScale.py
@@ -0,0 +1,70 @@
+# pylint: disable=too-few-public-methods
+
+""" Multiplies a SANS workspace by an absolute scale and divides it by the sample volume. """
+
+from __future__ import (absolute_import, division, print_function)
+from mantid.kernel import (Direction, PropertyManagerProperty)
+from mantid.api import (DataProcessorAlgorithm, MatrixWorkspaceProperty, AlgorithmFactory, PropertyMode, Progress)
+
+from sans.state.state_base import create_deserialized_sans_state_from_property_manager
+from sans.algorithm_detail.scale_helpers import (DivideByVolumeFactory, MultiplyByAbsoluteScaleFactory)
+from sans.common.general_functions import (append_to_sans_file_tag)
+
+
+class SANSScale(DataProcessorAlgorithm):
+    def category(self):
+        return 'SANS\\Scale'
+
+    def summary(self):
+        return 'Multiplies a SANS workspace by an absolute scale and divides it by the sample volume.'
+
+    def PyInit(self):
+        # State
+        self.declareProperty(PropertyManagerProperty('SANSState'),
+                             doc='A property manager which fulfills the SANSState contract.')
+
+        self.declareProperty(MatrixWorkspaceProperty("InputWorkspace", '',
+                                                     optional=PropertyMode.Mandatory, direction=Direction.Input),
+                             doc='The input workspace')
+
+        self.declareProperty(MatrixWorkspaceProperty("OutputWorkspace", '',
+                                                     optional=PropertyMode.Mandatory, direction=Direction.Output),
+                             doc='The scaled output workspace')
+
+    def PyExec(self):
+        state_property_manager = self.getProperty("SANSState").value
+        state = create_deserialized_sans_state_from_property_manager(state_property_manager)
+
+        # Get the correct SANS move strategy from the SANSMaskFactory
+        workspace = self.getProperty("InputWorkspace").value
+
+        progress = Progress(self, start=0.0, end=1.0, nreports=3)
+
+        # Multiply by the absolute scale
+        progress.report("Applying absolute scale.")
+        workspace = self._multiply_by_absolute_scale(workspace, state)
+
+        # Divide by the sample volume
+        progress.report("Dividing by the sample volume.")
+
+        workspace = self._divide_by_volume(workspace, state)
+
+        append_to_sans_file_tag(workspace, "_scale")
+        self.setProperty("OutputWorkspace", workspace)
+        progress.report("Finished applying absolute scale")
+
+    def _divide_by_volume(self, workspace, state):
+        divide_factory = DivideByVolumeFactory()
+        divider = divide_factory.create_divide_by_volume(state)
+        scale_info = state.scale
+        return divider.divide_by_volume(workspace, scale_info)
+
+    def _multiply_by_absolute_scale(self, workspace, state):
+        multiply_factory = MultiplyByAbsoluteScaleFactory()
+        multiplier = multiply_factory.create_multiply_by_absolute(state)
+        scale_info = state.scale
+        return multiplier.multiply_by_absolute_scale(workspace, scale_info)
+
+
+# Register algorithm with Mantid
+AlgorithmFactory.subscribe(SANSScale)
diff --git a/Framework/PythonInterface/test/python/plugins/algorithms/WorkflowAlgorithms/CMakeLists.txt b/Framework/PythonInterface/test/python/plugins/algorithms/WorkflowAlgorithms/CMakeLists.txt
index fb536975f55b8829d5b1481afedcd9c8d20304df..b63bcbbd338ee4f33732205fe702e4b52d866b4c 100644
--- a/Framework/PythonInterface/test/python/plugins/algorithms/WorkflowAlgorithms/CMakeLists.txt
+++ b/Framework/PythonInterface/test/python/plugins/algorithms/WorkflowAlgorithms/CMakeLists.txt
@@ -51,3 +51,4 @@ check_tests_valid ( ${CMAKE_CURRENT_SOURCE_DIR} ${TEST_PY_FILES} )
 
 # Prefix for test name=PythonWorkflowAlgorithms
 pyunittest_add_test ( ${CMAKE_CURRENT_SOURCE_DIR} PythonWorkflowAlgorithms ${TEST_PY_FILES} )
+add_subdirectory( sans )
diff --git a/Framework/PythonInterface/test/python/plugins/algorithms/SANS/CMakeLists.txt b/Framework/PythonInterface/test/python/plugins/algorithms/WorkflowAlgorithms/sans/CMakeLists.txt
similarity index 81%
rename from Framework/PythonInterface/test/python/plugins/algorithms/SANS/CMakeLists.txt
rename to Framework/PythonInterface/test/python/plugins/algorithms/WorkflowAlgorithms/sans/CMakeLists.txt
index af2ccd1fa663483815e5e7a866967080aa0f4094..384525860e635b3a126feefa1984f82be2e19e3e 100644
--- a/Framework/PythonInterface/test/python/plugins/algorithms/SANS/CMakeLists.txt
+++ b/Framework/PythonInterface/test/python/plugins/algorithms/WorkflowAlgorithms/sans/CMakeLists.txt
@@ -3,6 +3,9 @@
 ######################
 
 set ( TEST_PY_FILES
+SANSConvertToQTest.py
+SANSConvertToWavelengthTest.py
+SANSScaleTest.py
 SANSSliceEventTest.py
 )
 
diff --git a/Framework/PythonInterface/test/python/plugins/algorithms/WorkflowAlgorithms/sans/SANSConvertToQTest.py b/Framework/PythonInterface/test/python/plugins/algorithms/WorkflowAlgorithms/sans/SANSConvertToQTest.py
new file mode 100644
index 0000000000000000000000000000000000000000..4aa9101962a359dcf6c9923a6b02defc5ee64384
--- /dev/null
+++ b/Framework/PythonInterface/test/python/plugins/algorithms/WorkflowAlgorithms/sans/SANSConvertToQTest.py
@@ -0,0 +1,146 @@
+from __future__ import (absolute_import, division, print_function)
+import unittest
+import mantid
+
+from sans.common.general_functions import (create_unmanaged_algorithm)
+from sans.common.constants import EMPTY_NAME
+from sans.common.enums import (SANSFacility, SampleShape, ReductionDimensionality, RangeStepType)
+from sans.test_helper.test_director import TestDirector
+from sans.state.convert_to_q import get_convert_to_q_builder
+from sans.state.data import get_data_builder
+
+
+class SANSConvertToQTest(unittest.TestCase):
+    @staticmethod
+    def _get_workspace(x_unit="Wavelength", is_adjustment=False):
+        bank_pixel_width = 1 if is_adjustment else 2
+        sample_name = "CreateSampleWorkspace"
+        sample_options = {"WorkspaceType": "Histogram",
+                          "NumBanks": 1,
+                          "BankPixelWidth": bank_pixel_width,
+                          "OutputWorkspace": "test",
+                          "Function": "Flat background",
+                          "XUnit": x_unit,
+                          "XMin": 1.,
+                          "XMax": 10.,
+                          "BinWidth": 2.}
+        sample_alg = create_unmanaged_algorithm(sample_name, **sample_options)
+        sample_alg.execute()
+        workspace = sample_alg.getProperty("OutputWorkspace").value
+        return workspace
+
+    @staticmethod
+    def _get_sample_state(q_min=1., q_max=2., q_step=0.1, q_step_type=RangeStepType.Lin,
+                          q_xy_max=None, q_xy_step=None, q_xy_step_type=None,
+                          use_gravity=False, dim=ReductionDimensionality.OneDim):
+        # Arrange
+        facility = SANSFacility.ISIS
+        data_builder = get_data_builder(facility)
+        data_builder.set_sample_scatter("LOQ74044")
+        data_state = data_builder.build()
+
+        convert_to_q_builder = get_convert_to_q_builder(data_state)
+        convert_to_q_builder.set_reduction_dimensionality(dim)
+        convert_to_q_builder.set_use_gravity(use_gravity)
+        convert_to_q_builder.set_radius_cutoff(0.002)
+        convert_to_q_builder.set_wavelength_cutoff(2.)
+        convert_to_q_builder.set_q_min(q_min)
+        convert_to_q_builder.set_q_max(q_max)
+        prefix = 1. if q_step_type is RangeStepType.Lin else -1.
+        q_step *= prefix
+        rebin_string = str(q_min) + "," + str(q_step) + "," + str(q_max)
+        convert_to_q_builder.set_q_1d_rebin_string(rebin_string)
+        if q_xy_max is not None:
+            convert_to_q_builder.set_q_xy_max(q_xy_max)
+        if q_xy_step is not None:
+            convert_to_q_builder.set_q_xy_step(q_xy_step)
+        if q_xy_step_type is not None:
+            convert_to_q_builder.set_q_xy_step_type(q_xy_step_type)
+
+        convert_to_q_state = convert_to_q_builder.build()
+
+        test_director = TestDirector()
+        test_director.set_states(convert_to_q_state=convert_to_q_state, data_state=data_state)
+
+        return test_director.construct().property_manager
+
+    @staticmethod
+    def _do_run_convert_to_q(state, data_workspace, wavelength_adjustment_workspace=None,
+                             pixel_adjustment_workspace=None, wavelength_and_pixel_adjustment_workspace=None):
+        convert_name = "SANSConvertToQ"
+        convert_options = {"InputWorkspace": data_workspace,
+                           "OutputWorkspace": EMPTY_NAME,
+                           "SANSState": state,
+                           "OutputParts": True}
+        if wavelength_adjustment_workspace:
+            convert_options.update({"InputWorkspaceWavelengthAdjustment": wavelength_adjustment_workspace})
+        if pixel_adjustment_workspace:
+            convert_options.update({"InputWorkspacePixelAdjustment": pixel_adjustment_workspace})
+        if wavelength_and_pixel_adjustment_workspace:
+            convert_options.update({"InputWorkspaceWavelengthAndPixelAdjustment":
+                                        wavelength_and_pixel_adjustment_workspace})
+        convert_alg = create_unmanaged_algorithm(convert_name, **convert_options)
+        convert_alg.execute()
+        data_workspace = convert_alg.getProperty("OutputWorkspace").value
+        sum_of_counts = convert_alg.getProperty("SumOfCounts").value
+        sum_of_norms = convert_alg.getProperty("SumOfNormFactors").value
+        return data_workspace, sum_of_counts, sum_of_norms
+
+    def test_that_only_accepts_wavelength_based_workspaces(self):
+        # Arrange
+        workspace = SANSConvertToQTest._get_workspace("TOF")
+        state = self._get_sample_state(q_min=1., q_max=2., q_step=0.1, q_step_type=RangeStepType.Lin)
+        # Act + Assert
+        args = []
+        kwargs = {"state": state, "data_workspace": workspace}
+        self.assertRaises(ValueError, SANSConvertToQTest._do_run_convert_to_q, *args, **kwargs)
+
+    def test_that_converts_wavelength_workspace_to_q_for_1d_and_no_q_resolution(self):
+        # Arrange
+        workspace = SANSConvertToQTest._get_workspace("Wavelength")
+        workspace2 = SANSConvertToQTest._get_workspace("Wavelength", is_adjustment=True)
+
+        state = self._get_sample_state(q_min=1., q_max=2., q_step=0.1, q_step_type=RangeStepType.Lin)
+
+        # Act
+        output_workspace, sum_of_counts, sum_of_norms = SANSConvertToQTest._do_run_convert_to_q(state=state,
+                                         data_workspace=workspace, wavelength_adjustment_workspace=workspace2)
+
+        # Assert
+        # We expect a q-based workspace with one histogram and 10 bins.
+        self.assertTrue(output_workspace.getNumberHistograms() == 1)
+        self.assertTrue(len(output_workspace.dataX(0)) == 11)
+        self.assertTrue(output_workspace.getAxis(0).getUnit().unitID() == "MomentumTransfer")
+        self.assertFalse(output_workspace.hasDx(0))
+        self.assertTrue(output_workspace.getAxis(0).isNumeric())
+        self.assertTrue(output_workspace.getAxis(1).isSpectra())
+        self.assertTrue(sum_of_counts.getAxis(0).getUnit().unitID() == "MomentumTransfer")
+        self.assertTrue(sum_of_norms.getAxis(0).getUnit().unitID() == "MomentumTransfer")
+
+    def test_that_converts_wavelength_workspace_to_q_for_2d(self):
+        # Arrange
+        workspace = SANSConvertToQTest._get_workspace("Wavelength")
+        workspace2 = SANSConvertToQTest._get_workspace("Wavelength", is_adjustment=True)
+
+        state = self._get_sample_state(q_xy_max=2., q_xy_step=0.5, q_xy_step_type=RangeStepType.Lin,
+                                       dim=ReductionDimensionality.TwoDim)
+
+        # Act
+        output_workspace, sum_of_counts, sum_of_norms = SANSConvertToQTest._do_run_convert_to_q(state=state,
+                                         data_workspace=workspace, wavelength_adjustment_workspace=workspace2)
+
+        # Assert
+        # We expect a q-based workspace with 8 histograms and 8 bins
+        self.assertTrue(output_workspace.getNumberHistograms() == 8)
+        self.assertTrue(len(output_workspace.dataX(0)) == 9)
+        self.assertTrue(output_workspace.getAxis(0).getUnit().unitID() == "MomentumTransfer")
+        self.assertTrue(output_workspace.getAxis(1).getUnit().unitID() == "MomentumTransfer")
+        self.assertFalse(output_workspace.hasDx(0))
+        self.assertTrue(output_workspace.getAxis(0).isNumeric())
+        self.assertTrue(output_workspace.getAxis(1).isNumeric())
+        self.assertTrue(sum_of_counts.getAxis(0).getUnit().unitID() == "MomentumTransfer")
+        self.assertTrue(sum_of_norms.getAxis(0).getUnit().unitID() == "MomentumTransfer")
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Framework/PythonInterface/test/python/plugins/algorithms/WorkflowAlgorithms/sans/SANSConvertToWavelengthTest.py b/Framework/PythonInterface/test/python/plugins/algorithms/WorkflowAlgorithms/sans/SANSConvertToWavelengthTest.py
new file mode 100644
index 0000000000000000000000000000000000000000..0c736d8ef8107d375e5e175941a689077a22344d
--- /dev/null
+++ b/Framework/PythonInterface/test/python/plugins/algorithms/WorkflowAlgorithms/sans/SANSConvertToWavelengthTest.py
@@ -0,0 +1,129 @@
+from __future__ import (absolute_import, division, print_function)
+import unittest
+import mantid
+
+from mantid.dataobjects import EventWorkspace
+from sans.common.general_functions import (create_unmanaged_algorithm)
+from sans.common.constants import EMPTY_NAME
+from sans.common.enums import (RangeStepType)
+
+
+def provide_workspace(is_event=True):
+    sample_name = "CreateSampleWorkspace"
+    sample_options = {"OutputWorkspace": "dummy",
+                      "NumBanks": 1,
+                      "BankPixelWidth": 2}
+    if is_event:
+        sample_options.update({"WorkspaceType": "Event"})
+    else:
+        sample_options.update({"WorkspaceType": "Histogram"})
+
+    sample_alg = create_unmanaged_algorithm(sample_name, **sample_options)
+    sample_alg.execute()
+    return sample_alg.getProperty("OutputWorkspace").value
+
+
+class SANSSConvertToWavelengthImplementationTest(unittest.TestCase):
+    def test_that_event_workspace_and_interpolating_rebin_raises(self):
+        workspace = provide_workspace(is_event=True)
+        convert_options = {"InputWorkspace": workspace,
+                           "OutputWorkspace": EMPTY_NAME,
+                           "RebinMode": "InterpolatingRebin",
+                           "WavelengthLow": 1.0,
+                           "WavelengthHigh": 3.0,
+                           "WavelengthStep": 1.5,
+                           "WavelengthStepType":  RangeStepType.to_string(RangeStepType.Lin)}
+        convert_alg = create_unmanaged_algorithm("SANSConvertToWavelengthAndRebin", **convert_options)
+        had_run_time_error = False
+        try:
+            convert_alg.execute()
+        except RuntimeError:
+            had_run_time_error = True
+        self.assertTrue(had_run_time_error)
+
+    def test_that_negative_wavelength_values_raise(self):
+        workspace = provide_workspace(is_event=True)
+        convert_options = {"InputWorkspace": workspace,
+                           "OutputWorkspace": EMPTY_NAME,
+                           "RebinMode": "Rebin",
+                           "WavelengthLow": -1.0,
+                           "WavelengthHigh": 3.0,
+                           "WavelengthStep": 1.5,
+                           "WavelengthStepType":  RangeStepType.to_string(RangeStepType.Log)}
+        convert_alg = create_unmanaged_algorithm("SANSConvertToWavelengthAndRebin", **convert_options)
+        had_run_time_error = False
+        try:
+            convert_alg.execute()
+        except RuntimeError:
+            had_run_time_error = True
+        self.assertTrue(had_run_time_error)
+
+    def test_that_lower_wavelength_larger_than_higher_wavelength_raises(self):
+        workspace = provide_workspace(is_event=True)
+        convert_options = {"InputWorkspace": workspace,
+                           "OutputWorkspace": EMPTY_NAME,
+                           "RebinMode": "Rebin",
+                           "WavelengthLow":  4.0,
+                           "WavelengthHigh": 3.0,
+                           "WavelengthStep": 1.5,
+                           "WavelengthStepType":  RangeStepType.to_string(RangeStepType.Log)}
+        convert_alg = create_unmanaged_algorithm("SANSConvertToWavelengthAndRebin", **convert_options)
+        had_run_time_error = False
+        try:
+            convert_alg.execute()
+        except RuntimeError:
+            had_run_time_error = True
+        self.assertTrue(had_run_time_error)
+
+    def test_that_event_workspace_with_conversion_is_still_event_workspace(self):
+        workspace = provide_workspace(is_event=True)
+        convert_options = {"InputWorkspace": workspace,
+                           "OutputWorkspace": EMPTY_NAME,
+                           "RebinMode": "Rebin",
+                           "WavelengthLow": 1.0,
+                           "WavelengthHigh": 10.0,
+                           "WavelengthStep": 1.0,
+                           "WavelengthStepType": RangeStepType.to_string(RangeStepType.Lin)}
+        convert_alg = create_unmanaged_algorithm("SANSConvertToWavelengthAndRebin", **convert_options)
+        convert_alg.execute()
+        self.assertTrue(convert_alg.isExecuted())
+        output_workspace = convert_alg.getProperty("OutputWorkspace").value
+        self.assertTrue(isinstance(output_workspace, EventWorkspace))
+        # Check the rebinning part
+        data_x0 = output_workspace.dataX(0)
+        self.assertTrue(len(data_x0) == 10)
+        self.assertTrue(data_x0[0] == 1.0)
+        self.assertTrue(data_x0[-1] == 10.0)
+        # Check the units part
+        axis0 = output_workspace.getAxis(0)
+        unit = axis0.getUnit()
+        self.assertTrue(unit.unitID() == "Wavelength")
+
+    def test_that_not_setting_upper_bound_takes_it_from_original_value(self):
+        workspace = provide_workspace(is_event=True)
+        convert_options = {"InputWorkspace": workspace,
+                           "OutputWorkspace": EMPTY_NAME,
+                           "RebinMode": "Rebin",
+                           "WavelengthLow": 1.0,
+                           "WavelengthStep": 1.0,
+                           "WavelengthStepType": RangeStepType.to_string(RangeStepType.Lin)}
+        convert_alg = create_unmanaged_algorithm("SANSConvertToWavelengthAndRebin", **convert_options)
+        convert_alg.execute()
+        self.assertTrue(convert_alg.isExecuted())
+        output_workspace = convert_alg.getProperty("OutputWorkspace").value
+        self.assertTrue(isinstance(output_workspace, EventWorkspace))
+
+        # Check the rebinning part
+        data_x0 = output_workspace.dataX(0)
+        self.assertTrue(data_x0[0] == 1.0)
+        expected_upper_bound = 5.27471197274
+        self.assertTrue(data_x0[-1] == expected_upper_bound)
+
+        # Check the units part
+        axis0 = output_workspace.getAxis(0)
+        unit = axis0.getUnit()
+        self.assertTrue(unit.unitID() == "Wavelength")
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Framework/PythonInterface/test/python/plugins/algorithms/WorkflowAlgorithms/sans/SANSScaleTest.py b/Framework/PythonInterface/test/python/plugins/algorithms/WorkflowAlgorithms/sans/SANSScaleTest.py
new file mode 100644
index 0000000000000000000000000000000000000000..e2faa22f796ce63ec8861184dfdb8e090cf0d26f
--- /dev/null
+++ b/Framework/PythonInterface/test/python/plugins/algorithms/WorkflowAlgorithms/sans/SANSScaleTest.py
@@ -0,0 +1,74 @@
+from __future__ import (absolute_import, division, print_function)
+import unittest
+import mantid
+import math
+
+from sans.common.general_functions import (create_unmanaged_algorithm)
+from sans.common.constants import EMPTY_NAME
+from sans.common.enums import (SANSFacility, SampleShape)
+from sans.test_helper.test_director import TestDirector
+from sans.state.scale import get_scale_builder
+from sans.state.data import get_data_builder
+
+
+class SANSScaleTest(unittest.TestCase):
+    @staticmethod
+    def _get_workspace():
+        sample_name = "CreateSampleWorkspace"
+        sample_options = {"WorkspaceType": "Histogram",
+                          "NumBanks": 1,
+                          "BankPixelWidth": 1,
+                          "OutputWorkspace": "test"}
+        sample_alg = create_unmanaged_algorithm(sample_name, **sample_options)
+        sample_alg.execute()
+        workspace = sample_alg.getProperty("OutputWorkspace").value
+        return workspace
+
+    @staticmethod
+    def _get_sample_state(width, height, thickness, shape, scale):
+        # Arrange
+        facility = SANSFacility.ISIS
+        data_builder = get_data_builder(facility)
+        data_builder.set_sample_scatter("LOQ74044")
+        data_state = data_builder.build()
+
+        scale_builder = get_scale_builder(data_state)
+        scale_builder.set_scale(scale)
+        scale_builder.set_thickness(thickness)
+        scale_builder.set_width(width)
+        scale_builder.set_height(height)
+        scale_builder.set_shape(shape)
+        scale_state = scale_builder.build()
+
+        test_director = TestDirector()
+        test_director.set_states(scale_state=scale_state, data_state=data_state)
+        return test_director.construct()
+
+    def test_that_scales_the_workspace_correctly(self):
+        # Arrange
+        workspace = self._get_workspace()
+        width = 1.0
+        height = 2.0
+        scale = 7.2
+        state = self._get_sample_state(width=width, height=height, thickness=3.0, scale=scale,
+                                       shape=SampleShape.CylinderAxisUp)
+        serialized_state = state.property_manager
+        scale_name = "SANSScale"
+        scale_options = {"SANSState": serialized_state,
+                         "InputWorkspace": workspace,
+                         "OutputWorkspace": EMPTY_NAME}
+        scale_alg = create_unmanaged_algorithm(scale_name, **scale_options)
+
+        # Act
+        scale_alg.execute()
+        output_workspace = scale_alg.getProperty("OutputWorkspace").value
+
+        # Assert
+        # We have a LOQ data set, hence we need to divide by pi
+        expected_value = 0.3/(height * math.pi * math.pow(width, 2) / 4.0) * (scale / math.pi) * 100.
+        data_y = output_workspace.dataY(0)
+        tolerance = 1e-7
+        self.assertTrue(abs(data_y[0] - expected_value) < tolerance)
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Framework/PythonInterface/test/python/plugins/algorithms/SANS/SANSSliceEventTest.py b/Framework/PythonInterface/test/python/plugins/algorithms/WorkflowAlgorithms/sans/SANSSliceEventTest.py
similarity index 100%
rename from Framework/PythonInterface/test/python/plugins/algorithms/SANS/SANSSliceEventTest.py
rename to Framework/PythonInterface/test/python/plugins/algorithms/WorkflowAlgorithms/sans/SANSSliceEventTest.py
diff --git a/Testing/SystemTests/tests/analysis/SANSMaskWorkspaceTest.py b/Testing/SystemTests/tests/analysis/SANSMaskWorkspaceTest.py
index c43c31133b05021ef758aa09d4dda21e775767cb..989871eb263413a2ff133bf4021ba14ce6639805 100644
--- a/Testing/SystemTests/tests/analysis/SANSMaskWorkspaceTest.py
+++ b/Testing/SystemTests/tests/analysis/SANSMaskWorkspaceTest.py
@@ -110,8 +110,8 @@ class SANSMaskWorkspaceTest(unittest.TestCase):
 
         spectrum_range_start = [20, 30]
         spectrum_range_stop = [25, 35]
-        expected_spectra.extend(range(20, 25 + 1))
-        expected_spectra.extend(range(30, 35 + 1))
+        expected_spectra.extend(list(range(20, 25 + 1)))
+        expected_spectra.extend(list(range(30, 35 + 1)))
 
         # Detector-specific single horizontal strip mask
         # The horizontal strip will be evaluated for SANS2D on the LAB as:
diff --git a/Testing/SystemTests/tests/analysis/SANSSaveTest.py b/Testing/SystemTests/tests/analysis/SANSSaveTest.py
new file mode 100644
index 0000000000000000000000000000000000000000..8cfa862cac4ebd044997706f38a4a9491f404307
--- /dev/null
+++ b/Testing/SystemTests/tests/analysis/SANSSaveTest.py
@@ -0,0 +1,203 @@
+# pylint: disable=too-many-public-methods, invalid-name, too-many-arguments
+
+from __future__ import (absolute_import, division, print_function)
+import os
+import mantid
+import unittest
+import stresstesting
+
+from sans.common.general_functions import create_unmanaged_algorithm
+from sans.common.constants import EMPTY_NAME
+
+
+# -----------------------------------------------
+# Tests for the SANSSave algorithm
+# -----------------------------------------------
+class SANSSaveTest(unittest.TestCase):
+    @staticmethod
+    def _get_sample_workspace(with_zero_errors, convert_to_numeric_axis=False):
+        create_name = "CreateSimulationWorkspace"
+        create_options = {"Instrument": "LARMOR",
+                          "BinParams": '1,10,1000',
+                          "UnitX": 'MomentumTransfer',
+                          "OutputWorkspace": EMPTY_NAME}
+        create_alg = create_unmanaged_algorithm(create_name, **create_options)
+        create_alg.execute()
+        workspace = create_alg.getProperty("OutputWorkspace").value
+
+        crop_name = "CropWorkspace"
+        crop_options = {"InputWorkspace": workspace,
+                        "OutputWorkspace": EMPTY_NAME,
+                        "EndWorkspaceIndex": 0}
+        crop_alg = create_unmanaged_algorithm(crop_name, **crop_options)
+        crop_alg.execute()
+        workspace = crop_alg.getProperty("OutputWorkspace").value
+
+        if convert_to_numeric_axis:
+            convert_name = "ConvertSpectrumAxis"
+            convert_options = {"InputWorkspace": workspace,
+                               "OutputWorkspace": EMPTY_NAME,
+                               "Target": 'ElasticQ',
+                               "EFixed": 1}
+            convert_alg = create_unmanaged_algorithm(convert_name, **convert_options)
+            convert_alg.execute()
+            workspace = convert_alg.getProperty("OutputWorkspace").value
+
+        if with_zero_errors:
+            errors = workspace.dataE(0)
+            errors[0] = 0.0
+            errors[14] = 0.0
+            errors[45] = 0.0
+        return workspace
+
+    def _assert_that_file_exists(self, file_name):
+        self.assertTrue(os.path.exists(file_name))
+
+    def _remove_file(self, file_name):
+        if os.path.exists(file_name):
+            os.remove(file_name)
+
+    def test_that_workspace_can_be_saved_without_zero_error_free_option(self):
+        # Arrange
+        workspace = SANSSaveTest._get_sample_workspace(with_zero_errors=False, convert_to_numeric_axis=True)
+        file_name = os.path.join(mantid.config.getString('defaultsave.directory'), 'sample_sans_save_file')
+        use_zero_errors_free = False
+        save_name = "SANSSave"
+        save_options = {"InputWorkspace": workspace,
+                        "Filename": file_name,
+                        "UseZeroErrorFree": use_zero_errors_free,
+                        "Nexus": True,
+                        "CanSAS": True,
+                        "NXCanSAS": True,
+                        "NistQxy": True,
+                        "RKH": True,
+                        "CSV": True}
+        save_alg = create_unmanaged_algorithm(save_name, **save_options)
+
+        # Act
+        save_alg.execute()
+        self.assertTrue(save_alg.isExecuted())
+
+        # Assert
+        expected_files = ["sample_sans_save_file.xml", "sample_sans_save_file.txt", "sample_sans_save_file_nistqxy.dat",
+                          "sample_sans_save_file_nxcansas.nxs", "sample_sans_save_file.nxs",
+                          "sample_sans_save_file.csv"]
+        expected_full_file_names = [os.path.join(mantid.config.getString('defaultsave.directory'), elem)
+                                    for elem in expected_files]
+        for file_name in expected_full_file_names:
+            self._assert_that_file_exists(file_name)
+
+        # Clean up
+        for file_name in expected_full_file_names:
+            self._remove_file(file_name)
+
+    def test_that_nistqxy_cannot_be_saved_if_axis_is_spectra_axis(self):
+        # Arrange
+        workspace = SANSSaveTest._get_sample_workspace(with_zero_errors=False, convert_to_numeric_axis=False)
+        file_name = os.path.join(mantid.config.getString('defaultsave.directory'), 'sample_sans_save_file')
+        use_zero_errors_free = False
+        save_name = "SANSSave"
+        save_options = {"InputWorkspace": workspace,
+                        "Filename": file_name,
+                        "UseZeroErrorFree": use_zero_errors_free,
+                        "Nexus": False,
+                        "CanSAS": False,
+                        "NXCanSAS": False,
+                        "NistQxy": True,
+                        "RKH": False,
+                        "CSV": False}
+        save_alg = create_unmanaged_algorithm(save_name, **save_options)
+        save_alg.setRethrows(True)
+        # Act
+        try:
+            save_alg.execute()
+            did_raise = False
+        except RuntimeError:
+            did_raise = True
+        self.assertTrue(did_raise)
+
+    def test_that_if_no_format_is_selected_raises(self):
+        # Arrange
+        workspace = SANSSaveTest._get_sample_workspace(with_zero_errors=False, convert_to_numeric_axis=True)
+        file_name = os.path.join(mantid.config.getString('defaultsave.directory'), 'sample_sans_save_file')
+        use_zero_errors_free = True
+        save_name = "SANSSave"
+        save_options = {"InputWorkspace": workspace,
+                        "Filename": file_name,
+                        "UseZeroErrorFree": use_zero_errors_free,
+                        "Nexus": False,
+                        "CanSAS": False,
+                        "NXCanSAS": False,
+                        "NistQxy": False,
+                        "RKH": False,
+                        "CSV": False}
+        save_alg = create_unmanaged_algorithm(save_name, **save_options)
+        save_alg.setRethrows(True)
+        # Act
+        try:
+            save_alg.execute()
+            did_raise = False
+        except RuntimeError:
+            did_raise = True
+        self.assertTrue(did_raise)
+
+    def test_that_zero_error_is_removed(self):
+        # Arrange
+        workspace = SANSSaveTest._get_sample_workspace(with_zero_errors=True, convert_to_numeric_axis=True)
+        file_name = os.path.join(mantid.config.getString('defaultsave.directory'), 'sample_sans_save_file')
+        use_zero_errors_free = True
+        save_name = "SANSSave"
+        save_options = {"InputWorkspace": workspace,
+                        "Filename": file_name,
+                        "UseZeroErrorFree": use_zero_errors_free,
+                        "Nexus": True,
+                        "CanSAS": False,
+                        "NXCanSAS": False,
+                        "NistQxy": False,
+                        "RKH": False,
+                        "CSV": False}
+        save_alg = create_unmanaged_algorithm(save_name, **save_options)
+
+        # Act
+        save_alg.execute()
+        self.assertTrue(save_alg.isExecuted())
+        file_name = os.path.join(mantid.config.getString('defaultsave.directory'), "sample_sans_save_file.nxs")
+
+        load_name = "LoadNexusProcessed"
+        load_options = {"Filename": file_name,
+                        "OutputWorkspace": EMPTY_NAME}
+        load_alg = create_unmanaged_algorithm(load_name, **load_options)
+        load_alg.execute()
+        reloaded_workspace = load_alg.getProperty("OutputWorkspace").value
+        errors = reloaded_workspace.dataE(0)
+        # Make sure that the errors are not zero
+        self.assertTrue(errors[0] > 1.0)
+        self.assertTrue(errors[14] > 1.0)
+        self.assertTrue(errors[45] > 1.0)
+
+        # Clean up
+        self._remove_file(file_name)
+
+
+class SANSSaveRunnerTest(stresstesting.MantidStressTest):
+    def __init__(self):
+        stresstesting.MantidStressTest.__init__(self)
+        self._success = False
+
+    def runTest(self):
+        suite = unittest.TestSuite()
+        suite.addTest(unittest.makeSuite(SANSSaveTest, 'test'))
+        runner = unittest.TextTestRunner()
+        res = runner.run(suite)
+        if res.wasSuccessful():
+            self._success = True
+
+    def requiredMemoryMB(self):
+        return 1000
+
+    def validate(self):
+        return self._success
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/docs/source/algorithms/SANSConvertToQ-v1.rst b/docs/source/algorithms/SANSConvertToQ-v1.rst
new file mode 100644
index 0000000000000000000000000000000000000000..e876312fcb767d12db1f8477a2854203191b37df
--- /dev/null
+++ b/docs/source/algorithms/SANSConvertToQ-v1.rst
@@ -0,0 +1,112 @@
+.. algorithm::
+
+.. summary::
+
+.. alias::
+
+.. properties::
+
+Description
+-----------
+
+This algorithm converts a SANS workspace in wavelength to a workspace in momentum transfer. The conversion is either provided by :ref:`algm-Q1D` or by :ref:`algm-Qxy`. The settings for the algorithm
+are provided by the state object. Besides the algorithm which is to be converted three types of 
+adjustment workspaces can be provided:
+
+- Wavelength adjustment worspace which performs a correction on the bins but is the same for each pixel.
+- Pixel adjustment workspace which performs a correction on each pixel but is the same for each bin.
+- Wavelength and pixel adjustment workspace which performs a correction on the bins and pixels individually.
+
+Note that the *OutputParts* option allows for extracting the count and normalization workspaces which are being produced by :ref:`algm-Q1D` or by :ref:`algm-Qxy`.
+
+Currently the mask mechanism is implemented for **SANS2D**, **LOQ** and **LARMOR**.
+
+
+Relevant SANSState entries for SANSConvertToQ
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The required information for the momentum transfer conversion is retrieved from a state object.
+
+The elements are:
+
++---------------------------------+----------------+-------------------------------------+--------------------+--------+
+| Entry                           | Type           | Description                         | Mandatory          | Default|
++=================================+================+=====================================+====================+========+
+| reduction_dimensionality        | Reduction-     | The dimensionality of the reduction | No                 | None   |
+|                                 | Dimensionality |                                     |                    |        |
+|                                 | enum           |                                     |                    |        |
++---------------------------------+----------------+-------------------------------------+--------------------+--------+
+| use_gravity                     | Bool           | If a gravity correction is to       | No                 | False  |
+|                                 |                | be applied                          |                    |        |
++---------------------------------+----------------+-------------------------------------+--------------------+--------+
+| gravity_extra_length            | Float          | The additional length in m if a     | No                 | 0.     |
+|                                 |                | gravity correction is to be applied |                    |        |
++---------------------------------+----------------+-------------------------------------+--------------------+--------+
+| radius_cutoff                   | Float          | A radius cutoff in m                | No                 | 0.     |
++---------------------------------+----------------+-------------------------------------+--------------------+--------+
+| wavelength_cutoff               | RangeStepType  | A wavelength cutoff                 | No                 | 0.     |
+|                                 | enum           |                                     |                    |        |
++---------------------------------+----------------+-------------------------------------+--------------------+--------+
+| q_min                           | Float          | The minimal momentum transfer       | Yes, if 2D setting | None   |
+|                                 |                | (only relevant for 1D reductions)   | has not been prov. |        |
++---------------------------------+----------------+-------------------------------------+--------------------+--------+
+| q_max                           | Float          | The maximal momentum transfer       | Yes, if 2D setting | None   |
+|                                 |                | (only relevant for 1D reductions)   | has not been prov. |        |
++---------------------------------+----------------+-------------------------------------+--------------------+--------+
+| q_1d_rebin_string               | String         | Rebinning parameters for momentum   | Yes, if 2D setting | None   |
+|                                 |                | transfer                            | has not been prov. |        |
++---------------------------------+----------------+-------------------------------------+--------------------+--------+
+| q_xy_max                        | Float          | Maximal momentum transfer           | Yes, if 1D setting | None   |
+|                                 |                | (only relevant for 2D reduction)    | has not been prov. |        |
++---------------------------------+----------------+-------------------------------------+--------------------+--------+
+| q_xy_max                        | Float          | Maximal momentum transfer           | Yes, if 1D setting | None   |
+|                                 |                | (only relevant for 2D reduction)    | has not been prov. |        |
++---------------------------------+----------------+-------------------------------------+--------------------+--------+
+| q_xy_step                       | Float          | Momentum transfer step              | Yes, if 1D setting | None   |
+|                                 |                | (only relevant for 2D reduction)    | has not been prov. |        |
++---------------------------------+----------------+-------------------------------------+--------------------+--------+
+| q_xy_step_type                  | RangeStepType  | Momentum transfer step type         | Yes, if 1D setting | None   |
+|                                 | enum           | (only relevant for 2D reduction)    | has not been prov. |        |
++---------------------------------+----------------+-------------------------------------+--------------------+--------+
+| use_q_resolution                | Bool           | If momentum transfer resolution     | No                 | None   |
+|                                 |                | calculation is to be used           |                    |        |
++---------------------------------+----------------+-------------------------------------+--------------------+--------+
+| q_resolution_collimation_length | Float          | The collimation length in m         | No                 | None   |
++---------------------------------+----------------+-------------------------------------+--------------------+--------+
+| q_resolution_delta_r            |  Float         | The virtual ring width on the       | No                 | None   |
+|                                 |                | detector                            |                    |        |
++---------------------------------+----------------+-------------------------------------+--------------------+--------+
+| moderator_file                  |  String        | Moderator file with time spread     | No                 | None   |
+|                                 |                | information                         |                    |        |
++---------------------------------+----------------+-------------------------------------+--------------------+--------+
+| q_resolution_a1                 |  Float         | Source aperture radius              | If use_q_resolution| None   |
+|                                 |                | information                         | is set and rect.   |        |
+|                                 |                | information                         | app. is not set    |        |
++---------------------------------+----------------+-------------------------------------+--------------------+--------+
+| q_resolution_a2                 |  Float         | Sample aperture radius              | If use_q_resolution| None   |
+|                                 |                | information                         | is set and rect.   |        |
+|                                 |                | information                         | app. is not set    |        |
++---------------------------------+----------------+-------------------------------------+--------------------+--------+
+| q_resolution_h1                 |  Float         | Source aperture height              | If use_q_resolution| None   |
+|                                 |                | (rectangular)                       | is set and circ.   |        |
+|                                 |                |                                     | app. is not set    |        |
++---------------------------------+----------------+-------------------------------------+--------------------+--------+
+| q_resolution_h2                 |  Float         | Sample aperture height              | If use_q_resolution| None   |
+|                                 |                | (rectangular)                       | is set and circ.   |        |
+|                                 |                |                                     | app. is not set    |        |
++---------------------------------+----------------+-------------------------------------+--------------------+--------+
+| q_resolution_w1                 |  Float         | Source aperture width               | If use_q_resolution| None   |
+|                                 |                | (rectangular)                       | is set and circ.   |        |
+|                                 |                |                                     | app. is not set    |        |
++---------------------------------+----------------+-------------------------------------+--------------------+--------+
+| q_resolution_w2                 |  Float         | Sample aperture width               | If use_q_resolution| None   |
+|                                 |                | (rectangular)                       | is set and circ.   |        |
+|                                 |                |                                     | app. is not set    |        |
++---------------------------------+----------------+-------------------------------------+--------------------+--------+
+
+
+Note that the momentum transfer resolution calculation is only applicable for 1D reductions.
+
+.. categories::
+
+.. sourcelink::
diff --git a/docs/source/algorithms/SANSConvertToWavelength-v1.rst b/docs/source/algorithms/SANSConvertToWavelength-v1.rst
new file mode 100644
index 0000000000000000000000000000000000000000..4baa1d4addb2b69e90787f7dfc4cfde5b12d5384
--- /dev/null
+++ b/docs/source/algorithms/SANSConvertToWavelength-v1.rst
@@ -0,0 +1,43 @@
+.. algorithm::
+
+.. summary::
+
+.. alias::
+
+.. properties::
+
+Description
+-----------
+
+This algorithm converts a SANS workspace to wavelength and rebins it. The settings for the wavelength conversion and the rebining are stored in the state object. Currently the mask mechanism
+is implemented for **SANS2D**, **LOQ** and **LARMOR**.
+
+
+
+Relevant SANSState entries for SANSConvertToWavelength
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The required information for the wavelength conversion is retrieved from a state object.
+
+
+The elements of the wavelength conversion state are:
+
++----------------------+--------------------+----------------------------------------------+------------+---------------+
+| Entry                | Type               | Description                                  | Mandatory  | Default value |
++======================+====================+==============================================+============+===============+
+| rebin_type           | RebinType enum     | The type of rebin, ie Rebin or Interpolating | No         | None          |
++----------------------+--------------------+----------------------------------------------+------------+---------------+
+| wavelength_low       | Float              | Lower wavelength bound                       | No         | None          |
++----------------------+--------------------+----------------------------------------------+------------+---------------+
+| wavelength_high      | Float              | Upper wavelength bound                       | No         | None          |
++----------------------+--------------------+----------------------------------------------+------------+---------------+
+| wavelength_step      | Float              | Wavelength step                              | No         | None          |
++----------------------+--------------------+----------------------------------------------+------------+---------------+
+| wavelength_step_type | RangeStepType enum | Wavelength step type                         | No         | None          |
++----------------------+--------------------+----------------------------------------------+------------+---------------+
+
+
+
+.. categories::
+
+.. sourcelink::
diff --git a/docs/source/algorithms/SANSConvertToWavelengthAndRebin-v1.rst b/docs/source/algorithms/SANSConvertToWavelengthAndRebin-v1.rst
new file mode 100644
index 0000000000000000000000000000000000000000..3dc3f2e2dad2263774072c71bb729037a63abc05
--- /dev/null
+++ b/docs/source/algorithms/SANSConvertToWavelengthAndRebin-v1.rst
@@ -0,0 +1,45 @@
+.. algorithm::
+
+.. summary::
+
+.. alias::
+
+.. properties::
+
+Description
+-----------
+
+This algorithm converts the input workspace into a workspace with wavelength units. Subsequently it rebins the
+wavelength-valued workspace. Either :ref:`algm-Rebin` or :ref:`algm-InterpolatingRebin` is used for rebinning. This algorithm
+is geared towards SANS workspaces.
+
+
+Usage
+-----
+
+.. include:: ../usagedata-note.txt
+
+**Example - Convert sample workspace to wavelength and rebin:**
+
+.. testcode:: ExSANSConvertToWavelengthAndRebin
+
+    ws = CreateSampleWorkspace(NumBanks=1, BankPixelWidth=1)
+    converted = SANSConvertToWavelengthAndRebin(InputWorkspace=ws, WavelengthLow=1, WavelengthHigh=4, WavelengthStep=1, WavelengthStepType="Lin", RebinMode="Rebin")
+
+    print("There should be 4 values and got {}.".format(len(converted.dataX(0))))
+
+.. testcleanup:: ExSANSConvertToWavelengthAndRebin
+
+   DeleteWorkspace('ws')
+   DeleteWorkspace('converted')
+
+
+Output:
+
+.. testoutput:: ExSANSConvertToWavelengthAndRebin
+
+  There should be 4 values and got 4.
+
+.. categories::
+
+.. sourcelink::
diff --git a/docs/source/algorithms/SANSCrop-v1.rst b/docs/source/algorithms/SANSCrop-v1.rst
new file mode 100644
index 0000000000000000000000000000000000000000..9e5aa7fb7db119c0d8cd4bb321a7e56c4b23a4f5
--- /dev/null
+++ b/docs/source/algorithms/SANSCrop-v1.rst
@@ -0,0 +1,32 @@
+.. algorithm::
+
+.. summary::
+
+.. alias::
+
+.. properties::
+
+Description
+-----------
+
+This algorithm allows to crop a particular detector bank from a workspace. The supported configurations are *LAB* and *HAB*. Currently this crop mechanism is implemented for **SANS2D**, **LOQ** and **LARMOR**.
+
+Component setting: *LAB* and *HAB*
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The mapping of this setting is:
+
++------------+---------------------+-------------------+
+| Instrument | *LAB*               | *HAB*             |
++============+=====================+===================+
+| SANS2D     | "rear-detector"     | front-detector"   |
++------------+---------------------+-------------------+
+| LOQ        | "main-detector-bank"| "HAB"             |
++------------+---------------------+-------------------+
+| LARMOR     | "DetectorBench"     | "DetectorBench"   |
++------------+---------------------+-------------------+
+
+
+.. categories::
+
+.. sourcelink::
diff --git a/docs/source/algorithms/SANSLoad-v1.rst b/docs/source/algorithms/SANSLoad-v1.rst
index d6d69a56119a5a0552620c07c587fbb5029096ab..fdd8605077d152b807a9cfb6da82d25779bf343d 100644
--- a/docs/source/algorithms/SANSLoad-v1.rst
+++ b/docs/source/algorithms/SANSLoad-v1.rst
@@ -30,35 +30,46 @@ calibration file which is applied to the scatter workspaces.
 
 The elements of the SANSState are:
 
-+---------------------------+--------+------------------------------------------+----------------------------------------------+
-| Entry                     | Type   | Description                              | Mandatory                                    |
-+===========================+========+==========================================+==============================================+
-| sample_scatter            | String | The name of the sample scatter file      | Yes                                          |
-+---------------------------+--------+------------------------------------------+----------------------------------------------+
-| sample_scatter_period     | Int    | The selected period or (0 for all)       | No                                           |
-+---------------------------+--------+------------------------------------------+----------------------------------------------+
-| sample_transmission       | String | The name of the sample transmission file | No, only if sample_direct was specified      |
-+---------------------------+--------+------------------------------------------+----------------------------------------------+
-| sample_transmission_period| Int    | The selected period or (0 for all)       | No                                           |
-+---------------------------+--------+------------------------------------------+----------------------------------------------+
-| sample_direct             | String | The name of the sample direct file       | No, only if sample_transmission was specified|
-+---------------------------+--------+------------------------------------------+----------------------------------------------+
-| sample_direct_period      | Int    | The selected period or (0 for all)       | No                                           |
-+---------------------------+--------+------------------------------------------+----------------------------------------------+
-| can_scatter               | String | The name of the can scatter file         | No, only if can_transmission was specified   |
-+---------------------------+--------+------------------------------------------+----------------------------------------------+
-| can_scatter_period        | Int    | The selected period or (0 for all)       | No                                           |
-+---------------------------+--------+------------------------------------------+----------------------------------------------+
-| can_transmission          | String | The name of the can transmission file    | No, only if can_direct was specified         |
-+---------------------------+--------+------------------------------------------+----------------------------------------------+
-| can_transmission_period   | Int    | The selected period or (0 for all)       | No                                           |
-+---------------------------+--------+------------------------------------------+----------------------------------------------+
-| can_direct                | String | The name of the can direct file          | No, only if can_direct was specified         |
-+---------------------------+--------+------------------------------------------+----------------------------------------------+
-| can_direct_period         | Int    | The selected period or (0 for all)       | No                                           |
-+---------------------------+--------+------------------------------------------+----------------------------------------------+
-| calibration               | String | The name of the calibration file         | No                                           |
-+---------------------------+--------+------------------------------------------+----------------------------------------------+
++--------------------------------+---------------------+------------------------------------------+----------------------------------------------+
+| Entry                          | Type                | Description                              | Mandatory                                    |
++================================+=====================+==========================================+==============================================+
+| sample_scatter                 | String              | The name of the sample scatter file      | Yes                                          |
++--------------------------------+---------------------+------------------------------------------+----------------------------------------------+
+| sample_scatter_period          | Int                 | The selected period or (0 for all)       | No                                           |
++--------------------------------+---------------------+------------------------------------------+----------------------------------------------+
+| sample_transmission            | String              | The name of the sample transmission file | No, only if sample_direct was specified      |
++--------------------------------+---------------------+------------------------------------------+----------------------------------------------+
+| sample_transmission_period     | Int                 | The selected period or (0 for all)       | No                                           |
++--------------------------------+---------------------+------------------------------------------+----------------------------------------------+
+| sample_direct                  | String              | The name of the sample direct file       | No, only if sample_transmission was specified|
++--------------------------------+---------------------+------------------------------------------+----------------------------------------------+
+| sample_direct_period           | Int                 | The selected period or (0 for all)       | No                                           |
++--------------------------------+---------------------+------------------------------------------+----------------------------------------------+
+| can_scatter                    | String              | The name of the can scatter file         | No, only if can_transmission was specified   |
++--------------------------------+---------------------+------------------------------------------+----------------------------------------------+
+| can_scatter_period             | Int                 | The selected period or (0 for all)       | No                                           |
++--------------------------------+---------------------+------------------------------------------+----------------------------------------------+
+| can_transmission               | String              | The name of the can transmission file    | No, only if can_direct was specified         |
++--------------------------------+---------------------+------------------------------------------+----------------------------------------------+
+| can_transmission_period        | Int                 | The selected period or (0 for all)       | No                                           |
++--------------------------------+---------------------+------------------------------------------+----------------------------------------------+
+| can_direct                     | String              | The name of the can direct file          | No, only if can_direct was specified         |
++--------------------------------+---------------------+------------------------------------------+----------------------------------------------+
+| can_direct_period              | Int                 | The selected period or (0 for all)       | No                                           |
++--------------------------------+---------------------+------------------------------------------+----------------------------------------------+
+| calibration                    | String              | The name of the calibration file         | No                                           |
++--------------------------------+---------------------+------------------------------------------+----------------------------------------------+
+| sample_scatter_run_number      | Int                 | The run number of the sample scatter     | auto setup                                   |
++--------------------------------+---------------------+------------------------------------------+----------------------------------------------+
+| sample_scatter_is_multi_period | Bool                | If the sample is multi-period            | auto setup                                   |
++--------------------------------+---------------------+------------------------------------------+----------------------------------------------+
+| instrument                     | SANSInstrument enum | The name of the calibration file         | auto setup                                   |
++--------------------------------+---------------------+------------------------------------------+----------------------------------------------+
+| idf_file_path                  | String              | The path to the IDF                      | auto setup                                   |
++--------------------------------+---------------------+------------------------------------------+----------------------------------------------+
+| ipf_file_path                  | String              | The path to the IPF                      | auto setup                                   |
++--------------------------------+---------------------+------------------------------------------+----------------------------------------------+
+
 
 Note that these settings should be only populated via the GUI or the Python Interface of ISIS SANS.
 
diff --git a/docs/source/algorithms/SANSMaskWorkspace-v1.rst b/docs/source/algorithms/SANSMaskWorkspace-v1.rst
new file mode 100644
index 0000000000000000000000000000000000000000..61d5e81358fb1566c702b47e9f062787dd4655fa
--- /dev/null
+++ b/docs/source/algorithms/SANSMaskWorkspace-v1.rst
@@ -0,0 +1,133 @@
+.. algorithm::
+
+.. summary::
+
+.. alias::
+
+.. properties::
+
+Description
+-----------
+
+This algorithm masks a SANS workspace according to the settings in the state object. The user can specify which detector
+to mask. Currently the mask mechanism
+is implemented for **SANS2D**, **LOQ** and **LARMOR**.
+
+There are several types of masking which are currently supported:
+
+- Time/Bin masking.
+- Radius masking.
+- Mask files.
+- Angle masking.
+- Spectrum masking which includes individual spectra, spectra ranges, spectra blocks and spectra cross blocks. These masks are partially specified on a detector level (see below).
+- Beam stop masking.
+
+
+Relevant SANSState entries for SANSMaskWorkspace
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The required information for the mask operation is retrieved from a state object. The state for masking is a composite of 
+general settings and detector specific settings (see below).
+
+
+The elements of the general mask state are:
+
++-----------------------+-----------------+---------------------------------------------+------------+---------------+
+| Entry                 | Type            | Description                                 | Mandatory  | Default value |
++=======================+=================+=============================================+============+===============+
+| radius_min            | Float           | Minimum radius in m for the radius mask     | No         | None          |
++-----------------------+-----------------+---------------------------------------------+------------+---------------+
+| radius_min            | Float           | Maximum radius in m for the radius mask     | No         | None          |
++-----------------------+-----------------+---------------------------------------------+------------+---------------+
+| bin_mask_general_start| List of Float   | A list of start values for time masking     | No         | None          |
++-----------------------+-----------------+---------------------------------------------+------------+---------------+
+| bin_mask_general_stop | List of Float   | A list of stop values for time masking      | No         | None          |
++-----------------------+-----------------+---------------------------------------------+------------+---------------+
+| mask_files            | List of String  | A list of mask file names                   | No         | None          |
++-----------------------+-----------------+---------------------------------------------+------------+---------------+
+| phi_min               | Float           | Minimum angle for angle masking             | No         | None          |
++-----------------------+-----------------+---------------------------------------------+------------+---------------+
+| phi_max               | Float           | Maximum angle for angle masking             | No         | None          |
++-----------------------+-----------------+---------------------------------------------+------------+---------------+
+| use_mask_phi_mirror   | Bool            | If a mirrored angle mask is to be used      | No         | None          |
++-----------------------+-----------------+---------------------------------------------+------------+---------------+
+| beam_stop_arm_width   | Float           | Size fo the beam stop arm in m              | No         | None          |
++-----------------------+-----------------+---------------------------------------------+------------+---------------+
+| beam_stop_arm_angle   | Float           | Angle of the beam stop arm                  | No         | None          |
++-----------------------+-----------------+---------------------------------------------+------------+---------------+
+| beam_stop_arm_pos1    | Float           | First coordinate of the beam stop position  | No         | None          |
++-----------------------+-----------------+---------------------------------------------+------------+---------------+
+| beam_stop_arm_pos2    | Float           | Second coordinate of the beam stop position | No         | None          |
++-----------------------+-----------------+---------------------------------------------+------------+---------------+
+| clear                 | Bool            | If the spectra mask is to be cleared        | No         | None          |
++-----------------------+-----------------+---------------------------------------------+------------+---------------+
+| clear_time            | Float           | If the time mask is to be cleared           | No         | None          |
++-----------------------+-----------------+---------------------------------------------+------------+---------------+
+| single_spectra        | List of Integer | List of spectra which are to be masked      | No         | None          |
++-----------------------+-----------------+---------------------------------------------+------------+---------------+
+| spectrum_range_start  | List of Integer | List of specra where a range mask starts    | No         | None          |
++-----------------------+-----------------+---------------------------------------------+------------+---------------+
+| spectrum_range_stop   | List of Integer | List of specra where a range mask stops     | No         | None          |
++-----------------------+-----------------+---------------------------------------------+------------+---------------+
+| detectors             | Dict            | A map to detector-specific settings         | No         | None          |
++-----------------------+-----------------+---------------------------------------------+------------+---------------+
+| idf_path              | String          | The path to the IDF                         | auto setup | auto setup    |
++-----------------------+-----------------+---------------------------------------------+------------+---------------+
+
+
+
+The detectors dictionary above maps to a mask state object for the individual detectors :
+
++-----------------------------+-----------------+--------------------------------------+------------+---------------+
+| Entry                       | Type            | Description                          | Mandatory  | Default value |
++=============================+=================+======================================+============+===============+
+| single_vertical_strip_mask  | List of Integer | A list of vertical strip masks       | No         | 0.0           |
++-----------------------------+-----------------+--------------------------------------+------------+---------------+
+| range_vertical_strip_start  | List of Integer | A list of start spectra for vertical | No         | 0.0           |
+|                             |                 | strip mask ranges                    |            |               |
++-----------------------------+-----------------+--------------------------------------+------------+---------------+
+| range_vertical_strip_stop   | List of Integer | A list of stop spectra for vertical  | No         | 0.0           |
+|                             |                 | strip mask ranges                    |            |               |
++-----------------------------+-----------------+--------------------------------------+------------+---------------+
+| single_horizontal_strip_mask| List of Integer | A list of horizontal strip masks     | No         | 0.0           |
++-----------------------------+-----------------+--------------------------------------+------------+---------------+
+| range_horizontal_strip_start| List of Integer | A list of start spectra for          | No         | 0.0           |
+|                             |                 | horizontal strip mask ranges         |            |               |
++-----------------------------+-----------------+--------------------------------------+------------+---------------+
+| range_horizontal_strip_stop | List of Integer | A list of stop spectra for           | No         | 0.0           |
+|                             |                 | horizontal strip mask ranges         |            |               |
++-----------------------------+-----------------+--------------------------------------+------------+---------------+
+| block_horizontal_start      | List of Integer | A list of start spectra for the      | No         | 0.0           |
+|                             |                 | horizontal part of block masks       |            |               |
++-----------------------------+-----------------+--------------------------------------+------------+---------------+
+| block_horizontal_stop       | List of Integer | A list of stop spectra for the       | No         | 0.0           |
+|                             |                 | horizontal part of block masks       |            |               |
++-----------------------------+-----------------+--------------------------------------+------------+---------------+
+| block_vertical_start        | List of Integer | A list of start spectra for the      | No         | 0.0           |
+|                             |                 | vertical part of block masks         |            |               |
++-----------------------------+-----------------+--------------------------------------+------------+---------------+
+| block_vertical_stop         | List of Integer | A list of stop spectra for the       | No         | 0.0           |
+|                             |                 | vertical part of block masks         |            |               |
++-----------------------------+-----------------+--------------------------------------+------------+---------------+
+| block_cross_horizontal      | List of Integer | A list of spectra for the horizontal | No         | 0.0           |
+|                             |                 | part of cross block masks            |            |               |
++-----------------------------+-----------------+--------------------------------------+------------+---------------+
+| block_cross_vertical        | List of Integer | A list of spectra for the vertical   | No         | 0.0           |
+|                             |                 | part of cross block masks            |            |               |
++-----------------------------+-----------------+--------------------------------------+------------+---------------+
+
+
+Note that these settings should be only populated via the GUI or the Python Interface of ISIS SANS.
+
+
+Mask options for the detector: *LAB*, *HAB*
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The *LAB* (low angle bank) setting selects the first detector of the instrument.
+
+The *HAB* (high angle bank) setting selects the first detector of the instrument.
+
+
+.. categories::
+
+.. sourcelink::
diff --git a/docs/source/algorithms/SANSMove-v1.rst b/docs/source/algorithms/SANSMove-v1.rst
index a8761d53639479d703f3ca8d0db00a7d3fc7c118..2dab9bba62766581fec3b7ae760f1995d5d64eb8 100644
--- a/docs/source/algorithms/SANSMove-v1.rst
+++ b/docs/source/algorithms/SANSMove-v1.rst
@@ -24,18 +24,19 @@ specific to each instrument and to the specific IDF.
 
 Common elements of the move state object are:
 
-+---------------------------+---------------------------+-------------------------------------+------------+------------------------+
-| Entry                     | Type                      | Description                         | Mandatory  | Default value          |
-+===========================+===========================+=====================================+============+========================+
-| sample_offset             | Float                     | The offset of the sample in m       | No         | 0.0                    |
-+---------------------------+---------------------------+-------------------------------------+------------+------------------------+
-| sample_offset_direction   | CanonicalCoordinates enum | The direction of the sample offset  | No         | CanonicalCoordinates.Z |
-+---------------------------+---------------------------+-------------------------------------+------------+------------------------+
-| detectors                 | Dict                      |  Dictionary of detectors.           | auto setup | auto setup             |
-+---------------------------+---------------------------+-------------------------------------+------------+------------------------+
-
-
-The detectors dictionary above maps to a state object for the individual detectors.
++---------------------------+---------------------------+-------------------------------------------------+------------+------------------------+
+| Entry                     | Type                      | Description                                     | Mandatory  | Default value          |
++===========================+===========================+=================================================+============+========================+
+| sample_offset             | Float                     | The offset of the sample in m                   | No         | 0.0                    |
++---------------------------+---------------------------+-------------------------------------------------+------------+------------------------+
+| sample_offset_direction   | CanonicalCoordinates enum | The direction of the sample offset              | No         | CanonicalCoordinates.Z |
++---------------------------+---------------------------+-------------------------------------------------+------------+------------------------+
+| detectors                 | Dict                      | Dictionary of detectors.                        | auto setup | auto setup             |
++---------------------------+---------------------------+-------------------------------------------------+------------+------------------------+
+| monitor_names             | Dict                      | A dictionary with monitor index vs monitor name | auto setup | auto setup             |
++---------------------------+---------------------------+-------------------------------------------------+------------+------------------------+
+
+The detectors dictionary above maps to a state object for the individual detectors:
 
 +--------------------------+--------+------------------------------------------------------------------------+------------+---------------+
 | Entry                    | Type   | Description                                                            | Mandatory  | Default value |
@@ -64,34 +65,32 @@ The detectors dictionary above maps to a state object for the individual detecto
 +--------------------------+--------+------------------------------------------------------------------------+------------+---------------+
 | detector_name            | String | Detector name                                                          | auto setup | auto setup    |
 +--------------------------+--------+------------------------------------------------------------------------+------------+---------------+
-| detector_name_short      | String | Short detector name                                                    | No         | auto setup    |
+| detector_name_short      | String | Short detector name                                                    | auto setup | auto setup    |
 +--------------------------+--------+------------------------------------------------------------------------+------------+---------------+
 
 
 The individual instruments have additional settings.
 
 
-For LOQ
+For LOQ:
 
 +-----------------+-------+-------------------------------------------------+--------------+---------------+
 | Entry           | Type  | Description                                     | Mandatory    | Default value |
 +=================+=======+=================================================+==============+===============+
-| monitor_names   | Dict  | A dictionary with monitor index vs monitor name | auto setup   | auto setup    | 
 +-----------------+-------+-------------------------------------------------+--------------+---------------+
 | center_position | Float | The centre position                             | No           | 317.5 / 1000. |
 +-----------------+-------+-------------------------------------------------+--------------+---------------+
 
 
-For SANS2D
+For SANS2D:
 
 +---------------------------+-------+-------------------------------------------------+------------+---------------+
 | Entry                     | Type  | Description                                     | Mandatory  | Default value |
 +===========================+=======+=================================================+============+===============+
-| monitor_names             | Dict  | A dictionary with monitor index vs monitor name | auto setup | auto setup    |
 +---------------------------+-------+-------------------------------------------------+------------+---------------+
 | hab_detector_radius       | Float | Radius for the front detector in m              | auto setup | 306.0 / 1000. |
 +---------------------------+-------+-------------------------------------------------+------------+---------------+
-| hab_detector_default_sd_m | Float | Default sd for front detector in m              | auto setup | 317.5 / 1000. |
+| hab_detector_default_sd_m | Float | Default sd for front detector in m              | auto setup | 4.            |
 +---------------------------+-------+-------------------------------------------------+------------+---------------+
 | hab_detector_default_x_m  | Float | Default x for the front detector in m           | auto setup | 1.1           |
 +---------------------------+-------+-------------------------------------------------+------------+---------------+
@@ -99,7 +98,7 @@ For SANS2D
 +---------------------------+-------+-------------------------------------------------+------------+---------------+
 | hab_detector_x            | Float | X for the front detector in m                   | auto setup | 0.            |
 +---------------------------+-------+-------------------------------------------------+------------+---------------+
-| hab_detector_z            | Float | Z for the front detector in m                   | auto setup | 317.5 / 1000. |
+| hab_detector_z            | Float | Z for the front detector in m                   | auto setup | 0.            |
 +---------------------------+-------+-------------------------------------------------+------------+---------------+
 | hab_detector_rotation     | Float | Rotation for the front detector                 | auto setup | 0.            |
 +---------------------------+-------+-------------------------------------------------+------------+---------------+
@@ -116,7 +115,6 @@ For LARMOR
 +----------------+-------+-------------------------------------------------+------------+---------------+
 | Entry          | Type  | Description                                     | Mandatory  | Default value |
 +================+=======+=================================================+============+===============+
-| monitor_names  | Dict  | A dictionary with monitor index vs monitor name | auto setup | auto setup    |
 +----------------+-------+-------------------------------------------------+------------+---------------+
 | bench_rotation | Float | The angle for the bench rotation                | No         | 0.            |
 +----------------+-------+-------------------------------------------------+------------+---------------+
diff --git a/docs/source/algorithms/SANSSave-v1.rst b/docs/source/algorithms/SANSSave-v1.rst
new file mode 100644
index 0000000000000000000000000000000000000000..2c2cd936453e71d2143c9cd083f54a247c858688
--- /dev/null
+++ b/docs/source/algorithms/SANSSave-v1.rst
@@ -0,0 +1,28 @@
+.. algorithm::
+
+.. summary::
+
+.. alias::
+
+.. properties::
+
+Description
+-----------
+
+This algorithm saves a reduced SANS workspace in a variety of specified formats. The supported file formats are:
+
+- Nexus (1D/2D)
+- CanSAS (1D)
+- NXcanSAS (1D/2D)
+- NistQxy (2D)
+- RKH (1D/2D)
+- CSV (1D)
+
+
+Note that using the *UseZeroErrorFree* will result in outputs where the error is inflated if it had been 0. This ensures
+that the particular data point with vanishing error has no significance when used during fitting.
+
+
+.. categories::
+
+.. sourcelink::
diff --git a/docs/source/algorithms/SANSScale-v1.rst b/docs/source/algorithms/SANSScale-v1.rst
new file mode 100644
index 0000000000000000000000000000000000000000..a51495a1bd224e8398161a2ec13ba185fe179ce6
--- /dev/null
+++ b/docs/source/algorithms/SANSScale-v1.rst
@@ -0,0 +1,53 @@
+.. algorithm::
+
+.. summary::
+
+.. alias::
+
+.. properties::
+
+Description
+-----------
+
+This algorithm scales a SANS workspace according to the settings in the state object. The scaling includes division by the volume of the sample and 
+multiplication by an absolute scale. Currently the mask mechanism is implemented for **SANS2D**, **LOQ** and **LARMOR**.
+
+
+Relevant SANSState entries for SANSScale
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The required information for the scale operation is retrieved from a state object.
+
+
+The elements of the scale state are:
+
++---------------------+------------------+--------------------------------------------------------+------------+---------------+
+| Entry               | Type             | Description                                            | Mandatory  | Default value |
++=====================+==================+========================================================+============+===============+
+| shape               | SampleShape enum | The shape of the sample                                | No         | None          |
++---------------------+------------------+--------------------------------------------------------+------------+---------------+
+| thickness           | Float            | The sample thickness in m                              | No         | None          |
++---------------------+------------------+--------------------------------------------------------+------------+---------------+
+| width               |  Float           | The sample width in m                                  | None       |               |
++---------------------+------------------+--------------------------------------------------------+------------+---------------+
+| height              | Float            | The sample height in m                                 | None       |               |
++---------------------+------------------+--------------------------------------------------------+------------+---------------+
+| scale               | Float            | The absolute scale                                     | No         | None          |
++---------------------+------------------+--------------------------------------------------------+------------+---------------+
+| shape_from_file     | SampleShape enum | The shape of the sample as stored on the data file     | auto setup | Cylinder      |
++---------------------+------------------+--------------------------------------------------------+------------+---------------+
+| thickness_from_file | Float            | The thickness of the sample as stored on the data file | auto setup | 1.            |
++---------------------+------------------+--------------------------------------------------------+------------+---------------+
+| width_from_file     | Float            | The width of the sample as stored on the data file     | auto setup | 1.            |
++---------------------+------------------+--------------------------------------------------------+------------+---------------+
+| height_from_file    | Float            | The height of the sample as stored on the data file    | auto setup | 1.            |
++---------------------+------------------+--------------------------------------------------------+------------+---------------+
+
+
+Note that these settings should be only populated via the GUI or the Python Interface of ISIS SANS.
+
+
+
+.. categories::
+
+.. sourcelink::
diff --git a/docs/source/algorithms/SANSSliceEvent-v1.rst b/docs/source/algorithms/SANSSliceEvent-v1.rst
new file mode 100644
index 0000000000000000000000000000000000000000..03edb0f654715a63af58f67e43ad4da6a620b22c
--- /dev/null
+++ b/docs/source/algorithms/SANSSliceEvent-v1.rst
@@ -0,0 +1,46 @@
+.. algorithm::
+
+.. summary::
+
+.. alias::
+
+.. properties::
+
+Description
+-----------
+
+This algorithm creates a sliced workspaces from an event-based SANS input workspace according to the settings in the state object.
+The algorithm will extract a slice based on a start and end time which are set in the state object. In addtion the data type, ie
+if the slice is to be taken from a sample or a can workspace can be specified. Note that the monitor workspace is not being sliced but scaled by the ratio of the proton charge of the sliced worspace to the full workspace. Currently the mask mechanism is implemented for **SANS2D**, **LOQ** and **LARMOR**.
+
+
+Relevant SANSState entries for SANSSlice
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The required information for the slice operation is retrieved from a state object.
+
+
+The elements of the slice state are:
+
++-------------+---------------+-------------------------------------------------------+------------+---------------+
+| Entry       | Type          | Description                                           | Mandatory  | Default value |
++=============+===============+=======================================================+============+===============+
+| start_time  | List of Float | The first entry is used as a start time for the slice | No         | None          |
++-------------+---------------+-------------------------------------------------------+------------+---------------+
+| end_time    | List of Float | The first entry is used as a stop time for the slice  | No         | None          |
++-------------+---------------+-------------------------------------------------------+------------+---------------+
+
+
+Note that these settings should be only populated via the GUI or the Python Interface of ISIS SANS.
+
+Slice options for the data type: *Sample*, *Can*
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The *Sample* setting performs regular slicing
+
+The *Can* setting does not apply slicing to the specified workspaces.
+
+
+.. categories::
+
+.. sourcelink::
diff --git a/scripts/SANS/sans/README.md b/scripts/SANS/sans/README.md
index 4c54bd44384b903ffdc0cfb40139d94011231328..b5db376f60453d0bfe137788db5b4ae2986fa9df 100644
--- a/scripts/SANS/sans/README.md
+++ b/scripts/SANS/sans/README.md
@@ -16,6 +16,10 @@ The elements in the `common` package include widely used general purpose functio
 
 The elements in the `state` package contain the definition of the reduction configuration and the corresponding builders.
 
+## `test_helper`
+
+This elements of this package are used for testing the SANS reduction framework.
+
 ## `user_file`
 
-This elements of this package are used to parse a SANS user file and to setup a state object from the specified settings.
\ No newline at end of file
+The elements of this package are used to parse a SANS user file and to setup a state object from the specified settings.
\ No newline at end of file
diff --git a/scripts/SANS/sans/algorithm_detail/bundles.py b/scripts/SANS/sans/algorithm_detail/bundles.py
new file mode 100644
index 0000000000000000000000000000000000000000..c01c3a95c0ad71a79b61a76010188b50c6e1cad1
--- /dev/null
+++ b/scripts/SANS/sans/algorithm_detail/bundles.py
@@ -0,0 +1,42 @@
+""" This module contains bundle definitions for passing reduction settings between functions."""
+from __future__ import (absolute_import, division, print_function)
+from collections import namedtuple
+
+# The ReductionSettingBundle contains the information and data for starting a SANSReductionCore reduction.
+# This is:
+# 1. The state object for this particular reduction.
+# 2. The data type, i.e. if can or sample
+# 3. The reduction mode, ie HAB, LAB, All or Merged.
+# 4. A boolean flag if the output should also provide parts
+# 5. A handle to the scatter workspace (sample or can)
+# 6. A handle to the scatter monitor workspace (sample or can)
+# 7. A handle to the transmission workspace (sample or can)
+# 8. A handle to the direct workspace (sample or can)
+ReductionSettingBundle = namedtuple('ReductionSettingBundle', 'state, data_type, reduction_mode, '
+                                                              'output_parts, '
+                                                              'scatter_workspace, '
+                                                              'scatter_monitor_workspace, '
+                                                              'transmission_workspace, '
+                                                              'direct_workspace')
+
+# The MergeBundle contains:
+# 1. Handle to a merged workspace
+# 2. The shift factor which was used for the merge.
+# 3. The scale factor which was used for the merge.
+MergeBundle = namedtuple('MergeBundle', 'merged_workspace, shift, scale')
+
+# The OutputBundle contains.
+# 1. The state object for this particular reduction.
+# 2. The data type, i.e. if can or sample
+# 3. The reduction mode, ie HAB, LAB, All or Merged.
+# 3. Handle to the output workspace of the reduction.
+OutputBundle = namedtuple('OutputBundle', 'state, data_type, reduction_mode, output_workspace')
+
+# The OutputPartsBundle tuple contains information for partial output workspaces from a SANSReductionCore operation.
+# 1. The state object for this particular reduction.
+# 2. The data type, i.e. if can or sample
+# 3. The reduction mode, ie HAB, LAB, All or Merged.
+# 4. Handle to the partial output workspace which contains the counts.
+# 5. Handle to the partial output workspace which contains the normalization.
+OutputPartsBundle = namedtuple('OutputPartsBundle', 'state, data_type, reduction_mode, '
+                                                    'output_workspace_count, output_workspace_norm')
diff --git a/scripts/SANS/sans/algorithm_detail/load_data.py b/scripts/SANS/sans/algorithm_detail/load_data.py
index 25e85a20dbaaf0fa910c6987ea608c5a0f0594d5..62f53e110c29973f50142bcdde2d42429b4b38e9 100644
--- a/scripts/SANS/sans/algorithm_detail/load_data.py
+++ b/scripts/SANS/sans/algorithm_detail/load_data.py
@@ -685,7 +685,6 @@ def load_isis(data_type, file_information, period, use_cached, calibration_file_
 # ----------------------------------------------------------------------------------------------------------------------
 class SANSLoadData(with_metaclass(ABCMeta, object)):
     """ Base class for all SANSLoad implementations."""
-
     @abstractmethod
     def do_execute(self, data_info, use_cached, publish_to_ads, progress, parent_alg):
         pass
diff --git a/scripts/SANS/sans/algorithm_detail/q_resolution_calculator.py b/scripts/SANS/sans/algorithm_detail/q_resolution_calculator.py
new file mode 100644
index 0000000000000000000000000000000000000000..744826d82922a32cb313283fc1c893a67afc3fff
--- /dev/null
+++ b/scripts/SANS/sans/algorithm_detail/q_resolution_calculator.py
@@ -0,0 +1,159 @@
+from __future__ import (absolute_import, division, print_function)
+from abc import (ABCMeta, abstractmethod)
+from six import with_metaclass
+from math import sqrt
+from sans.common.constants import EMPTY_NAME
+from sans.common.enums import (SANSInstrument)
+from sans.common.general_functions import create_unmanaged_algorithm
+
+
+# ----------------------------------------------------------------------------------
+# Free Functions
+# ----------------------------------------------------------------------------------
+def load_sigma_moderator_workspace(file_name):
+    """
+    Gets the sigma moderator workspace.
+    @param file_name: the file name of the sigma moderator
+    @returns the sigma moderator workspace
+    """
+    load_name = "LoadRKH"
+    load_option = {"Filename": file_name,
+                   "OutputWorkspace": EMPTY_NAME,
+                   "FirstColumnValue": "Wavelength"}
+    load_alg = create_unmanaged_algorithm(load_name, **load_option)
+    load_alg.execute()
+    moderator_workspace = load_alg.getProperty("OutputWorkspace").value
+
+    convert_name = "ConvertToHistogram"
+    convert_options = {"InputWorkspace": moderator_workspace,
+                       "OutputWorkspace": EMPTY_NAME}
+    convert_alg = create_unmanaged_algorithm(convert_name, **convert_options)
+    convert_alg.execute()
+    return convert_alg.getProperty("OutputWorkspace").value
+
+
+def get_aperture_diameters(convert_to_q):
+    """
+    Gets the aperture diameters for the sample and the source
+    If all fields are specified for a rectangular aperture then this is used, else a circular aperture is
+    used.
+    @param convert_to_q: a SANSStateConvertToQ object.
+    @return: aperture diameter for the source, aperture diameter for the sample
+    """
+    def set_up_diameter(height, width):
+        """
+        Prepare the diameter parameter. If there are corresponding H and W values, then
+        use them instead. Richard provided the formula: A = 2*sqrt((H^2 + W^2)/6)
+        """
+        return 2*sqrt((height*height+width*width)/6)
+    h1 = convert_to_q.q_resolution_h1
+    h2 = convert_to_q.q_resolution_h2
+    w1 = convert_to_q.q_resolution_w1
+    w2 = convert_to_q.q_resolution_w2
+
+    if all(element is not None for element in [h1, h2, w1, w2]):
+        a1 = set_up_diameter(h1, w1)
+        a2 = set_up_diameter(h2, w2)
+    else:
+        a1 = convert_to_q.q_resolution_a1
+        a2 = convert_to_q.q_resolution_a2
+
+    return a1, a2
+
+
+def create_q_resolution_workspace(convert_to_q, data_workspace):
+    """
+    Provides a q resolution workspace
+    @param convert_to_q: a SANSStateConvertToQ object.
+    @param data_workspace: the workspace which is to be reduced.
+    @return: a q resolution workspace
+    """
+    # Load the sigma moderator
+    file_name = convert_to_q.moderator_file
+    sigma_moderator = load_sigma_moderator_workspace(file_name)
+
+    # Get the aperture diameters
+    a1, a2 = get_aperture_diameters(convert_to_q)
+
+    # We need the radius, not the diameter in the TOFSANSResolutionByPixel algorithm
+    sample_radius = 0.5 * a2
+    source_radius = 0.5 * a1
+
+    # The radii and the deltaR are expected to be in mm
+    sample_radius *= 1000.
+    source_radius *= 1000.
+    delta_r = convert_to_q.q_resolution_delta_r
+    delta_r *= 1000.
+
+    collimation_length = convert_to_q.q_resolution_collimation_length
+    use_gravity = convert_to_q.use_gravity
+    gravity_extra_length = convert_to_q.gravity_extra_length
+
+    resolution_name = "TOFSANSResolutionByPixel"
+    resolution_options = {"InputWorkspace": data_workspace,
+                          "OutputWorkspace": EMPTY_NAME,
+                          "DeltaR": delta_r,
+                          "SampleApertureRadius": sample_radius,
+                          "SourceApertureRadius": source_radius,
+                          "SigmaModerator": sigma_moderator,
+                          "CollimationLength": collimation_length,
+                          "AccountForGravity": use_gravity,
+                          "ExtraLength": gravity_extra_length}
+    resolution_alg = create_unmanaged_algorithm(resolution_name, **resolution_options)
+    resolution_alg.execute()
+    return resolution_alg.getProperty("OutputWorkspace").value
+
+
+# ----------------------------------------------------------------------------------
+# QResolution Classes
+# ----------------------------------------------------------------------------------
+class QResolutionCalculator(with_metaclass(ABCMeta, object)):
+    def __init__(self):
+        super(QResolutionCalculator, self).__init__()
+
+    @abstractmethod
+    def get_q_resolution_workspace(self, convert_to_q_info, data_workspace):
+        """
+        Calculates the q resolution workspace which is required for the Q1D algorithm
+        @param convert_to_q_info: a SANSStateConvertToQ object
+        @param data_workspace: the workspace which is being reduced.
+        @return: a q resolution workspace or None
+        """
+        pass
+
+
+class NullQResolutionCalculator(QResolutionCalculator):
+    def __init__(self):
+        super(QResolutionCalculator, self).__init__()
+
+    def get_q_resolution_workspace(self, convert_to_q_info, data_workspace):
+        return None
+
+
+class QResolutionCalculatorISIS(QResolutionCalculator):
+    def __init__(self):
+        super(QResolutionCalculatorISIS, self).__init__()
+
+    def get_q_resolution_workspace(self, convert_to_q_info, data_workspace):
+        return create_q_resolution_workspace(convert_to_q_info, data_workspace)
+
+
+class QResolutionCalculatorFactory(object):
+    def __init__(self):
+        super(QResolutionCalculatorFactory, self).__init__()
+
+    @staticmethod
+    def create_q_resolution_calculator(state):
+        data = state.data
+        instrument = data.instrument
+        is_isis_instrument = instrument is SANSInstrument.LARMOR or instrument is SANSInstrument.SANS2D or\
+                             instrument is SANSInstrument.LOQ  # noqa
+        if is_isis_instrument:
+            convert_to_q = state.convert_to_q
+            if convert_to_q.use_q_resolution:
+                q_resolution = QResolutionCalculatorISIS()
+            else:
+                q_resolution = NullQResolutionCalculator()
+        else:
+            raise RuntimeError("QResolutionCalculatorFactory: Other instruments are not implemented yet.")
+        return q_resolution
diff --git a/scripts/SANS/sans/algorithm_detail/save_workspace.py b/scripts/SANS/sans/algorithm_detail/save_workspace.py
new file mode 100644
index 0000000000000000000000000000000000000000..121df937ea1356df9c9a4d60ca5ad208e1930477
--- /dev/null
+++ b/scripts/SANS/sans/algorithm_detail/save_workspace.py
@@ -0,0 +1,104 @@
+from __future__ import (absolute_import, division, print_function)
+from collections import namedtuple
+from mantid.api import MatrixWorkspace
+from mantid.dataobjects import EventWorkspace
+from sans.common.general_functions import create_unmanaged_algorithm
+from sans.common.constants import EMPTY_NAME
+from sans.common.enums import SaveType
+
+ZERO_ERROR_DEFAULT = 1e6
+
+file_format_with_append = namedtuple('file_format_with_append', 'file_format, append_file_format_name')
+
+
+def save_to_file(workspace, file_format, file_name):
+    """
+    Save a workspace to a file.
+
+    :param workspace: the workspace to save.
+    :param file_format: the selected file format type.
+    :param file_name: the file name.
+    :return:
+    """
+    save_options = {"InputWorkspace": workspace}
+    save_alg = get_save_strategy(file_format, file_name, save_options)
+    save_alg.setRethrows(True)
+    save_alg.execute()
+
+
+def get_save_strategy(file_format_bundle, file_name, save_options):
+    """
+    Provide a save strategy based on the selected file format
+
+    :param file_format_bundle: the selected file_format_bundle
+    :param file_name: the name of the file
+    :param save_options: the save options such as file name and input workspace
+    :return: a handle to a save algorithm
+    """
+    file_format = file_format_bundle.file_format
+    if file_format is SaveType.Nexus:
+        file_name = get_file_name(file_format_bundle, file_name, "", ".nxs")
+        save_name = "SaveNexusProcessed"
+    elif file_format is SaveType.CanSAS:
+        file_name = get_file_name(file_format_bundle, file_name, "", ".xml")
+        save_name = "SaveCanSAS1D"
+    elif file_format is SaveType.NXcanSAS:
+        file_name = get_file_name(file_format_bundle, file_name, "_nxcansas", ".nxs")
+        save_name = "SaveNXcanSAS"
+    elif file_format is SaveType.NistQxy:
+        file_name = get_file_name(file_format_bundle, file_name, "_nistqxy", ".dat")
+        save_name = "SaveNISTDAT"
+    elif file_format is SaveType.RKH:
+        file_name = get_file_name(file_format_bundle, file_name, "", ".txt")
+        save_name = "SaveRKH"
+    elif file_format is SaveType.CSV:
+        file_name = get_file_name(file_format_bundle, file_name, "", ".csv")
+        save_name = "SaveCSV"
+    else:
+        raise RuntimeError("SaveWorkspace: The requested data {0} format is "
+                           "currently not supported.".format(file_format))
+    save_options.update({"Filename": file_name})
+    return create_unmanaged_algorithm(save_name, **save_options)
+
+
+def get_file_name(file_format, file_name, post_fix, extension):
+    if file_format.append_file_format_name:
+        file_name += post_fix
+    file_name += extension
+    return file_name
+
+
+def get_zero_error_free_workspace(workspace):
+    """
+    Creates a cloned workspace where all zero-error values have been replaced with a large value
+
+    :param workspace: The input workspace
+    :return: The zero-error free workspace
+    """
+    clone_name = "CloneWorkspace"
+    clone_options = {"InputWorkspace": workspace,
+                     "OutputWorkspace": EMPTY_NAME}
+    clone_alg = create_unmanaged_algorithm(clone_name, **clone_options)
+    clone_alg.execute()
+    cloned_workspace = clone_alg.getProperty("OutputWorkspace").value
+    remove_zero_errors_from_workspace(cloned_workspace)
+    return cloned_workspace
+
+
+def remove_zero_errors_from_workspace(workspace):
+    """
+    Removes the zero errors from a matrix workspace
+
+    :param workspace: The workspace which will have its zero error values removed.
+    :return: A zero-error free workspace
+    """
+    # Make sure we are dealing with a MatrixWorkspace
+    if not isinstance(workspace, MatrixWorkspace) or isinstance(workspace,EventWorkspace):
+        raise ValueError('Cannot remove zero errors from a workspace which is not a MatrixWorkspace.')
+
+    # Iterate over the workspace and replace the zero values with a large default value
+    number_of_spectra = workspace.getNumberHistograms()
+    errors = workspace.dataE
+    for index in range(0, number_of_spectra):
+        spectrum = errors(index)
+        spectrum[spectrum <= 0.0] = ZERO_ERROR_DEFAULT
diff --git a/scripts/SANS/sans/algorithm_detail/scale_helpers.py b/scripts/SANS/sans/algorithm_detail/scale_helpers.py
new file mode 100644
index 0000000000000000000000000000000000000000..9b9a03f6e871596d4858273c095ea0583b0640bd
--- /dev/null
+++ b/scripts/SANS/sans/algorithm_detail/scale_helpers.py
@@ -0,0 +1,161 @@
+from __future__ import (absolute_import, division, print_function)
+import math
+from abc import (ABCMeta, abstractmethod)
+from six import (with_metaclass)
+from sans.common.enums import (SANSInstrument, SampleShape)
+from sans.common.general_functions import create_unmanaged_algorithm
+from sans.common.constants import EMPTY_NAME
+
+
+class DivideByVolume(with_metaclass(ABCMeta, object)):
+    def __init__(self):
+        super(DivideByVolume, self).__init__()
+
+    @abstractmethod
+    def divide_by_volume(self, workspace, scale_info):
+        pass
+
+
+class NullDivideByVolume(DivideByVolume):
+    def __init__(self):
+        super(NullDivideByVolume, self).__init__()
+
+    def divide_by_volume(self, workspace, scale_info):
+        _ = scale_info  # noqa
+        return workspace
+
+
+class DivideByVolumeISIS(DivideByVolume):
+
+    def __init__(self):
+        super(DivideByVolumeISIS, self).__init__()
+
+    def divide_by_volume(self, workspace, scale_info):
+        volume = self._get_volume(scale_info)
+
+        single_valued_name = "CreateSingleValuedWorkspace"
+        single_valued_options = {"OutputWorkspace": EMPTY_NAME,
+                                 "DataValue": volume}
+        single_valued_alg = create_unmanaged_algorithm(single_valued_name, **single_valued_options)
+        single_valued_alg.execute()
+        single_valued_workspace = single_valued_alg.getProperty("OutputWorkspace").value
+
+        divide_name = "Divide"
+        divide_options = {"LHSWorkspace": workspace,
+                          "RHSWorkspace": single_valued_workspace}
+        divide_alg = create_unmanaged_algorithm(divide_name, **divide_options)
+        divide_alg.setPropertyValue("OutputWorkspace", EMPTY_NAME)
+        divide_alg.setProperty("OutputWorkspace", workspace)
+        divide_alg.execute()
+        return divide_alg.getProperty("OutputWorkspace").value
+
+    def _get_volume(self, scale_info):
+        thickness = scale_info.thickness if scale_info.thickness is not None else scale_info.thickness_from_file
+        width = scale_info.width if scale_info.width is not None else scale_info.width_from_file
+        height = scale_info.height if scale_info.height is not None else scale_info.height_from_file
+        shape = scale_info.shape if scale_info.shape is not None else scale_info.shape_from_file
+
+        # Now we calculate the volume
+        if shape is SampleShape.CylinderAxisUp:
+            # Volume = circle area * height
+            # Factor of four comes from radius = width/2
+            volume = height * math.pi
+            volume *= math.pow(width, 2) / 4.0
+        elif shape is SampleShape.Cuboid:
+            # Flat plate sample
+            volume = width * height * thickness
+        elif shape is SampleShape.CylinderAxisAlong:
+            # Factor of four comes from radius = width/2
+            # Disc - where height is not used
+            volume = thickness * math.pi
+            volume *= math.pow(width, 2) / 4.0
+        else:
+            raise NotImplementedError('DivideByVolumeISIS: The shape {0} is not in the list of '
+                                      'supported shapes'.format(shape))
+        return volume
+
+
+class DivideByVolumeFactory(object):
+    def __init__(self):
+        super(DivideByVolumeFactory, self).__init__()
+
+    @staticmethod
+    def create_divide_by_volume(state):
+        data = state.data
+        instrument = data.instrument
+
+        is_isis_instrument = instrument is SANSInstrument.LARMOR or instrument is SANSInstrument.SANS2D or instrument is SANSInstrument.LOQ  # noqa
+        if is_isis_instrument:
+            divider = DivideByVolumeISIS()
+        else:
+            raise RuntimeError("DivideVolumeFactory: Other instruments are not implemented yet.")
+        return divider
+
+
+class MultiplyByAbsoluteScale(with_metaclass(ABCMeta, object)):
+    DEFAULT_SCALING = 100.0
+
+    def __init__(self):
+        super(MultiplyByAbsoluteScale, self).__init__()
+
+    @staticmethod
+    def do_scale(workspace, scale_factor):
+        single_valued_name = "CreateSingleValuedWorkspace"
+        single_valued_options = {"OutputWorkspace": EMPTY_NAME,
+                                 "DataValue": scale_factor}
+        single_valued_alg = create_unmanaged_algorithm(single_valued_name, **single_valued_options)
+        single_valued_alg.execute()
+        single_valued_workspace = single_valued_alg.getProperty("OutputWorkspace").value
+
+        multiply_name = "Multiply"
+        multiply_options = {"LHSWorkspace": workspace,
+                            "RHSWorkspace": single_valued_workspace}
+        multiply_alg = create_unmanaged_algorithm(multiply_name, **multiply_options)
+        multiply_alg.setPropertyValue("OutputWorkspace", EMPTY_NAME)
+        multiply_alg.setProperty("OutputWorkspace", workspace)
+        multiply_alg.execute()
+        return multiply_alg.getProperty("OutputWorkspace").value
+
+    @abstractmethod
+    def multiply_by_absolute_scale(self, workspace, scale_info):
+        pass
+
+
+class MultiplyByAbsoluteScaleLOQ(MultiplyByAbsoluteScale):
+    def __init__(self):
+        super(MultiplyByAbsoluteScaleLOQ, self).__init__()
+
+    def multiply_by_absolute_scale(self, workspace, scale_info):
+        scale_factor = scale_info.scale*self.DEFAULT_SCALING if scale_info.scale is not None else self.DEFAULT_SCALING
+        rescale_to_colette = math.pi
+        scale_factor /= rescale_to_colette
+        return self.do_scale(workspace, scale_factor)
+
+
+class MultiplyByAbsoluteScaleISIS(MultiplyByAbsoluteScale):
+    def __init__(self):
+        super(MultiplyByAbsoluteScaleISIS, self).__init__()
+
+    def multiply_by_absolute_scale(self, workspace, scale_info):
+        scale_factor = scale_info.scale*self.DEFAULT_SCALING if scale_info.scale is not None else self.DEFAULT_SCALING
+        return self.do_scale(workspace, scale_factor)
+
+
+class MultiplyByAbsoluteScaleFactory(object):
+    def __init__(self):
+        super(MultiplyByAbsoluteScaleFactory, self).__init__()
+
+    @staticmethod
+    def create_multiply_by_absolute(state):
+        data = state.data
+        instrument = data.instrument
+
+        is_isis_instrument = instrument is SANSInstrument.LARMOR or instrument is SANSInstrument.SANS2D or \
+                             SANSInstrument.LOQ  # noqa
+        if instrument is SANSInstrument.LOQ:
+            multiplier = MultiplyByAbsoluteScaleLOQ()
+        elif is_isis_instrument:
+            multiplier = MultiplyByAbsoluteScaleISIS()
+        else:
+            raise NotImplementedError("MultiplyByAbsoluteScaleFactory: Other instruments are not implemented yet.")
+        return multiplier
diff --git a/scripts/SANS/sans/common/constants.py b/scripts/SANS/sans/common/constants.py
index 42b16d47abfb181e769938a8d43f15e6bf71506f..b58a773eaabc9f10432cf006978c257abe2428e9 100644
--- a/scripts/SANS/sans/common/constants.py
+++ b/scripts/SANS/sans/common/constants.py
@@ -35,6 +35,9 @@ SANS2D = "SANS2D"
 LARMOR = "LARMOR"
 LOQ = "LOQ"
 
+REDUCED_WORKSPACE_BASE_NAME_IN_LOGS = "reduced_workspace_base_name"
+REDUCED_WORKSPACE_NAME_BY_USER_IN_LOGS = "reduced_workspace_name_by_user"
+
 SANS_FILE_TAG = "sans_file_tag"
 REDUCED_CAN_TAG = "reduced_can_hash"
 
diff --git a/scripts/SANS/sans/common/file_information.py b/scripts/SANS/sans/common/file_information.py
index 5cb05069a56708b93b7f5a27014357d54ad581d5..10c22c97eb5edf2961d4fc34b78ea7cecee74289 100644
--- a/scripts/SANS/sans/common/file_information.py
+++ b/scripts/SANS/sans/common/file_information.py
@@ -396,6 +396,7 @@ def get_geometry_information_isis_nexus(file_name):
             shape = None
     return height, width, thickness, shape
 
+
 # ----------------------------------------------------------------------------------------------------------------------
 # Functions for Added data
 # ----------------------------------------------------------------------------------------------------------------------
@@ -413,13 +414,13 @@ def get_date_and_run_number_added_nexus(file_name):
     with h5.File(file_name) as h5_file:
         keys = list(h5_file.keys())
         first_entry = h5_file[keys[0]]
-        logs = first_entry[LOGS]
+        logs = first_entry["logs"]
         # Start time
-        start_time = logs[START_TIME]
-        start_time_value = DateAndTime(start_time[VALUE][0])
+        start_time = logs["start_time"]
+        start_time_value = DateAndTime(start_time["value"][0])
         # Run number
-        run_number = logs[RUN_NUMBER]
-        run_number_value = int(run_number[VALUE][0])
+        run_number = logs["run_number"]
+        run_number_value = int(run_number["value"][0])
     return start_time_value, run_number_value
 
 
@@ -430,6 +431,9 @@ def get_added_nexus_information(file_name):  # noqa
     :param file_name: the full file path.
     :return: if the file was a Nexus file and the number of periods.
     """
+    ADDED_SUFFIX = "-add_added_event_data"
+    ADDED_MONITOR_SUFFIX = "-add_monitors_added_event_data"
+
     def get_all_keys_for_top_level(key_collection):
         top_level_key_collection = []
         for key in key_collection:
@@ -438,10 +442,10 @@ def get_added_nexus_information(file_name):  # noqa
         return sorted(top_level_key_collection)
 
     def check_if_event_mode(entry):
-        return EVENT_WORKSPACE in list(entry.keys())
+        return "event_workspace" in list(entry.keys())
 
     def get_workspace_name(entry):
-        return entry[WORKSPACE_NAME][0].decode("utf-8")
+        return entry["workspace_name"][0].decode("utf-8")
 
     def has_same_number_of_entries(workspace_names, monitor_workspace_names):
         return len(workspace_names) == len(monitor_workspace_names)
@@ -480,7 +484,7 @@ def get_added_nexus_information(file_name):  # noqa
         #    random_name-add_monitors_added_event_data_4.s
         if (has_same_number_of_entries(workspace_names, monitor_workspace_names) and
             has_added_tag(workspace_names, monitor_workspace_names) and
-                entries_match(workspace_names, monitor_workspace_names)):
+            entries_match(workspace_names, monitor_workspace_names)):  # noqa
             is_added_file_event = True
             num_periods = len(workspace_names)
         else:
@@ -537,12 +541,13 @@ def get_added_nexus_information(file_name):  # noqa
 
 
 def get_date_for_added_workspace(file_name):
-    value = get_top_level_nexus_entry(file_name, START_TIME)
+    value = get_top_level_nexus_entry(file_name, "start_time")
     return DateAndTime(value)
 
 
 def has_added_suffix(file_name):
-    return file_name.upper().endswith(ADD_FILE_SUFFIX)
+    suffix = "-ADD.NXS"
+    return file_name.upper().endswith(suffix)
 
 
 def is_added_histogram(file_name):
diff --git a/scripts/SANS/sans/common/general_functions.py b/scripts/SANS/sans/common/general_functions.py
index 17dc7963286264078bff60108d3f50ea0acf6eed..34f1640f43b83e9684162e32d5a452be227e9af0 100644
--- a/scripts/SANS/sans/common/general_functions.py
+++ b/scripts/SANS/sans/common/general_functions.py
@@ -8,8 +8,8 @@ import re
 from copy import deepcopy
 import json
 from mantid.api import (AlgorithmManager, AnalysisDataService, isSameWorkspaceObject)
-from sans.common.constants import (SANS_FILE_TAG, ALL_PERIODS,
-                                   SANS2D, LOQ, LARMOR, EMPTY_NAME, REDUCED_CAN_TAG)
+from sans.common.constants import (SANS_FILE_TAG, ALL_PERIODS, SANS2D, LOQ, LARMOR, EMPTY_NAME,
+                                   REDUCED_CAN_TAG)
 from sans.common.log_tagger import (get_tag, has_tag, set_tag, has_hash, get_hash_value, set_hash)
 from sans.common.enums import (DetectorType, RangeStepType, ReductionDimensionality, OutputParts, ISISReductionMode)
 
@@ -95,6 +95,7 @@ def create_unmanaged_algorithm(name, **kwargs):
     alg = AlgorithmManager.createUnmanaged(name)
     alg.initialize()
     alg.setChild(True)
+    alg.setRethrows(True)
     for key, value in list(kwargs.items()):
         alg.setProperty(key, value)
     return alg
@@ -111,6 +112,7 @@ def create_managed_non_child_algorithm(name, **kwargs):
     alg = AlgorithmManager.create(name)
     alg.initialize()
     alg.setChild(False)
+    alg.setRethrows(True)
     for key, value in list(kwargs.items()):
         alg.setProperty(key, value)
     return alg
@@ -127,6 +129,7 @@ def create_child_algorithm(parent_alg, name, **kwargs):
     """
     if parent_alg:
         alg = parent_alg.createChildAlgorithm(name)
+        alg.setRethrows(True)
         for key, value in list(kwargs.items()):
             alg.setProperty(key, value)
     else:
@@ -150,7 +153,7 @@ def get_input_workspace_as_copy_if_not_same_as_output_workspace(alg):
         clone_alg.execute()
         return clone_alg.getProperty("OutputWorkspace").value
 
-    if "InputWorkspace" not in alg or "OutputWorkspace" not in alg:  #  noqa
+    if "InputWorkspace" not in alg or "OutputWorkspace" not in alg:
         raise RuntimeError("The algorithm {} does not seem to have an InputWorkspace and"
                            " an OutputWorkspace property.".format(alg.name()))
 
diff --git a/scripts/SANS/sans/user_file/user_file_parser.py b/scripts/SANS/sans/user_file/user_file_parser.py
index e197bd1313e06d2854691d37bf5892b6d70a3c5f..59a0111273c8c52c77dc0180f6f4e8b9b6dcc945 100644
--- a/scripts/SANS/sans/user_file/user_file_parser.py
+++ b/scripts/SANS/sans/user_file/user_file_parser.py
@@ -612,7 +612,8 @@ class LimitParser(UserFileComponentParser):
             simple_pattern = self._extract_simple_pattern(event_binning, LimitsId.events_binning)
             rebin_values = simple_pattern[LimitsId.events_binning]
             prefix = -1. if rebin_values.step_type is RangeStepType.Log else 1.
-            binning_string = str(rebin_values.start) + "," + str(prefix*rebin_values.step) + "," + str(rebin_values.stop)  # noqa
+            binning_string = str(rebin_values.start) + "," + str(prefix*rebin_values.step) + "," + \
+                             str(rebin_values.stop)  # noqa
         else:
             rebin_values = extract_float_list(event_binning)
             binning_string = ",".join([str(val) for val in rebin_values])
diff --git a/scripts/SANS/sans/user_file/user_file_state_director.py b/scripts/SANS/sans/user_file/user_file_state_director.py
index 233e93b12d8acbcbc1953dd14fbcbb08a097abd2..e20c22dd61bbe6f501afa82710073f383f98c194 100644
--- a/scripts/SANS/sans/user_file/user_file_state_director.py
+++ b/scripts/SANS/sans/user_file/user_file_state_director.py
@@ -6,9 +6,12 @@ from sans.common.file_information import find_full_file_path
 from sans.common.general_functions import (get_ranges_for_rebin_setting, get_ranges_for_rebin_array,
                                            get_ranges_from_event_slice_setting)
 from sans.user_file.user_file_reader import UserFileReader
-from sans.user_file.user_file_common import (DetectorId, BackId, LimitsId, rebin_string_values, simple_range,
-                                             complex_range, MaskId, SampleId, SetId, TransId, TubeCalibrationFileId,
-                                             QResolutionId, FitId, MonId, GravityId, OtherId)
+from sans.user_file.user_file_common import (DetectorId, BackId, LimitsId,
+                                             simple_range, complex_range, MaskId,
+                                             rebin_string_values, SampleId, SetId,
+                                             TransId, TubeCalibrationFileId, QResolutionId, FitId,
+                                             MonId, GravityId,
+                                             OtherId)
 
 from sans.state.automatic_setters import set_up_setter_forwarding_from_director_to_builder
 from sans.state.state import get_state_builder
diff --git a/scripts/test/SANS/CMakeLists.txt b/scripts/test/SANS/CMakeLists.txt
index 6dbb49b1214150fd07969a9f3f71eb4f50059f0d..5d5c749db332d426e2c53f150ca9d531c813b7e2 100644
--- a/scripts/test/SANS/CMakeLists.txt
+++ b/scripts/test/SANS/CMakeLists.txt
@@ -1,3 +1,4 @@
+add_subdirectory(algorithm_detail)
 add_subdirectory(common)
 add_subdirectory(state)
 add_subdirectory(user_file)
diff --git a/scripts/test/SANS/algorithm_detail/CMakeLists.txt b/scripts/test/SANS/algorithm_detail/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..15aee543d62c8c80f48736ed1b941b869ae5f646
--- /dev/null
+++ b/scripts/test/SANS/algorithm_detail/CMakeLists.txt
@@ -0,0 +1,13 @@
+##
+## Tests for SANS
+##
+
+set ( TEST_PY_FILES
+  scale_helper_test.py
+  q_resolution_calculator_test.py
+)
+
+check_tests_valid ( ${CMAKE_CURRENT_SOURCE_DIR} ${TEST_PY_FILES} )
+
+# Prefix for test name=PythonSANS
+pyunittest_add_test ( ${CMAKE_CURRENT_SOURCE_DIR} PythonSANS ${TEST_PY_FILES} )
diff --git a/scripts/test/SANS/algorithm_detail/q_resolution_calculator_test.py b/scripts/test/SANS/algorithm_detail/q_resolution_calculator_test.py
new file mode 100644
index 0000000000000000000000000000000000000000..51fa462f2057eafcb5e56fb520a45b7115d144c9
--- /dev/null
+++ b/scripts/test/SANS/algorithm_detail/q_resolution_calculator_test.py
@@ -0,0 +1,179 @@
+from __future__ import (absolute_import, division, print_function)
+import unittest
+import mantid
+import os
+
+from mantid.kernel import config
+from mantid.api import AnalysisDataService
+from mantid.simpleapi import (CreateSampleWorkspace, DeleteWorkspace)
+from math import sqrt
+
+from sans.algorithm_detail.q_resolution_calculator import (QResolutionCalculatorFactory, NullQResolutionCalculator,
+                                                           QResolutionCalculatorISIS, get_aperture_diameters,
+                                                           load_sigma_moderator_workspace, create_q_resolution_workspace)
+from sans.common.enums import SANSInstrument
+
+
+class MockContainer:
+    convert_to_q = None
+    data = None
+
+
+class QResolutionCalculatorTest(unittest.TestCase):
+    moderator_file_name = "test_moderator_file.txt"
+    data_workspace = None
+    data_workspace_name = "q_resolution_test_workspace"
+
+    def _assert_collection_elements_are_equal(self, collection1, collection2):
+        tolerance = 1e-7
+        self.assertTrue(len(collection1) == len(collection2))
+        for index in range(len(collection1)):
+            self.assertTrue(abs(collection1[index] - collection2[index]) < tolerance)
+
+    @staticmethod
+    def _get_path(file_name):
+        save_directory = config['defaultsave.directory']
+        if not os.path.isdir(save_directory):
+            save_directory = os.getcwd()
+        return os.path.join(save_directory, file_name)
+
+    @staticmethod
+    def _save_file(file_path, content):
+        with open(file_path, "w") as f:
+            f.write(content)
+
+    @classmethod
+    def setUpClass(cls):
+        # Create a moderator file
+        moderator_content = ("  Thu 30-JUL-2015 15:04 Workspace: TS1_H2_resultStddev_hist \n"
+                             "\n"
+                             "   4    0    0    0    1   4    0\n"
+                             "         0         0         0         0\n"
+                             " 3 (F12.5,2E16.6)\n"
+                             "     1.0      1.000000e+00    0.000000e+00\n"
+                             "     4.0      2.000000e+00    0.000000e+00\n"
+                             "     7.0      3.000000e+00    0.000000e+00\n"
+                             "     10.0     4.000000e+00    0.000000e+00\n")
+        cls.moderator_file_name = cls._get_path(cls.moderator_file_name)
+        cls._save_file(cls.moderator_file_name, moderator_content)
+
+        # Create a data workspace with 4 histograms and 4 bins with centres at 2, 4, 6, 8 Angstrom.
+        if cls.data_workspace is None:
+            CreateSampleWorkspace(OutputWorkspace=cls.data_workspace_name, NumBanks=1, BankPixelWidth=2,
+                                  XUnit='Wavelength', XMin=1, XMax=10, BinWidth=2)
+            cls.data_workspace = AnalysisDataService.retrieve(cls.data_workspace_name)
+
+    @classmethod
+    def tearDownClass(cls):
+        if cls.data_workspace:
+            DeleteWorkspace(cls.data_workspace)
+        if os.path.exists(cls.moderator_file_name):
+            os.remove(cls.moderator_file_name)
+
+    @staticmethod
+    def _provide_mock_state(use_q_resolution, **kwargs):
+        mock_state = MockContainer()
+        mock_state.data = MockContainer()
+        mock_state.data.instrument = SANSInstrument.LARMOR
+        convert_to_q = MockContainer()
+        convert_to_q.use_q_resolution = use_q_resolution
+        for key, value in kwargs.items():
+            setattr(convert_to_q, key, value)
+        mock_state.convert_to_q = convert_to_q
+        return mock_state
+
+    def test_that_raises_when_unknown_instrument_is_chosen(self):
+        # Arrange
+        mock_state = MockContainer()
+        mock_state.data = MockContainer()
+        mock_state.data.instrument = None
+        factory = QResolutionCalculatorFactory()
+
+        # Act + Assert
+        args = [mock_state]
+        self.assertRaises(RuntimeError, factory.create_q_resolution_calculator, *args)
+
+    def test_that_provides_null_q_resolution_calculator_when_is_turned_off(self):
+        # Arrange
+        mock_state = QResolutionCalculatorTest._provide_mock_state(use_q_resolution=False)
+        factory = QResolutionCalculatorFactory()
+        # Act
+        calculator = factory.create_q_resolution_calculator(mock_state)
+        # Assert
+        self.assertTrue(isinstance(calculator, NullQResolutionCalculator))
+
+    def test_that_provides_isis_q_resolution_calculator_when_is_turned_on(self):
+        # Arrange
+        mock_state = QResolutionCalculatorTest._provide_mock_state(use_q_resolution=True)
+        factory = QResolutionCalculatorFactory()
+        # Act
+        calculator = factory.create_q_resolution_calculator(mock_state)
+        # Assert
+        self.assertTrue(isinstance(calculator, QResolutionCalculatorISIS))
+
+    def test_that_calculates_the_aperture_diameters_for_circular_settings(self):
+        # Arrange
+        q_options = {"q_resolution_a1": 2., "q_resolution_a2": 3., "q_resolution_h1": None, "q_resolution_h2": None,
+                     "q_resolution_w1": None, "q_resolution_w2": None}
+        mock_state = QResolutionCalculatorTest._provide_mock_state(use_q_resolution=True, **q_options)
+        # Act
+        a1, a2 = get_aperture_diameters(mock_state.convert_to_q)
+        # Assert
+        self.assertTrue(a1 == 2.)
+        self.assertTrue(a2 == 3.)
+
+    def test_that_calculates_the_aperture_diameters_for_rectangular_settings(self):
+        # Arrange
+        q_options = {"q_resolution_a1": 2., "q_resolution_a2": 3., "q_resolution_h1": 4., "q_resolution_h2": 6.,
+                     "q_resolution_w1": 7., "q_resolution_w2": 8.}
+        mock_state = QResolutionCalculatorTest._provide_mock_state(use_q_resolution=True, **q_options)
+        # Act
+        a1, a2 = get_aperture_diameters(mock_state.convert_to_q)
+        # Assert
+        expected_a1 = 2*sqrt((16. + 49.) / 6)
+        expected_a2 = 2 * sqrt((36. + 64.) / 6)
+        self.assertTrue(a1 == expected_a1)
+        self.assertTrue(a2 == expected_a2)
+
+    def test_that_defaults_to_circular_if_not_all_rectangular_are_specified(self):
+        q_options = {"q_resolution_a1": 2., "q_resolution_a2": 3., "q_resolution_h1": 4., "q_resolution_h2": None,
+                     "q_resolution_w1": 7., "q_resolution_w2": 8.}
+        mock_state = QResolutionCalculatorTest._provide_mock_state(use_q_resolution=True, **q_options)
+        # Act
+        a1, a2 = get_aperture_diameters(mock_state.convert_to_q)
+        # Assert
+        self.assertTrue(a1 == 2.)
+        self.assertTrue(a2 == 3.)
+
+    def test_that_moderator_workspace_is_histogram(self):
+        # Arrange
+        file_name = self._get_path(QResolutionCalculatorTest.moderator_file_name)
+        # Act
+        workspace = load_sigma_moderator_workspace(file_name)
+        # Assert
+        self.assertTrue(len(workspace.dataX(0)) == len(workspace.dataY(0)) + 1)
+
+    def test_that_executes_the_q_resolution_calculation_without_issues(self):
+        # Arrange
+        q_options = {"q_resolution_a1": 2., "q_resolution_a2": 3., "q_resolution_h1": None, "q_resolution_h2": None,
+                     "q_resolution_w1": None, "q_resolution_w2": None}
+        mock_state = QResolutionCalculatorTest._provide_mock_state(use_q_resolution=True, **q_options)
+        file_name = self._get_path(QResolutionCalculatorTest.moderator_file_name)
+        mock_state.convert_to_q.moderator_file = file_name
+        mock_state.convert_to_q.q_resolution_collimation_length = 3.
+        mock_state.convert_to_q.q_resolution_delta_r = 0.002
+        mock_state.convert_to_q.use_gravity = False
+        mock_state.convert_to_q.gravity_extra_length = 0.
+
+        # Act
+        q_resolution_workspace = create_q_resolution_workspace(mock_state.convert_to_q,
+                                                               QResolutionCalculatorTest.data_workspace)
+        # Assert
+        self.assertTrue(q_resolution_workspace is not None)
+        self.assertTrue(len(q_resolution_workspace.dataX(0)) == len(QResolutionCalculatorTest.data_workspace.dataX(0)))
+        self.assertTrue(len(q_resolution_workspace.dataY(0)) == len(QResolutionCalculatorTest.data_workspace.dataY(0)))
+        for e1, e2 in zip(q_resolution_workspace.dataX(0), QResolutionCalculatorTest.data_workspace.dataX(0)):
+            self.assertTrue(e1 == e2)
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/scripts/test/SANS/algorithm_detail/scale_helper_test.py b/scripts/test/SANS/algorithm_detail/scale_helper_test.py
new file mode 100644
index 0000000000000000000000000000000000000000..b69ffc1cc7165aa436e2c5f59dc04ced72b42a40
--- /dev/null
+++ b/scripts/test/SANS/algorithm_detail/scale_helper_test.py
@@ -0,0 +1,174 @@
+from __future__ import (absolute_import, division, print_function)
+import unittest
+import mantid
+import math
+from sans.test_helper.test_director import TestDirector
+from sans.algorithm_detail.scale_helpers import (DivideByVolumeFactory, DivideByVolumeISIS, NullDivideByVolume,
+                                                 MultiplyByAbsoluteScaleFactory, MultiplyByAbsoluteScaleLOQ,
+                                                 MultiplyByAbsoluteScaleISIS)
+from sans.common.general_functions import create_unmanaged_algorithm
+from sans.common.enums import (SampleShape, SANSFacility, DataType)
+from sans.state.scale import get_scale_builder
+from sans.state.data import get_data_builder
+
+
+class ScaleHelperTest(unittest.TestCase):
+    @staticmethod
+    def _get_workspace(width=1.0, height=1.0, thickness=1.0, shape=1):
+        sample_name = "CreateSampleWorkspace"
+        sample_options = {"WorkspaceType": "Histogram",
+                          "NumBanks": 1,
+                          "BankPixelWidth": 1,
+                          "OutputWorkspace": "test"}
+        sample_alg = create_unmanaged_algorithm(sample_name, **sample_options)
+        sample_alg.execute()
+        workspace = sample_alg.getProperty("OutputWorkspace").value
+
+        sample = workspace.sample()
+        sample.setGeometryFlag(shape)
+        sample.setThickness(thickness)
+        sample.setWidth(width)
+        sample.setHeight(height)
+        return workspace
+
+    def test_that_divide_strategy_is_selected_for_isis_instrument_and_is_not_can(self):
+        # Arrange
+        test_director = TestDirector()
+        state_isis = test_director.construct()
+        divide_factory = DivideByVolumeFactory()
+        # Act
+        divider = divide_factory.create_divide_by_volume(state_isis)
+        # Arrange
+        self.assertTrue(isinstance(divider, DivideByVolumeISIS))
+
+    def test_that_divide_uses_settings_from_workspace(self):
+        # Arrange
+        facility = SANSFacility.ISIS
+        data_builder = get_data_builder(facility)
+        data_builder.set_sample_scatter("SANS2D00022024")
+        data_state = data_builder.build()
+
+        scale_builder = get_scale_builder(data_state)
+        scale_state = scale_builder.build()
+
+        test_director = TestDirector()
+        test_director.set_states(scale_state=scale_state, data_state=data_state)
+        state = test_director.construct()
+
+        divide_factory = DivideByVolumeFactory()
+        divider = divide_factory.create_divide_by_volume(state)
+
+        # Apply the settings from the SANS2D00022024 workspace
+        width = 8.
+        height = 8.
+        thickness = 1.
+        shape = 3
+
+        workspace = ScaleHelperTest._get_workspace(width, height, thickness, shape)
+
+        # Act
+        output_workspace = divider.divide_by_volume(workspace, scale_state)
+
+        # Assert
+        expected_volume = thickness * math.pi * math.pow(width, 2) / 4.0
+        expected_value = 0.3 / expected_volume
+        data_y = output_workspace.dataY(0)
+        self.assertEqual(data_y[0], expected_value)
+
+    def test_that_divide_uses_settings_from_state_if_they_are_set(self):
+        # Arrange
+        facility = SANSFacility.ISIS
+        data_builder = get_data_builder(facility)
+        data_builder.set_sample_scatter("SANS2D00022024")
+        data_state = data_builder.build()
+
+        scale_builder = get_scale_builder(data_state)
+        width = 10.
+        height = 5.
+        thickness = 2.
+        scale_builder.set_shape(SampleShape.CylinderAxisAlong)
+        scale_builder.set_thickness(thickness)
+        scale_builder.set_width(width)
+        scale_builder.set_height(height)
+        scale_state = scale_builder.build()
+
+        test_director = TestDirector()
+        test_director.set_states(scale_state=scale_state, data_state=data_state)
+        state = test_director.construct()
+
+        divide_factory = DivideByVolumeFactory()
+        divider = divide_factory.create_divide_by_volume(state)
+
+        workspace = ScaleHelperTest._get_workspace()
+
+        # Act
+        output_workspace = divider.divide_by_volume(workspace, scale_state)
+
+        # Assert
+        expected_volume = thickness * math.pi * math.pow(width, 2) / 4.0
+        expected_value = 0.3 / expected_volume
+        data_y = output_workspace.dataY(0)
+        self.assertEqual(data_y[0], expected_value)
+
+    def test_that_correct_scale_strategy_is_selected_for_non_loq_isis_instrument(self):
+        # Arrange
+        test_director = TestDirector()
+        state_isis = test_director.construct()
+        absolute_multiply_factory = MultiplyByAbsoluteScaleFactory()
+        # Act
+        multiplier = absolute_multiply_factory.create_multiply_by_absolute(state_isis)
+        # Arrange
+        self.assertTrue(isinstance(multiplier, MultiplyByAbsoluteScaleISIS))
+
+    def test_that_correct_scale_strategy_is_selected_for_loq(self):
+        # Arrange
+        facility = SANSFacility.ISIS
+        data_builder = get_data_builder(facility)
+        data_builder.set_sample_scatter("LOQ74044")
+        data_state = data_builder.build()
+
+        scale_builder = get_scale_builder(data_state)
+        scale_state = scale_builder.build()
+
+        test_director = TestDirector()
+        test_director.set_states(scale_state=scale_state, data_state=data_state)
+        state_loq = test_director.construct()
+
+        absolute_multiply_factory = MultiplyByAbsoluteScaleFactory()
+        # Act
+        multiplier = absolute_multiply_factory.create_multiply_by_absolute(state_loq)
+
+        # Assert
+        self.assertTrue(isinstance(multiplier, MultiplyByAbsoluteScaleLOQ))
+
+    def test_that_correct_scale_strategy_is_selected_for_loq_2(self):
+        # Arrange
+        facility = SANSFacility.ISIS
+        data_builder = get_data_builder(facility)
+        data_builder.set_sample_scatter("LOQ74044")
+        data_state = data_builder.build()
+
+        scale_builder = get_scale_builder(data_state)
+        scale_builder.set_scale(2.4)
+        scale_state = scale_builder.build()
+
+        test_director = TestDirector()
+        test_director.set_states(scale_state=scale_state, data_state=data_state)
+        state_loq = test_director.construct()
+
+        absolute_multiply_factory = MultiplyByAbsoluteScaleFactory()
+        multiplier = absolute_multiply_factory.create_multiply_by_absolute(state_loq)
+
+        workspace = self._get_workspace()
+
+        # Act
+        output_workspace = multiplier.multiply_by_absolute_scale(workspace, state_loq.scale)
+
+        # Assert
+        expected_value = 0.3 * 2.4 / math.pi * 100.
+        data_y = output_workspace.dataY(0)
+        self.assertEqual(data_y[0], expected_value)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/scripts/test/SANS/user_file/user_file_state_director_test.py b/scripts/test/SANS/user_file/user_file_state_director_test.py
index f80a5a68eb65988ab67117ebd1a78656618951e4..008681f76cc02a348c240a3480703fbf483c07f4 100644
--- a/scripts/test/SANS/user_file/user_file_state_director_test.py
+++ b/scripts/test/SANS/user_file/user_file_state_director_test.py
@@ -219,4 +219,4 @@ if __name__ == "__main__":
     unittest.main()
 
 if __name__ == "__main__":
-    unittest.main()
+    unittest.main()
\ No newline at end of file