diff --git a/CMakeLists.txt b/CMakeLists.txt
index fff9d083b402a36793d1fe11939326b7bbc94ad7..cfd352c88f44017558cecbad027539cb667a7d15 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -203,12 +203,13 @@ if ( MSVC )
   add_definitions ( -DQWT_DLL )
 endif ()
 
+add_custom_target ( AllTests )
+add_dependencies( AllTests FrameworkTests )
 if ( ENABLE_MANTIDPLOT OR ENABLE_WORKBENCH )
   add_custom_target ( GUITests )
   add_dependencies ( check GUITests )
   # Collect all tests together
-  add_custom_target ( AllTests )
-  add_dependencies ( AllTests FrameworkTests GUITests )
+  add_dependencies ( AllTests GUITests )
   add_subdirectory ( qt )
 endif()
 
diff --git a/Framework/API/CMakeLists.txt b/Framework/API/CMakeLists.txt
index 5cf25c750d09a7725241e29a9765dc93c935e112..76bd564e909e9d26b760c5171ec659e8162c9e35 100644
--- a/Framework/API/CMakeLists.txt
+++ b/Framework/API/CMakeLists.txt
@@ -9,6 +9,7 @@ set ( SRC_FILES
 	src/AlgorithmProperty.cpp
 	src/AlgorithmProxy.cpp
 	src/AnalysisDataService.cpp
+	src/AnalysisDataServiceObserver.cpp
 	src/ArchiveSearchFactory.cpp
 	src/Axis.cpp
 	src/BinEdgeAxis.cpp
@@ -175,6 +176,7 @@ set ( INC_FILES
 	inc/MantidAPI/AlgorithmProperty.h
 	inc/MantidAPI/AlgorithmProxy.h
 	inc/MantidAPI/AnalysisDataService.h
+	inc/MantidAPI/AnalysisDataServiceObserver.h
 	inc/MantidAPI/ArchiveSearchFactory.h
 	inc/MantidAPI/Axis.h
 	inc/MantidAPI/BinEdgeAxis.h
@@ -374,6 +376,7 @@ set ( TEST_FILES
 	AlgorithmProxyTest.h
 	AlgorithmTest.h
 	AnalysisDataServiceTest.h
+	AnalysisDataServiceObserverTest.h
 	AsynchronousTest.h
 	BinEdgeAxisTest.h
 	BoxControllerTest.h
diff --git a/Framework/API/inc/MantidAPI/AnalysisDataServiceObserver.h b/Framework/API/inc/MantidAPI/AnalysisDataServiceObserver.h
new file mode 100644
index 0000000000000000000000000000000000000000..f07a2aadb9a70d25dc84ffe3eedd5c1c4b400339
--- /dev/null
+++ b/Framework/API/inc/MantidAPI/AnalysisDataServiceObserver.h
@@ -0,0 +1,140 @@
+// Mantid Repository : https://github.com/mantidproject/mantid
+//
+// Copyright © 2007 ISIS Rutherford Appleton Laboratory UKRI,
+//     NScD Oak Ridge National Laboratory, European Spallation Source
+//     & Institut Laue - Langevin
+// SPDX - License - Identifier: GPL - 3.0 +
+#ifndef MANTID_KERNEL_ANALYSISDATASERVICEOBSERVER_H_
+#define MANTID_KERNEL_ANALYSISDATASERVICEOBSERVER_H_
+
+#include "MantidAPI/AnalysisDataService.h"
+#include "MantidAPI/DllConfig.h"
+#include "MantidKernel/DataService.h"
+#include <Poco/NObserver.h>
+
+using namespace Mantid::Kernel;
+using namespace Mantid::API;
+
+namespace Mantid {
+namespace API {
+
+/*
+ * To use the AnalysisDataServiceObserver you will need to do a few things:
+ *
+ * 1. Inherit from this class in the class you wish to take effect on
+ *
+ * 2. Make sure that the effect you are attempting to observe has been added
+ * to the AnalysisDataService itself by using the public method in this
+ * class, e.g. observeAll, observeAdd, observeReplace etc.
+ *
+ * 3. The last thing to actually have something take effect is by overriding
+ * the relevant handle function e.g. when observing all override
+ * anyChangeHandle and anything done in that overriden method will happen
+ * every time something changes in the AnalysisDataService.
+ *
+ * This works in both C++ and Python, some functionality is limited in
+ * python, but the handlers will all be called.
+ */
+
+class MANTID_API_DLL AnalysisDataServiceObserver {
+public:
+  AnalysisDataServiceObserver();
+  virtual ~AnalysisDataServiceObserver();
+
+  void observeAll(bool turnOn = true);
+  void observeAdd(bool turnOn = true);
+  void observeReplace(bool turnOn = true);
+  void observeDelete(bool turnOn = true);
+  void observeClear(bool turnOn = true);
+  void observeRename(bool turnOn = true);
+  void observeGroup(bool turnOn = true);
+  void observeUnGroup(bool turnOn = true);
+  void observeGroupUpdate(bool turnOn = true);
+
+  virtual void anyChangeHandle();
+  virtual void addHandle(const std::string &wsName, const Workspace_sptr &ws);
+  virtual void replaceHandle(const std::string &wsName,
+                             const Workspace_sptr &ws);
+  virtual void deleteHandle(const std::string &wsName,
+                            const Workspace_sptr &ws);
+  virtual void clearHandle();
+  virtual void renameHandle(const std::string &wsName,
+                            const std::string &newName);
+  virtual void groupHandle(const std::string &wsName, const Workspace_sptr &ws);
+  virtual void unGroupHandle(const std::string &wsName,
+                             const Workspace_sptr &ws);
+  virtual void groupUpdateHandle(const std::string &wsName,
+                                 const Workspace_sptr &ws);
+
+private:
+  bool m_observingAdd{false}, m_observingReplace{false},
+      m_observingDelete{false}, m_observingClear{false},
+      m_observingRename{false}, m_observingGroup{false},
+      m_observingUnGroup{false}, m_observingGroupUpdate{false};
+
+  void _addHandle(
+      const Poco::AutoPtr<AnalysisDataServiceImpl::AddNotification> &pNf);
+  void _replaceHandle(
+      const Poco::AutoPtr<AnalysisDataServiceImpl::AfterReplaceNotification>
+          &pNf);
+  void _deleteHandle(
+      const Poco::AutoPtr<AnalysisDataServiceImpl::PreDeleteNotification> &pNf);
+  void _clearHandle(
+      const Poco::AutoPtr<AnalysisDataServiceImpl::ClearNotification> &pNf);
+  void _renameHandle(
+      const Poco::AutoPtr<AnalysisDataServiceImpl::RenameNotification> &pNf);
+  void _groupHandle(
+      const Poco::AutoPtr<AnalysisDataServiceImpl::GroupWorkspacesNotification>
+          &pNf);
+  void _unGroupHandle(
+      const Poco::AutoPtr<
+          AnalysisDataServiceImpl::UnGroupingWorkspaceNotification> &pNf);
+  void _groupUpdateHandle(
+      const Poco::AutoPtr<AnalysisDataServiceImpl::GroupUpdatedNotification>
+          &pNf);
+
+  /// Poco::NObserver for AddNotification.
+  Poco::NObserver<AnalysisDataServiceObserver,
+                  AnalysisDataServiceImpl::AddNotification>
+      m_addObserver;
+
+  /// Poco::NObserver for ReplaceNotification.
+  Poco::NObserver<AnalysisDataServiceObserver,
+                  AnalysisDataServiceImpl::AfterReplaceNotification>
+      m_replaceObserver;
+
+  /// Poco::NObserver for DeleteNotification.
+  Poco::NObserver<AnalysisDataServiceObserver,
+                  AnalysisDataServiceImpl::PreDeleteNotification>
+      m_deleteObserver;
+
+  /// Poco::NObserver for ClearNotification
+  Poco::NObserver<AnalysisDataServiceObserver,
+                  AnalysisDataServiceImpl::ClearNotification>
+      m_clearObserver;
+
+  /// Poco::NObserver for RenameNotification
+  Poco::NObserver<AnalysisDataServiceObserver,
+                  AnalysisDataServiceImpl::RenameNotification>
+      m_renameObserver;
+
+  /// Poco::NObserver for GroupNotification
+  Poco::NObserver<AnalysisDataServiceObserver,
+                  AnalysisDataServiceImpl::GroupWorkspacesNotification>
+      m_groupObserver;
+
+  /// Poco::NObserver for UnGroupNotification
+  Poco::NObserver<AnalysisDataServiceObserver,
+                  AnalysisDataServiceImpl::UnGroupingWorkspaceNotification>
+      m_unGroupObserver;
+
+  /// Poco::NObserver for GroupUpdateNotification
+  Poco::NObserver<AnalysisDataServiceObserver,
+                  AnalysisDataServiceImpl::GroupUpdatedNotification>
+      m_groupUpdatedObserver;
+};
+
+} // namespace API
+} // namespace Mantid
+
+#endif /*MANTID_KERNEL_ANALYSISDATASERVICEOBSERVER_H_*/
\ No newline at end of file
diff --git a/Framework/API/src/Algorithm.cpp b/Framework/API/src/Algorithm.cpp
index bca85de4090054db07337f38aac8286666a32455..4878e93e52e6e49b706cfe8fbf7906fc0995b7e8 100644
--- a/Framework/API/src/Algorithm.cpp
+++ b/Framework/API/src/Algorithm.cpp
@@ -569,6 +569,7 @@ bool Algorithm::execute() {
   // Invoke exec() method of derived class and catch all uncaught exceptions
   try {
     try {
+      setExecuted(false);
       if (!isChild()) {
         m_running = true;
       }
diff --git a/Framework/API/src/AnalysisDataServiceObserver.cpp b/Framework/API/src/AnalysisDataServiceObserver.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..7dffb883ac6c6c49fec0467d9040b8a255300443
--- /dev/null
+++ b/Framework/API/src/AnalysisDataServiceObserver.cpp
@@ -0,0 +1,353 @@
+// Mantid Repository : https://github.com/mantidproject/mantid
+//
+// Copyright &copy; 2007 ISIS Rutherford Appleton Laboratory UKRI,
+//     NScD Oak Ridge National Laboratory, European Spallation Source
+//     & Institut Laue - Langevin
+// SPDX - License - Identifier: GPL - 3.0 +
+
+#include "MantidAPI/AnalysisDataServiceObserver.h"
+
+AnalysisDataServiceObserver::AnalysisDataServiceObserver()
+    : m_addObserver(*this, &AnalysisDataServiceObserver::_addHandle),
+      m_replaceObserver(*this, &AnalysisDataServiceObserver::_replaceHandle),
+      m_deleteObserver(*this, &AnalysisDataServiceObserver::_deleteHandle),
+      m_clearObserver(*this, &AnalysisDataServiceObserver::_clearHandle),
+      m_renameObserver(*this, &AnalysisDataServiceObserver::_renameHandle),
+      m_groupObserver(*this, &AnalysisDataServiceObserver::_groupHandle),
+      m_unGroupObserver(*this, &AnalysisDataServiceObserver::_unGroupHandle),
+      m_groupUpdatedObserver(*this,
+                             &AnalysisDataServiceObserver::_groupUpdateHandle) {
+}
+
+AnalysisDataServiceObserver::~AnalysisDataServiceObserver() {
+  // Turn off/remove all observers
+  this->observeAll(false);
+}
+
+// ------------------------------------------------------------
+// Observe Methods
+// ------------------------------------------------------------
+
+/**
+ * @brief Function will turn on/off all observers for the ADS
+ *
+ * @param turnOn bool; if this is True then if not already present the observer
+ * will be added else removed if it's false.
+ */
+void AnalysisDataServiceObserver::observeAll(bool turnOn) {
+  this->observeAdd(turnOn);
+  this->observeReplace(turnOn);
+  this->observeDelete(turnOn);
+  this->observeClear(turnOn);
+  this->observeRename(turnOn);
+  this->observeGroup(turnOn);
+  this->observeUnGroup(turnOn);
+  this->observeGroupUpdate(turnOn);
+}
+
+/**
+ * @brief Function will add/remove the observer to the ADS for if a workspace is
+ * added to it.
+ *
+ * @param turnOn bool; if this is True then if not already present the observer
+ * will be added else removed if it's false.
+ */
+void AnalysisDataServiceObserver::observeAdd(bool turnOn) {
+  if (turnOn && !m_observingAdd) {
+    AnalysisDataService::Instance().notificationCenter.addObserver(
+        m_addObserver);
+  } else if (!turnOn && m_observingAdd) {
+    AnalysisDataService::Instance().notificationCenter.removeObserver(
+        m_addObserver);
+  }
+  m_observingAdd = turnOn;
+}
+
+/**
+ * @brief Function will add/remove the observer to the ADS for if a workspace is
+ * replaced
+ *
+ * @param turnOn bool; if this is True then if not already present the observer
+ * will be added else removed if it's false.
+ */
+void AnalysisDataServiceObserver::observeReplace(bool turnOn) {
+  if (turnOn && !m_observingReplace) {
+    AnalysisDataService::Instance().notificationCenter.addObserver(
+        m_replaceObserver);
+  } else if (!turnOn && m_observingReplace) {
+    AnalysisDataService::Instance().notificationCenter.removeObserver(
+        m_replaceObserver);
+  }
+  m_observingReplace = turnOn;
+}
+
+/**
+ * @brief Function will add/remove the observer to the ADS for if a workspace is
+ * deleted.
+ *
+ * @param turnOn bool; if this is True then if not already present the observer
+ * will be added else removed if it's false.
+ */
+void AnalysisDataServiceObserver::observeDelete(bool turnOn) {
+  if (turnOn && !m_observingDelete) {
+    AnalysisDataService::Instance().notificationCenter.addObserver(
+        m_deleteObserver);
+  } else if (!turnOn && m_observingDelete) {
+    AnalysisDataService::Instance().notificationCenter.removeObserver(
+        m_deleteObserver);
+  }
+  m_observingDelete = turnOn;
+}
+
+/**
+ * @brief Function will add/remove the observer to the ADS for if the ADS is
+ * cleared.
+ *
+ * @param turnOn bool; if this is True then if not already present the observer
+ * will be added else removed if it's false.
+ */
+void AnalysisDataServiceObserver::observeClear(bool turnOn) {
+  if (turnOn && !m_observingClear) {
+    AnalysisDataService::Instance().notificationCenter.addObserver(
+        m_clearObserver);
+  } else if (!turnOn && m_observingClear) {
+    AnalysisDataService::Instance().notificationCenter.removeObserver(
+        m_clearObserver);
+  }
+  m_observingClear = turnOn;
+}
+
+/**
+ * @brief Function will add/remove the observer to the ADS for if a workspace is
+ * renamed
+ *
+ * @param turnOn bool; if this is True then if not already present the observer
+ * will be added else removed if it's false.
+ */
+void AnalysisDataServiceObserver::observeRename(bool turnOn) {
+  if (turnOn && !m_observingRename) {
+    AnalysisDataService::Instance().notificationCenter.addObserver(
+        m_renameObserver);
+  } else if (!turnOn && m_observingRename) {
+    AnalysisDataService::Instance().notificationCenter.removeObserver(
+        m_renameObserver);
+  }
+  m_observingRename = turnOn;
+}
+
+/**
+ * @brief Function will add/remove the observer to the ADS for if a group is
+ * added/created in the ADS
+ *
+ * @param turnOn bool; if this is True then if not already present the observer
+ * will be added else removed if it's false.
+ */
+void AnalysisDataServiceObserver::observeGroup(bool turnOn) {
+  if (turnOn && !m_observingGroup) {
+    AnalysisDataService::Instance().notificationCenter.addObserver(
+        m_groupObserver);
+  } else if (!turnOn && m_observingGroup) {
+    AnalysisDataService::Instance().notificationCenter.removeObserver(
+        m_groupObserver);
+  }
+  m_observingGroup = turnOn;
+}
+
+/**
+ * @brief Function will add/remove the observer to the ADS for if a group is
+ * removed/delete from the ADS
+ *
+ * @param turnOn bool; if this is True then if not already present the observer
+ * will be added else removed if it's false.
+ */
+void AnalysisDataServiceObserver::observeUnGroup(bool turnOn) {
+  if (turnOn && !m_observingUnGroup) {
+    AnalysisDataService::Instance().notificationCenter.addObserver(
+        m_unGroupObserver);
+  } else if (!turnOn && m_observingUnGroup) {
+    AnalysisDataService::Instance().notificationCenter.removeObserver(
+        m_unGroupObserver);
+  }
+  m_observingUnGroup = turnOn;
+}
+
+/**
+ * @brief Function will add/remove the observer to the ADS for if a workspace is
+ * added to a group or removed.
+ *
+ * @param turnOn bool; if this is True then if not already present the observer
+ * will be added else removed if it's false.
+ */
+void AnalysisDataServiceObserver::observeGroupUpdate(bool turnOn) {
+  if (turnOn && !m_observingGroupUpdate) {
+    AnalysisDataService::Instance().notificationCenter.addObserver(
+        m_groupUpdatedObserver);
+  } else if (!turnOn && m_observingGroupUpdate) {
+    AnalysisDataService::Instance().notificationCenter.removeObserver(
+        m_groupUpdatedObserver);
+  }
+  m_observingGroupUpdate = turnOn;
+}
+
+// ------------------------------------------------------------
+// Virtual Methods
+// ------------------------------------------------------------
+/**
+ * @brief If anyChange to the ADS occurs then this function will trigger, works
+ * by overloading this class and overriding this function.
+ */
+void AnalysisDataServiceObserver::anyChangeHandle() {}
+
+/**
+ * @brief If a workspace is added to the ADS, then this function will trigger,
+ * works by overloading this class and overriding this function.
+ *
+ * @param wsName std::string; the name of the workspace added
+ * @param ws Workspace_sptr; the Workspace that is added
+ */
+void AnalysisDataServiceObserver::addHandle(
+    const std::string &wsName, const Mantid::API::Workspace_sptr &ws) {
+  UNUSED_ARG(wsName)
+  UNUSED_ARG(ws)
+}
+
+/**
+ * @brief If a workspace is replaced in the ADS, then this function will
+ * trigger, works by overloading this class and overriding this function
+ *
+ * @param wsName std::string; the name of the workspace replacing
+ * @param ws Workspace_sptr; the Workspace that is replacing
+ */
+void AnalysisDataServiceObserver::replaceHandle(
+    const std::string &wsName, const Mantid::API::Workspace_sptr &ws) {
+  UNUSED_ARG(wsName)
+  UNUSED_ARG(ws)
+}
+
+/**
+ * @brief If a workspace is deleted from the ADS, then this function will
+ * trigger, works by overloading this class and overriding this function
+ *
+ * @param wsName std::string; the name of the workspace
+ * @param ws Workspace_sptr; the Workspace that is deleted
+ */
+void AnalysisDataServiceObserver::deleteHandle(
+    const std::string &wsName, const Mantid::API::Workspace_sptr &ws) {
+  UNUSED_ARG(wsName)
+  UNUSED_ARG(ws)
+}
+
+/**
+ * @brief If the ADS is cleared, then this function will trigger, works by
+ * overloading this class and overriding this function
+ */
+void AnalysisDataServiceObserver::clearHandle() {}
+
+/**
+ * @brief If a workspace is renamed in the ADS, then this function will trigger,
+ * works by overloading this class and overriding this function
+ *
+ * @param wsName std::string; the name of the workspace
+ * @param newName std::string; the new name of the workspace
+ */
+void AnalysisDataServiceObserver::renameHandle(const std::string &wsName,
+                                               const std::string &newName) {
+  UNUSED_ARG(wsName)
+  UNUSED_ARG(newName)
+}
+
+/**
+ * @brief If a group is created/added to the ADS, then this function will
+ * trigger, works by overloading this class and overriding this function
+ *
+ * @param wsName std::string; the name of the workspace
+ * @param ws Workspace_sptr; the WorkspaceGroup that was added/created
+ */
+void AnalysisDataServiceObserver::groupHandle(const std::string &wsName,
+                                              const Workspace_sptr &ws) {
+  UNUSED_ARG(wsName)
+  UNUSED_ARG(ws)
+}
+
+/**
+ * @brief If a group is removed from the ADS, then this function will trigger,
+ * works by overloading this class and overriding this function
+ *
+ * @param wsName std::string; the name of the workspace
+ * @param ws Workspace_sptr; the WorkspaceGroup that was ungrouped
+ */
+void AnalysisDataServiceObserver::unGroupHandle(const std::string &wsName,
+                                                const Workspace_sptr &ws) {
+  UNUSED_ARG(wsName)
+  UNUSED_ARG(ws)
+}
+
+/**
+ * @brief If a group has a workspace added/removed in the ADS, then this
+ * function will trigger, works by overloading this class and overriding this
+ * function.
+ *
+ * @param wsName std::string; the name of the workspace
+ * @param ws Workspace_sptr; the WorkspaceGroup that was updated
+ */
+void AnalysisDataServiceObserver::groupUpdateHandle(const std::string &wsName,
+                                                    const Workspace_sptr &ws) {
+  UNUSED_ARG(wsName)
+  UNUSED_ARG(ws)
+}
+
+// ------------------------------------------------------------
+// Private Methods
+// ------------------------------------------------------------
+void AnalysisDataServiceObserver::_addHandle(
+    const Poco::AutoPtr<AnalysisDataServiceImpl::AddNotification> &pNf) {
+  this->anyChangeHandle();
+  this->addHandle(pNf->objectName(), pNf->object());
+}
+
+void AnalysisDataServiceObserver::_replaceHandle(
+    const Poco::AutoPtr<AnalysisDataServiceImpl::AfterReplaceNotification>
+        &pNf) {
+  this->anyChangeHandle();
+  this->replaceHandle(pNf->objectName(), pNf->object());
+}
+
+void AnalysisDataServiceObserver::_deleteHandle(
+    const Poco::AutoPtr<AnalysisDataServiceImpl::PreDeleteNotification> &pNf) {
+  this->anyChangeHandle();
+  this->deleteHandle(pNf->objectName(), pNf->object());
+}
+
+void AnalysisDataServiceObserver::_clearHandle(
+    const Poco::AutoPtr<AnalysisDataServiceImpl::ClearNotification> &pNf) {
+  UNUSED_ARG(pNf)
+  this->anyChangeHandle();
+  this->clearHandle();
+}
+
+void AnalysisDataServiceObserver::_renameHandle(
+    const Poco::AutoPtr<AnalysisDataServiceImpl::RenameNotification> &pNf) {
+  this->anyChangeHandle();
+  this->renameHandle(pNf->objectName(), pNf->newObjectName());
+}
+
+void AnalysisDataServiceObserver::_groupHandle(
+    const Poco::AutoPtr<AnalysisDataServiceImpl::GroupWorkspacesNotification>
+        &pNf) {
+  this->anyChangeHandle();
+  this->groupHandle(pNf->objectName(), pNf->object());
+}
+
+void AnalysisDataServiceObserver::_unGroupHandle(
+    const Poco::AutoPtr<
+        AnalysisDataServiceImpl::UnGroupingWorkspaceNotification> &pNf) {
+  this->anyChangeHandle();
+  this->unGroupHandle(pNf->objectName(), pNf->object());
+}
+
+void AnalysisDataServiceObserver::_groupUpdateHandle(
+    const Poco::AutoPtr<AnalysisDataServiceImpl::GroupUpdatedNotification>
+        &pNf) {
+  this->anyChangeHandle();
+  this->groupUpdateHandle(pNf->objectName(), pNf->object());
+}
\ No newline at end of file
diff --git a/Framework/API/test/AnalysisDataServiceObserverTest.h b/Framework/API/test/AnalysisDataServiceObserverTest.h
new file mode 100644
index 0000000000000000000000000000000000000000..c16b3ff1e7577af64fac65fd900e39f248921b77
--- /dev/null
+++ b/Framework/API/test/AnalysisDataServiceObserverTest.h
@@ -0,0 +1,243 @@
+// Mantid Repository : https://github.com/mantidproject/mantid
+//
+// Copyright &copy; 2018 ISIS Rutherford Appleton Laboratory UKRI,
+//     NScD Oak Ridge National Laboratory, European Spallation Source
+//     & Institut Laue - Langevin
+// SPDX - License - Identifier: GPL - 3.0 +
+#ifndef ANALYSISDATASERVICEOBSERVERTEST_H_
+#define ANALYSISDATASERVICEOBSERVERTEST_H_
+
+#include <cxxtest/TestSuite.h>
+
+#include "MantidAPI/Algorithm.h"
+#include "MantidAPI/AlgorithmManager.h"
+#include "MantidAPI/AnalysisDataServiceObserver.h"
+#include "MantidAPI/FrameworkManager.h"
+#include "MantidAPI/MatrixWorkspace.h"
+
+using namespace Mantid::API;
+
+class FakeAnalysisDataServiceObserver
+    : public Mantid::API::AnalysisDataServiceObserver {
+
+public:
+  FakeAnalysisDataServiceObserver()
+      : m_anyChangeHandleCalled(false), m_addHandleCalled(false),
+        m_replaceHandleCalled(false), m_deleteHandleCalled(false),
+        m_clearHandleCalled(false), m_renameHandleCalled(false),
+        m_groupHandleCalled(false), m_unGroupHandleCalled(false),
+        m_groupUpdateHandleCalled(false) {
+    this->observeAll(false);
+  }
+
+  ~FakeAnalysisDataServiceObserver() { this->observeAll(false); }
+
+  void anyChangeHandle() override { m_anyChangeHandleCalled = true; }
+  void addHandle(const std::string &wsName, const Workspace_sptr &ws) override {
+    UNUSED_ARG(wsName)
+    UNUSED_ARG(ws)
+    m_addHandleCalled = true;
+  }
+  void replaceHandle(const std::string &wsName,
+                     const Workspace_sptr &ws) override {
+    UNUSED_ARG(wsName)
+    UNUSED_ARG(ws)
+    m_replaceHandleCalled = true;
+  }
+  void deleteHandle(const std::string &wsName,
+                    const Workspace_sptr &ws) override {
+    UNUSED_ARG(wsName)
+    UNUSED_ARG(ws)
+    m_deleteHandleCalled = true;
+  }
+  void clearHandle() override { m_clearHandleCalled = true; }
+  void renameHandle(const std::string &wsName,
+                    const std::string &newName) override {
+    UNUSED_ARG(wsName)
+    UNUSED_ARG(newName)
+    m_renameHandleCalled = true;
+  }
+  void groupHandle(const std::string &wsName,
+                   const Workspace_sptr &ws) override {
+    UNUSED_ARG(wsName)
+    UNUSED_ARG(ws)
+    m_groupHandleCalled = true;
+  }
+  void unGroupHandle(const std::string &wsName,
+                     const Workspace_sptr &ws) override {
+    UNUSED_ARG(wsName)
+    UNUSED_ARG(ws)
+    m_unGroupHandleCalled = true;
+  }
+  void groupUpdateHandle(const std::string &wsName,
+                         const Workspace_sptr &ws) override {
+    UNUSED_ARG(wsName)
+    UNUSED_ARG(ws)
+    m_groupUpdateHandleCalled = true;
+  }
+
+public:
+  bool m_anyChangeHandleCalled, m_addHandleCalled, m_replaceHandleCalled,
+      m_deleteHandleCalled, m_clearHandleCalled, m_renameHandleCalled,
+      m_groupHandleCalled, m_unGroupHandleCalled, m_groupUpdateHandleCalled;
+};
+
+class AnalysisDataServiceObserverTest : public CxxTest::TestSuite {
+private:
+  AnalysisDataServiceImpl &ads;
+  std::unique_ptr<FakeAnalysisDataServiceObserver> m_mockInheritingClass;
+
+public:
+  // This pair of boilerplate methods prevent the suite being created statically
+  // This means the constructor isn't called when running other tests
+  static AnalysisDataServiceObserverTest *createSuite() {
+    return new AnalysisDataServiceObserverTest();
+  }
+  static void destroySuite(AnalysisDataServiceObserverTest *suite) {
+    delete suite;
+  }
+
+  AnalysisDataServiceObserverTest()
+      : ads(AnalysisDataService::Instance()),
+        m_mockInheritingClass(
+            std::make_unique<FakeAnalysisDataServiceObserver>()) {
+    // Loads the framework manager
+    Mantid::API::FrameworkManager::Instance();
+  }
+
+  void setUp() override {
+    ads.clear();
+    m_mockInheritingClass = std::make_unique<FakeAnalysisDataServiceObserver>();
+  }
+
+  void addWorkspaceToADS(std::string name = "dummy") {
+    IAlgorithm_sptr alg =
+        Mantid::API::AlgorithmManager::Instance().createUnmanaged(
+            "CreateSampleWorkspace");
+    alg->setChild(true);
+    alg->initialize();
+    alg->setPropertyValue("OutputWorkspace", name);
+    alg->execute();
+    MatrixWorkspace_sptr ws = alg->getProperty("OutputWorkspace");
+    ads.addOrReplace(name, ws);
+  }
+
+  void test_anyChangeHandle_is_called_on_add() {
+    m_mockInheritingClass->observeAll();
+    addWorkspaceToADS("dummy");
+
+    TS_ASSERT(m_mockInheritingClass->m_anyChangeHandleCalled)
+  }
+
+  void test_addHandle_is_called_on_add() {
+    m_mockInheritingClass->observeAdd();
+    addWorkspaceToADS("dummy");
+
+    TS_ASSERT(m_mockInheritingClass->m_addHandleCalled)
+  }
+
+  void test_deleteHandle_is_called_on_delete() {
+    addWorkspaceToADS();
+
+    m_mockInheritingClass->observeDelete();
+    ads.remove("dummy");
+
+    TS_ASSERT(m_mockInheritingClass->m_deleteHandleCalled)
+  }
+
+  void test_replaceHandle_is_called_on_replace() {
+    addWorkspaceToADS("dummy");
+
+    m_mockInheritingClass->observeReplace();
+    addWorkspaceToADS("dummy");
+
+    TS_ASSERT(m_mockInheritingClass->m_replaceHandleCalled)
+  }
+
+  void test_clearHandle_is_called_on_clear() {
+    addWorkspaceToADS("dummy");
+
+    m_mockInheritingClass->observeClear();
+    ads.clear();
+
+    TS_ASSERT(m_mockInheritingClass->m_clearHandleCalled)
+  }
+
+  void test_renameHandle_is_called_on_rename() {
+    addWorkspaceToADS("dummy");
+
+    m_mockInheritingClass->observeRename();
+    IAlgorithm_sptr alg =
+        Mantid::API::AlgorithmManager::Instance().createUnmanaged(
+            "RenameWorkspace");
+    alg->initialize();
+    alg->setPropertyValue("InputWorkspace", "dummy");
+    alg->setPropertyValue("OutputWorkspace", "dummy2");
+    alg->execute();
+
+    TS_ASSERT(m_mockInheritingClass->m_renameHandleCalled)
+  }
+
+  void test_groupHandle_is_called_on_group_made() {
+    addWorkspaceToADS("dummy");
+    addWorkspaceToADS("dummy2");
+
+    m_mockInheritingClass->observeGroup();
+
+    IAlgorithm_sptr alg =
+        Mantid::API::AlgorithmManager::Instance().createUnmanaged(
+            "GroupWorkspaces");
+    alg->initialize();
+    alg->setPropertyValue("InputWorkspaces", "dummy,dummy2");
+    alg->setPropertyValue("OutputWorkspace", "newGroup");
+    alg->execute();
+
+    TS_ASSERT(m_mockInheritingClass->m_groupHandleCalled)
+  }
+
+  void test_unGroupHandle_is_called_on_un_grouping() {
+    addWorkspaceToADS("dummy");
+    addWorkspaceToADS("dummy2");
+
+    IAlgorithm_sptr alg =
+        Mantid::API::AlgorithmManager::Instance().createUnmanaged(
+            "GroupWorkspaces");
+    alg->initialize();
+    alg->setPropertyValue("InputWorkspaces", "dummy,dummy2");
+    alg->setPropertyValue("OutputWorkspace", "newGroup");
+    alg->execute();
+
+    m_mockInheritingClass->observeUnGroup();
+
+    IAlgorithm_sptr alg2 =
+        Mantid::API::AlgorithmManager::Instance().createUnmanaged(
+            "UnGroupWorkspace");
+    alg2->initialize();
+    alg2->setPropertyValue("InputWorkspace", "newGroup");
+    alg2->execute();
+
+    TS_ASSERT(m_mockInheritingClass->m_unGroupHandleCalled)
+  }
+
+  void test_groupUpdated_is_called_on_group_updated() {
+    addWorkspaceToADS("dummy");
+    addWorkspaceToADS("dummy2");
+    addWorkspaceToADS("dummy3");
+
+    IAlgorithm_sptr alg =
+        Mantid::API::AlgorithmManager::Instance().createUnmanaged(
+            "GroupWorkspaces");
+    alg->initialize();
+    alg->setPropertyValue("InputWorkspaces", "dummy,dummy2");
+    alg->setPropertyValue("OutputWorkspace", "newGroup");
+    alg->execute();
+
+    m_mockInheritingClass->observeGroupUpdate();
+
+    ads.addToGroup("newGroup", "dummy3");
+
+    TS_ASSERT(m_mockInheritingClass->m_groupUpdateHandleCalled)
+  }
+};
+
+#endif /* ANALYSISDATASERVICEOBSERVERTEST_H_ */
\ No newline at end of file
diff --git a/Framework/Algorithms/inc/MantidAlgorithms/TimeAtSampleStrategy.h b/Framework/Algorithms/inc/MantidAlgorithms/TimeAtSampleStrategy.h
index 8a7063175b1f847acaa8b21321db3512b7227f60..e315b2d241c4cf9eeff0b8535867fcfb07084560 100644
--- a/Framework/Algorithms/inc/MantidAlgorithms/TimeAtSampleStrategy.h
+++ b/Framework/Algorithms/inc/MantidAlgorithms/TimeAtSampleStrategy.h
@@ -13,7 +13,7 @@ namespace Mantid {
 namespace Algorithms {
 
 /**
- * @brief The Correction struct
+ * @brief The Correction struct to be applied as factor * TOF + offset
  * offset:: TOF offset in unit of TOF
  * factor:  TOF correction factor to multiply with
  */
diff --git a/Framework/Algorithms/src/AbsorptionCorrection.cpp b/Framework/Algorithms/src/AbsorptionCorrection.cpp
index f74a8aa03e12675626433206ca857c168a8c8cd1..7ab980db4e194b3611df72ea56565b3b61e036cf 100644
--- a/Framework/Algorithms/src/AbsorptionCorrection.cpp
+++ b/Framework/Algorithms/src/AbsorptionCorrection.cpp
@@ -138,6 +138,11 @@ void AbsorptionCorrection::exec() {
 
   // Calculate the cached values of L1 and element volumes.
   initialiseCachedDistances();
+  if (m_L1s.empty()) {
+    throw std::runtime_error(
+        "Failed to define any initial scattering gauge volume for geometry");
+  }
+
   // If sample not at origin, shift cached positions.
   const auto &spectrumInfo = m_inputWS->spectrumInfo();
   const V3D samplePos = spectrumInfo.samplePosition();
diff --git a/Framework/Algorithms/src/FilterEvents.cpp b/Framework/Algorithms/src/FilterEvents.cpp
index 08ddd95a774fcf0c3f92ab95d3bcb54c6d5dd29b..ac90660c91d569f68e47b33444cb655d0288965f 100644
--- a/Framework/Algorithms/src/FilterEvents.cpp
+++ b/Framework/Algorithms/src/FilterEvents.cpp
@@ -65,7 +65,7 @@ FilterEvents::FilterEvents()
       m_filterByPulseTime(false), m_informationWS(), m_hasInfoWS(),
       m_progress(0.), m_outputWSNameBase(), m_toGroupWS(false),
       m_vecSplitterTime(), m_vecSplitterGroup(), m_splitSampleLogs(false),
-      m_useDBSpectrum(false), m_dbWSIndex(-1), m_tofCorrType(),
+      m_useDBSpectrum(false), m_dbWSIndex(-1), m_tofCorrType(NoneCorrect),
       m_specSkipType(), m_vecSkip(), m_isSplittersRelativeTime(false),
       m_filterStartTime(0), m_runStartTime(0) {}
 
@@ -81,7 +81,9 @@ void FilterEvents::init() {
                   "An input SpilltersWorskpace for filtering");
 
   declareProperty("OutputWorkspaceBaseName", "OutputWorkspace",
-                  "The base name to use for the output workspace");
+                  "The base name to use for the output workspace. The output "
+                  "workspace names are a combination of this and the index in "
+                  "splitter.");
 
   declareProperty(
       Kernel::make_unique<WorkspaceProperty<TableWorkspace>>(
@@ -184,15 +186,22 @@ std::map<std::string, std::string> FilterEvents::validateInputs() {
   std::map<std::string, std::string> result;
 
   // check the splitters workspace for special behavior
-  API::Workspace_const_sptr wksp = this->getProperty(SPLITER_PROP_NAME);
+  API::Workspace_const_sptr splitter = this->getProperty(SPLITER_PROP_NAME);
   // SplittersWorkspace is a special type that needs no further checking
-  if (!bool(boost::dynamic_pointer_cast<const SplittersWorkspace>(wksp))) {
-    const auto table = boost::dynamic_pointer_cast<const TableWorkspace>(wksp);
+  if (bool(boost::dynamic_pointer_cast<const SplittersWorkspace>(splitter))) {
+    if (boost::dynamic_pointer_cast<const SplittersWorkspace>(splitter)
+            ->rowCount() == 0)
+      result[SPLITER_PROP_NAME] = "SplittersWorkspace must have rows defined";
+  } else {
+    const auto table =
+        boost::dynamic_pointer_cast<const TableWorkspace>(splitter);
     const auto matrix =
-        boost::dynamic_pointer_cast<const MatrixWorkspace>(wksp);
+        boost::dynamic_pointer_cast<const MatrixWorkspace>(splitter);
     if (bool(table)) {
       if (table->columnCount() != 3)
         result[SPLITER_PROP_NAME] = "TableWorkspace must have 3 columns";
+      else if (table->rowCount() == 0)
+        result[SPLITER_PROP_NAME] = "TableWorkspace must have rows defined";
     } else if (bool(matrix)) {
       if (matrix->getNumberHistograms() == 1) {
         if (!matrix->isHistogramData())
@@ -206,6 +215,30 @@ std::map<std::string, std::string> FilterEvents::validateInputs() {
     }
   }
 
+  const string correctiontype = getPropertyValue("CorrectionToSample");
+  if (correctiontype == "Direct") {
+    double ei = getProperty("IncidentEnergy");
+    if (isEmpty(ei)) {
+      EventWorkspace_const_sptr inputWS = this->getProperty("InputWorkspace");
+      if (!inputWS->run().hasProperty("Ei")) {
+        const string msg(
+            "InputWorkspace does not have Ei. Must specify IncidentEnergy");
+        result["CorrectionToSample"] = msg;
+        result["IncidentEnergy"] = msg;
+      }
+    }
+  } else if (correctiontype == "Customized") {
+    TableWorkspace_const_sptr correctionWS =
+        getProperty("DetectorTOFCorrectionWorkspace");
+    if (!correctionWS) {
+      const string msg(
+          "Must specify correction workspace with CorrectionToSample=Custom");
+      result["CorrectionToSample"] = msg;
+      result["DetectorTOFCorrectionWorkspace"] = msg;
+    }
+  }
+  // "None" and "Elastic" and "Indirect" don't require extra information
+
   return result;
 }
 
@@ -221,9 +254,9 @@ void FilterEvents::exec() {
   // Parse splitters
   m_progress = 0.0;
   progress(m_progress, "Processing SplittersWorkspace.");
-  if (m_useSplittersWorkspace)
+  if (m_useSplittersWorkspace) // SplittersWorkspace the class in nanoseconds
     processSplittersWorkspace();
-  else if (m_useArbTableSplitters)
+  else if (m_useArbTableSplitters) // TableWorkspace in seconds
     processTableSplittersWorkspace();
   else
     processMatrixSplitterWorkspace();
@@ -437,14 +470,6 @@ void FilterEvents::processAlgorithmProperties() {
     throw runtime_error("Impossible situation!");
   }
 
-  if (m_tofCorrType == CustomizedCorrect) {
-    // Customized correciton
-    m_detCorrectWorkspace = getProperty("DetectorTOFCorrectionWorkspace");
-    if (!m_detCorrectWorkspace)
-      throw runtime_error("In case of customized TOF correction, correction "
-                          "workspace must be given!");
-  }
-
   // Spectrum skip
   string skipappr = getPropertyValue("SpectrumWithoutDetector");
   if (skipappr == "Skip")
@@ -951,6 +976,14 @@ void FilterEvents::processMatrixSplitterWorkspace() {
   return;
 }
 
+namespace {
+// offset_ns - an offset from the GPS epoch
+int64_t timeInSecondsToNanoseconds(const int64_t offset_ns,
+                                   const double time_sec) {
+  return offset_ns + static_cast<int64_t>(time_sec * 1.E9);
+}
+} // anonymous namespace
+
 //----------------------------------------------------------------------------------------------
 /** process the input splitters given by a TableWorkspace
  * The method will transfer the start/stop time to "m_vecSplitterTime"
@@ -959,8 +992,6 @@ void FilterEvents::processMatrixSplitterWorkspace() {
  *"m_wsGroupIndexTargetMap".
  * Also, "m_maxTargetIndex" is set up to record the highest target group/index,
  * i.e., max value of m_vecSplitterGroup
- *
- * @brief FilterEvents::processTableSplittersWorkspace
  */
 void FilterEvents::processTableSplittersWorkspace() {
   // check input workspace's validity
@@ -986,27 +1017,26 @@ void FilterEvents::processTableSplittersWorkspace() {
   size_t num_rows = m_splitterTableWorkspace->rowCount();
   for (size_t irow = 0; irow < num_rows; ++irow) {
     // get start and stop time in second
-    double start_time = m_splitterTableWorkspace->cell_cast<double>(irow, 0);
-    double stop_time = m_splitterTableWorkspace->cell_cast<double>(irow, 1);
-    std::string target = m_splitterTableWorkspace->cell<std::string>(irow, 2);
-
-    int64_t start_64 =
-        filter_shift_time + static_cast<int64_t>(start_time * 1.E9);
-    int64_t stop_64 =
-        filter_shift_time + static_cast<int64_t>(stop_time * 1.E9);
+    const auto start_time = timeInSecondsToNanoseconds(
+        filter_shift_time,
+        m_splitterTableWorkspace->cell_cast<double>(irow, 0));
+    const auto stop_time = timeInSecondsToNanoseconds(
+        filter_shift_time,
+        m_splitterTableWorkspace->cell_cast<double>(irow, 1));
+    const auto target = m_splitterTableWorkspace->cell<std::string>(irow, 2);
 
     if (m_vecSplitterTime.empty()) {
       // first splitter: push the start time to vector
-      m_vecSplitterTime.push_back(start_64);
-    } else if (start_64 - m_vecSplitterTime.back() > TOLERANCE) {
+      m_vecSplitterTime.push_back(start_time);
+    } else if (start_time - m_vecSplitterTime.back() > TOLERANCE) {
       // the start time is way behind previous splitter's stop time
       // create a new splitter and set the time interval in the middle to target
       // -1
-      m_vecSplitterTime.push_back(start_64);
+      m_vecSplitterTime.push_back(start_time);
       // NOTE: use index = 0 for un-defined slot
       m_vecSplitterGroup.push_back(UNDEFINED_SPLITTING_TARGET);
       found_undefined_splitter = true;
-    } else if (abs(start_64 - m_vecSplitterTime.back()) < TOLERANCE) {
+    } else if (abs(start_time - m_vecSplitterTime.back()) < TOLERANCE) {
       // new splitter's start time is same (within tolerance) as the stop time
       // of the previous
       ;
@@ -1017,21 +1047,12 @@ void FilterEvents::processTableSplittersWorkspace() {
     }
 
     // convert string-target to integer target
-    bool addnew = false;
     int int_target(-1);
-    if (m_targetIndexMap.empty()) {
-      addnew = true;
-    } else {
-      std::map<std::string, int>::iterator mapiter =
-          m_targetIndexMap.find(target);
-      if (mapiter == m_targetIndexMap.end())
-        addnew = true;
-      else
-        int_target = mapiter->second;
-    }
+    const auto &mapiter = m_targetIndexMap.find(target);
 
-    // add a new ordered-integer-target
-    if (addnew) {
+    if (mapiter != m_targetIndexMap.end()) {
+      int_target = mapiter->second;
+    } else {
       // target is not in map
       int_target = max_target_index;
       m_targetIndexMap.insert(std::pair<std::string, int>(target, int_target));
@@ -1041,7 +1062,7 @@ void FilterEvents::processTableSplittersWorkspace() {
     }
 
     // add start time, stop time and 'target
-    m_vecSplitterTime.push_back(stop_64);
+    m_vecSplitterTime.push_back(stop_time);
     m_vecSplitterGroup.push_back(int_target);
   } // END-FOR (irow)
 
@@ -1413,16 +1434,16 @@ TimeAtSampleStrategy *FilterEvents::setupElasticTOFCorrection() const {
 TimeAtSampleStrategy *FilterEvents::setupDirectTOFCorrection() const {
 
   // Get incident energy Ei
-  double ei = 0.;
-  if (m_eventWS->run().hasProperty("Ei")) {
-    Kernel::Property *eiprop = m_eventWS->run().getProperty("Ei");
-    ei = boost::lexical_cast<double>(eiprop->value());
-    g_log.debug() << "Using stored Ei value " << ei << "\n";
-  } else {
-    ei = getProperty("IncidentEnergy");
-    if (isEmpty(ei))
+  double ei = getProperty("IncidentEnergy");
+  if (isEmpty(ei)) {
+    if (m_eventWS->run().hasProperty("Ei")) {
+      ei = m_eventWS->run().getLogAsSingleValue("Ei");
+      g_log.debug() << "Using stored Ei value " << ei << "\n";
+    } else {
       throw std::invalid_argument(
           "No Ei value has been set or stored within the run information.");
+    }
+  } else {
     g_log.debug() << "Using user-input Ei value " << ei << "\n";
   }
 
@@ -1442,6 +1463,7 @@ TimeAtSampleStrategy *FilterEvents::setupIndirectTOFCorrection() const {
  */
 void FilterEvents::setupCustomizedTOFCorrection() {
   // Check input workspace
+  m_detCorrectWorkspace = getProperty("DetectorTOFCorrectionWorkspace");
   vector<string> colnames = m_detCorrectWorkspace->getColumnNames();
   bool hasshift = false;
   if (colnames.size() < 2)
diff --git a/Framework/Algorithms/src/TimeAtSampleStrategyDirect.cpp b/Framework/Algorithms/src/TimeAtSampleStrategyDirect.cpp
index d19f8d97dbdb83cf9ebd93f3f31e28ff68ed11b5..ff68180e289c0344bcfa0e579285584d928dc058 100644
--- a/Framework/Algorithms/src/TimeAtSampleStrategyDirect.cpp
+++ b/Framework/Algorithms/src/TimeAtSampleStrategyDirect.cpp
@@ -30,14 +30,17 @@ TimeAtSampleStrategyDirect::TimeAtSampleStrategyDirect(
     MatrixWorkspace_const_sptr ws, double ei)
     : m_constShift(0) {
 
+  // A constant among all spectra
+  constexpr double TWO_MEV_OVER_MASS =
+      2. * PhysicalConstants::meV / PhysicalConstants::NeutronMass;
+
   // Get L1
-  V3D samplepos = ws->getInstrument()->getSample()->getPos();
-  V3D sourcepos = ws->getInstrument()->getSource()->getPos();
+  const auto &samplepos = ws->getInstrument()->getSample()->getPos();
+  const auto &sourcepos = ws->getInstrument()->getSource()->getPos();
   double l1 = samplepos.distance(sourcepos);
 
   // Calculate constant (to all spectra) shift
-  m_constShift = l1 / std::sqrt(ei * 2. * PhysicalConstants::meV /
-                                PhysicalConstants::NeutronMass);
+  m_constShift = l1 / std::sqrt(ei * TWO_MEV_OVER_MASS);
 }
 
 /**
@@ -51,6 +54,5 @@ Correction Mantid::Algorithms::TimeAtSampleStrategyDirect::calculate(
   // required.
   return Correction(0, m_constShift);
 }
-
 } // namespace Algorithms
 } // namespace Mantid
diff --git a/Framework/Algorithms/src/TimeAtSampleStrategyElastic.cpp b/Framework/Algorithms/src/TimeAtSampleStrategyElastic.cpp
index a005257641b379c67f78750f4eb2a81294865d39..0d7f3732da2714fad7fdfa8d2384bc47f39736ef 100644
--- a/Framework/Algorithms/src/TimeAtSampleStrategyElastic.cpp
+++ b/Framework/Algorithms/src/TimeAtSampleStrategyElastic.cpp
@@ -33,22 +33,20 @@ TimeAtSampleStrategyElastic::TimeAtSampleStrategyElastic(
  */
 Correction
 TimeAtSampleStrategyElastic::calculate(const size_t &workspace_index) const {
-  Correction retvalue(0, 0);
 
   // Calculate TOF ratio
   const double L1s = m_spectrumInfo.l1();
+  double scale;
   if (m_spectrumInfo.isMonitor(workspace_index)) {
     double L1m =
         m_beamDir.scalar_prod(m_spectrumInfo.sourcePosition() -
                               m_spectrumInfo.position(workspace_index));
-    retvalue.factor = std::abs(L1s / L1m);
+    scale = std::abs(L1s / L1m);
   } else {
-    retvalue.factor = L1s / (L1s + m_spectrumInfo.l2(workspace_index));
+    scale = L1s / (L1s + m_spectrumInfo.l2(workspace_index));
   }
 
-  retvalue.offset = 0;
-
-  return retvalue;
+  return Correction(0., scale);
 }
 
 } // namespace Algorithms
diff --git a/Framework/Algorithms/src/TimeAtSampleStrategyIndirect.cpp b/Framework/Algorithms/src/TimeAtSampleStrategyIndirect.cpp
index 8ca7195c0dc16f41e5f0b9d8e0ca395929272d58..ee30d23235f1ac4557f91b60c7611ac3f7204628 100644
--- a/Framework/Algorithms/src/TimeAtSampleStrategyIndirect.cpp
+++ b/Framework/Algorithms/src/TimeAtSampleStrategyIndirect.cpp
@@ -36,44 +36,50 @@ Correction
 TimeAtSampleStrategyIndirect::calculate(const size_t &workspace_index) const {
 
   // A constant among all spectra
-  double twomev_d_mass =
+  constexpr double TWO_MEV_OVER_MASS =
       2. * PhysicalConstants::meV / PhysicalConstants::NeutronMass;
 
-  // Get the parameter map
-  const ParameterMap &pmap = m_ws->constInstrumentParameters();
-
-  double shift;
   const IDetector *det = &m_spectrumInfo.detector(workspace_index);
-  if (!m_spectrumInfo.isMonitor(workspace_index)) {
-    // Get E_fix
-    double efix = 0.;
-    try {
-      Parameter_sptr par = pmap.getRecursive(det, "Efixed");
-      if (par) {
-        efix = par->value<double>();
-      }
-    } catch (std::runtime_error &) {
-      // Throws if a DetectorGroup, use single provided value
-      std::stringstream errmsg;
-      errmsg << "Inelastic instrument detector " << det->getID()
-             << " of spectrum " << workspace_index << " does not have EFixed ";
-      throw std::runtime_error(errmsg.str());
-    }
-
-    double l2 = m_spectrumInfo.l2(workspace_index);
-    shift = -1. * l2 / sqrt(efix * twomev_d_mass);
+  if (m_spectrumInfo.isMonitor(workspace_index)) {
+    // use the same math as TimeAtSampleStrategyElastic
+    const double L1s = m_spectrumInfo.l1();
+    const auto &beamDir =
+        m_ws->getInstrument()->getReferenceFrame()->vecPointingAlongBeam();
+    const double L1m =
+        beamDir.scalar_prod(m_spectrumInfo.sourcePosition() -
+                            m_spectrumInfo.position(workspace_index));
+    const double scale = std::abs(L1s / L1m);
+    return Correction(0., scale);
+  }
 
-  } else {
-    std::stringstream errormsg;
-    errormsg << "Workspace index " << workspace_index << " is a monitor. ";
-    throw std::invalid_argument(errormsg.str());
+  // Get E_fix
+  double efix{0.};
+  try {
+    // Get the parameter map
+    const ParameterMap &pmap = m_ws->constInstrumentParameters();
+    Parameter_sptr par = pmap.getRecursive(det, "Efixed");
+    if (par) {
+      efix = par->value<double>();
+    }
+  } catch (std::runtime_error &) {
+    // Throws if a DetectorGroup, use single provided value
+    std::stringstream errmsg;
+    errmsg << "Inelastic instrument detector " << det->getID()
+           << " of spectrum " << workspace_index << " does not have EFixed ";
+    throw std::runtime_error(errmsg.str());
+  }
+  if (efix <= 0.) {
+    std::stringstream errmsg;
+    errmsg << "Inelastic instrument detector " << det->getID()
+           << " of spectrum " << workspace_index << " does not have EFixed ";
+    throw std::runtime_error(errmsg.str());
   }
 
-  Correction retvalue(0, 0);
-  retvalue.factor = 1.0;
-  retvalue.offset = shift;
+  const double l2 = m_spectrumInfo.l2(workspace_index);
+  const double shift = -1. * l2 / sqrt(efix * TWO_MEV_OVER_MASS);
 
-  return retvalue;
+  // 1.0 * tof + shift
+  return Correction(shift, 1.0);
 }
 
 } // namespace Algorithms
diff --git a/Framework/Algorithms/test/AnyShapeAbsorptionTest.h b/Framework/Algorithms/test/AnyShapeAbsorptionTest.h
index deada58ef8d4a1fd94bb0bda8d58247e3dcd6745..526810f1f8eab18b138b65faaed92417e0db168c 100644
--- a/Framework/Algorithms/test/AnyShapeAbsorptionTest.h
+++ b/Framework/Algorithms/test/AnyShapeAbsorptionTest.h
@@ -127,8 +127,7 @@ public:
     TS_ASSERT(cyl.isExecuted());
 
     // Using the output of the CylinderAbsorption algorithm is convenient
-    // because
-    // it adds the sample object to the workspace
+    // because it adds the sample object to the workspace
     TS_ASSERT_THROWS_NOTHING(atten2.setPropertyValue("InputWorkspace", cylWS));
     std::string outputWS("factors");
     TS_ASSERT_THROWS_NOTHING(
@@ -199,6 +198,55 @@ public:
     Mantid::API::AnalysisDataService::Instance().remove("gauge");
   }
 
+  void testTinyVolume() {
+    if (!atten.isInitialized())
+      atten.initialize();
+
+    // Create a small test workspace
+    MatrixWorkspace_sptr testWS =
+        WorkspaceCreationHelper::create2DWorkspaceWithFullInstrument(1, 10);
+    // Needs to have units of wavelength
+    testWS->getAxis(0)->unit() =
+        Mantid::Kernel::UnitFactory::Instance().create("Wavelength");
+
+    Mantid::Algorithms::FlatPlateAbsorption flat;
+    flat.initialize();
+    TS_ASSERT_THROWS_NOTHING(
+        flat.setProperty<MatrixWorkspace_sptr>("InputWorkspace", testWS));
+    std::string flatWS("flat");
+    TS_ASSERT_THROWS_NOTHING(flat.setPropertyValue("OutputWorkspace", flatWS));
+    TS_ASSERT_THROWS_NOTHING(
+        flat.setPropertyValue("AttenuationXSection", "5.08"));
+    TS_ASSERT_THROWS_NOTHING(
+        flat.setPropertyValue("ScatteringXSection", "5.1"));
+    TS_ASSERT_THROWS_NOTHING(
+        flat.setPropertyValue("SampleNumberDensity", "0.07192"));
+    TS_ASSERT_THROWS_NOTHING(flat.setPropertyValue("SampleHeight", "2.3"));
+    TS_ASSERT_THROWS_NOTHING(flat.setPropertyValue("SampleWidth", "1.8"));
+    // too thin to work in any shapes gauge volume creation
+    TS_ASSERT_THROWS_NOTHING(flat.setPropertyValue("SampleThickness", ".1"));
+    TS_ASSERT_THROWS_NOTHING(flat.execute());
+    TS_ASSERT(flat.isExecuted());
+
+    // Using the output of the FlatPlateAbsorption algorithm is convenient
+    // because it adds the sample object to the workspace
+    TS_ASSERT_THROWS_NOTHING(atten.setPropertyValue("InputWorkspace", flatWS));
+    std::string outputWS("factors");
+    TS_ASSERT_THROWS_NOTHING(
+        atten.setPropertyValue("OutputWorkspace", outputWS));
+    TS_ASSERT_THROWS_NOTHING(
+        atten.setPropertyValue("AttenuationXSection", "5.08"));
+    TS_ASSERT_THROWS_NOTHING(
+        atten.setPropertyValue("ScatteringXSection", "5.1"));
+    TS_ASSERT_THROWS_NOTHING(
+        atten.setPropertyValue("SampleNumberDensity", "0.07192"));
+    atten.setRethrows(true); // needed for the next check to work
+    TS_ASSERT_THROWS(atten.execute(), std::runtime_error);
+    TS_ASSERT(!atten.isExecuted());
+
+    Mantid::API::AnalysisDataService::Instance().remove(flatWS);
+  }
+
 private:
   Mantid::Algorithms::AnyShapeAbsorption atten;
 };
diff --git a/Framework/Algorithms/test/ApplyDetailedBalanceTest.h b/Framework/Algorithms/test/ApplyDetailedBalanceTest.h
index 85a614793ff1369db1f8b0ef8867e9aed08decea..9ef20ae48ddf9aa9804bca230c2f37575703c2bf 100644
--- a/Framework/Algorithms/test/ApplyDetailedBalanceTest.h
+++ b/Framework/Algorithms/test/ApplyDetailedBalanceTest.h
@@ -89,7 +89,7 @@ public:
         alg.setPropertyValue("OutputWorkspace", outputWSname));
     TS_ASSERT_THROWS_NOTHING(alg.setPropertyValue("Temperature", "x"));
     TS_ASSERT_THROWS_NOTHING(alg.execute());
-    TS_ASSERT(alg.isExecuted());
+    TS_ASSERT(!alg.isExecuted());
     Workspace2D_sptr outws;
     TS_ASSERT_THROWS_ANYTHING(
         outws = AnalysisDataService::Instance().retrieveWS<Workspace2D>(
diff --git a/Framework/Algorithms/test/TimeAtSampleStrategyDirectTest.h b/Framework/Algorithms/test/TimeAtSampleStrategyDirectTest.h
index d997c8ce86fa7453bf48671048560d6ed0e2a5ae..471e9d4d9f7d4270f06b33ee788d46ba30a353ef 100644
--- a/Framework/Algorithms/test/TimeAtSampleStrategyDirectTest.h
+++ b/Framework/Algorithms/test/TimeAtSampleStrategyDirectTest.h
@@ -57,7 +57,7 @@ public:
     double expectedShift = L1 / std::sqrt(ei * 2. * PhysicalConstants::meV /
                                           PhysicalConstants::NeutronMass);
 
-    TSM_ASSERT_EQUALS("L1 / (L1 + L2)", expectedShift, shift);
+    TSM_ASSERT_DELTA("L1 / (L1 + L2)", expectedShift, shift, 0.0000001);
   }
 };
 
diff --git a/Framework/Algorithms/test/TimeAtSampleStrategyIndirectTest.h b/Framework/Algorithms/test/TimeAtSampleStrategyIndirectTest.h
index 4430eb35a1fd7b2a2d5e70c31456ebb5e40e5cfc..f6e5b2b972b628b58f1f7ff3ac17a744e5c9fed2 100644
--- a/Framework/Algorithms/test/TimeAtSampleStrategyIndirectTest.h
+++ b/Framework/Algorithms/test/TimeAtSampleStrategyIndirectTest.h
@@ -8,6 +8,8 @@
 #define MANTID_ALGORITHMS_TIMEATSAMPLESTRATEGYINDIRECTTEST_H_
 
 #include "MantidAlgorithms/TimeAtSampleStrategyIndirect.h"
+#include "MantidGeometry/Instrument.h"
+#include "MantidGeometry/Instrument/ReferenceFrame.h"
 #include "MantidTestHelpers/WorkspaceCreationHelper.h"
 #include <cxxtest/TestSuite.h>
 
@@ -28,9 +30,27 @@ public:
     auto ws = WorkspaceCreationHelper::
         create2DWorkspaceWithReflectometryInstrument(); // workspace has
                                                         // monitors
+
+    const size_t monitorIndex = 1; // monitor workspace index.
+    const auto &instrument = ws->getInstrument();
+    auto sample = instrument->getSample();
+    auto source = instrument->getSource();
+
+    const auto &beamDir =
+        instrument->getReferenceFrame()->vecPointingAlongBeam();
+
+    auto monitor = ws->getDetector(monitorIndex);
+
+    const double L1 = source->getPos().distance(sample->getPos());
+
     TimeAtSampleStrategyIndirect strategy(ws);
-    TS_ASSERT_THROWS(strategy.calculate(1 /*monitor index*/),
-                     std::invalid_argument &);
+    const auto correction = strategy.calculate(monitorIndex);
+
+    TSM_ASSERT_EQUALS("L1/L1m",
+                      std::abs(L1 / beamDir.scalar_prod(source->getPos() -
+                                                        monitor->getPos())),
+                      correction.factor);
+    TS_ASSERT_EQUALS(0., correction.offset);
   }
 };
 
diff --git a/Framework/Crystal/CMakeLists.txt b/Framework/Crystal/CMakeLists.txt
index 5d22c05bcba5270d7eeb3d18781b1a238c2cdcb9..29285d0473cd843c661dcad4d2adb6f367a46289 100644
--- a/Framework/Crystal/CMakeLists.txt
+++ b/Framework/Crystal/CMakeLists.txt
@@ -58,6 +58,7 @@ set ( SRC_FILES
 	src/SaveLauenorm.cpp
 	src/SelectCellOfType.cpp
 	src/SelectCellWithForm.cpp
+	src/SetCrystalLocation.cpp
 	src/SetGoniometer.cpp
 	src/SetSpecialCoordinates.cpp
 	src/SetUB.cpp
@@ -132,6 +133,7 @@ set ( INC_FILES
 	inc/MantidCrystal/SaveLauenorm.h
 	inc/MantidCrystal/SelectCellOfType.h
 	inc/MantidCrystal/SelectCellWithForm.h
+	inc/MantidCrystal/SetCrystalLocation.h
 	inc/MantidCrystal/SetGoniometer.h
 	inc/MantidCrystal/SetSpecialCoordinates.h
 	inc/MantidCrystal/SetUB.h
@@ -199,6 +201,7 @@ set ( TEST_FILES
 	SaveLauenormTest.h
 	SelectCellOfTypeTest.h
 	SelectCellWithFormTest.h
+    SetCrystalLocationTest.h
 	SetGoniometerTest.h
 	SetSpecialCoordinatesTest.h
 	SetUBTest.h
diff --git a/Framework/Crystal/inc/MantidCrystal/SetCrystalLocation.h b/Framework/Crystal/inc/MantidCrystal/SetCrystalLocation.h
new file mode 100644
index 0000000000000000000000000000000000000000..4eb14a9af3877b98b603812d486431af32dd93fc
--- /dev/null
+++ b/Framework/Crystal/inc/MantidCrystal/SetCrystalLocation.h
@@ -0,0 +1,57 @@
+// Mantid Repository : https://github.com/mantidproject/mantid
+//
+// Copyright &copy; 2018 ISIS Rutherford Appleton Laboratory UKRI,
+//     NScD Oak Ridge National Laboratory, European Spallation Source
+//     & Institut Laue - Langevin
+// SPDX - License - Identifier: GPL - 3.0 +
+/*
+ * SetCrystalLocation.h
+ *
+ *  Created on: Dec 12, 2018
+ *      Author: Brendan Sullivan
+ */
+
+#ifndef SETCRYSTALLOCATION_H_
+#define SETCRYSTALLOCATION_H_
+
+#include "MantidAPI/Algorithm.h"
+#include "MantidKernel/System.h"
+
+namespace Mantid {
+namespace Crystal {
+
+/** SetCrystalLocation
+
+Description:
+This algorithm provides a convenient interface to sets the
+sample position of an events workspace.
+@author Brendan Sullivan, SNS,ORNL
+@date Dec 20 2018
+*/
+class DLLExport SetCrystalLocation : public API::Algorithm {
+public:
+  const std::string name() const override { return "SetCrystalLocation"; };
+  /// Summary of algorithms purpose
+  const std::string summary() const override {
+    return "This algorithm sets the sample location of the "
+           "input event workspace.";
+  }
+  const std::vector<std::string> seeAlso() const override {
+    return {"OptimizeCrystalPlacement"};
+  }
+
+  int version() const override { return 1; };
+
+  const std::string category() const override {
+    return "Crystal\\Corrections";
+  };
+
+private:
+  void init() override;
+
+  void exec() override;
+};
+} // namespace Crystal
+} // namespace Mantid
+
+#endif /* SETCRYSTALLOCATION_H_ */
diff --git a/Framework/Crystal/src/OptimizeCrystalPlacement.cpp b/Framework/Crystal/src/OptimizeCrystalPlacement.cpp
index 2b02a4e69a1f9a34c43e7162ecb077950fce8b77..257a0f8129e8008bd1ec51ca64da3e15582f888d 100644
--- a/Framework/Crystal/src/OptimizeCrystalPlacement.cpp
+++ b/Framework/Crystal/src/OptimizeCrystalPlacement.cpp
@@ -118,10 +118,10 @@ void OptimizeCrystalPlacement::init() {
   declareProperty("MaxAngularChange", 5.0,
                   "Max offset in degrees from current settings(def=5)");
 
-  declareProperty("MaxIndexingError", .25,
+  declareProperty("MaxIndexingError", 0.15,
                   "Use only peaks whose fractional "
                   "hkl values are below this "
-                  "tolerance(def=.25)");
+                  "tolerance(def=0.15)");
 
   declareProperty("MaxHKLPeaks2Use", -1.0,
                   "If less than 0 all peaks are used, "
@@ -449,8 +449,9 @@ void OptimizeCrystalPlacement::exec() {
   UBinv.Invert();
   UBinv /= (2 * M_PI);
   for (int i = 0; i < outPeaks->getNumberPeaks(); ++i) {
-    outPeaks->getPeak(i).setSamplePos(newSampPos);
-    int RunNum = outPeaks->getPeak(i).getRunNumber();
+    auto peak = outPeaks->getPeak(i);
+    peak.setSamplePos(peak.getSamplePos() + newSampPos);
+    int RunNum = peak.getRunNumber();
     std::string RunNumStr = std::to_string(RunNum);
     Matrix<double> GonMatrix;
     if (RunNum == prevRunNum ||
@@ -472,12 +473,12 @@ void OptimizeCrystalPlacement::exec() {
       GonMatrix = GonTilt * uniGonio.getR();
       MapRunNum2GonMat[RunNum] = GonMatrix;
     } else {
-      GonMatrix = GonTilt * outPeaks->getPeak(i).getGoniometerMatrix();
+      GonMatrix = GonTilt * peak.getGoniometerMatrix();
       MapRunNum2GonMat[RunNum] = GonMatrix;
     }
 
-    outPeaks->getPeak(i).setGoniometerMatrix(GonMatrix);
-    V3D hkl = UBinv * outPeaks->getPeak(i).getQSampleFrame();
+    peak.setGoniometerMatrix(GonMatrix);
+    V3D hkl = UBinv * peak.getQSampleFrame();
     if (Geometry::IndexingUtils::ValidIndex(hkl, HKLintOffsetMax))
       nIndexed++;
 
diff --git a/Framework/Crystal/src/PeakHKLErrors.cpp b/Framework/Crystal/src/PeakHKLErrors.cpp
index 059fd6191aeaf743624341ffcede102cbe88ce0c..3edfe24e37f5f5fc0a06130cca41282b3975c7ae 100644
--- a/Framework/Crystal/src/PeakHKLErrors.cpp
+++ b/Framework/Crystal/src/PeakHKLErrors.cpp
@@ -208,7 +208,7 @@ PeakHKLErrors::getNewInstrument(PeaksWorkspace_sptr Peaks) const {
   } else // catch(... )
   {
     auto P1 = boost::make_shared<Geometry::Instrument>(
-        instSave->baseInstrument(), pmap);
+        instSave->baseInstrument(), instSave->makeLegacyParameterMap());
     instChange = P1;
   }
 
@@ -394,6 +394,10 @@ void PeakHKLErrors::function1D(double *out, const double *xValues,
     } else {
       peak.setGoniometerMatrix(GonRot * peak.getGoniometerMatrix());
     }
+    V3D sampOffsets(getParameter("SampleXOffset"),
+                    getParameter("SampleYOffset"),
+                    getParameter("SampleZOffset"));
+    peak.setSamplePos(peak.getSamplePos() + sampOffsets);
 
     V3D hkl = UBinv * peak.getQSampleFrame();
 
@@ -513,6 +517,10 @@ void PeakHKLErrors::functionDeriv1D(Jacobian *out, const double *xValues,
       chiParamNum = phiParamNum = omegaParamNum = nParams() + 10;
       peak.setGoniometerMatrix(GonRot * peak.getGoniometerMatrix());
     }
+    V3D sampOffsets(getParameter("SampleXOffset"),
+                    getParameter("SampleYOffset"),
+                    getParameter("SampleZOffset"));
+    peak.setSamplePos(peak.getSamplePos() + sampOffsets);
     // NOTE:Use getQLabFrame except for below.
     // For parameters the getGoniometerMatrix should remove GonRot, for derivs
     // wrt GonRot*, wrt chi*,phi*,etc.
diff --git a/Framework/Crystal/src/SetCrystalLocation.cpp b/Framework/Crystal/src/SetCrystalLocation.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..86568da261d89f048a44c31d9cf0e326feb065fb
--- /dev/null
+++ b/Framework/Crystal/src/SetCrystalLocation.cpp
@@ -0,0 +1,80 @@
+// Mantid Repository : https://github.com/mantidproject/mantid
+//
+// Copyright &copy; 2018 ISIS Rutherford Appleton Laboratory UKRI,
+//     NScD Oak Ridge National Laboratory, European Spallation Source
+//     & Institut Laue - Langevin
+// SPDX - License - Identifier: GPL - 3.0 +
+/*
+ *
+ * SetCrystalLocation.cpp
+ *
+ *  Created on: Dec 12, 2018
+ *      Author: Brendan Sullivan
+ */
+#include "MantidCrystal/SetCrystalLocation.h"
+
+#include "MantidAPI/IMDEventWorkspace.h"
+#include "MantidAPI/Run.h"
+#include "MantidAPI/Sample.h"
+#include "MantidAPI/WorkspaceFactory.h"
+#include "MantidCrystal/CalibrationHelpers.h"
+#include "MantidCrystal/PeakHKLErrors.h"
+#include "MantidCrystal/SCDCalibratePanels.h"
+#include "MantidDataObjects/EventWorkspace.h"
+#include "MantidGeometry/Crystal/IPeak.h"
+#include "MantidGeometry/Crystal/IndexingUtils.h"
+#include "MantidGeometry/Instrument/ComponentInfo.h"
+#include "MantidGeometry/Instrument/Goniometer.h"
+#include "MantidKernel/ArrayProperty.h"
+#include "MantidKernel/EnabledWhenProperty.h"
+
+#include <cstdarg>
+
+using namespace Mantid::API;
+using namespace Mantid::DataObjects;
+using namespace Mantid::Kernel;
+using Mantid::Geometry::IndexingUtils;
+using Mantid::Geometry::Instrument_const_sptr;
+using namespace Mantid::Geometry;
+
+namespace Mantid {
+
+namespace Crystal {
+
+DECLARE_ALGORITHM(SetCrystalLocation)
+
+void SetCrystalLocation::init() {
+  declareProperty(make_unique<WorkspaceProperty<EventWorkspace>>(
+                      "InputWorkspace", "", Direction::Input),
+                  "Original event workspace");
+  declareProperty(make_unique<WorkspaceProperty<EventWorkspace>>(
+                      "OutputWorkspace", "", Direction::Output),
+                  "Output event workspace with a modified sample position");
+  declareProperty("NewX", 0.0, "New Absolute X position of crystal.");
+  declareProperty("NewY", 0.0, "New Absolute Y position of crystal.");
+  declareProperty("NewZ", 0.0, "New Absolute Z position of crystal.");
+}
+
+// simple algorithm that changes the sample position of the input
+// event workspace.
+void SetCrystalLocation::exec() {
+  EventWorkspace_sptr events = getProperty("InputWorkspace");
+  EventWorkspace_sptr outEvents = getProperty("OutputWorkspace");
+  const double newX = getProperty("NewX");
+  const double newY = getProperty("NewY");
+  const double newZ = getProperty("NewZ");
+  V3D newSamplePos = V3D(newX, newY, newZ);
+  if (events != outEvents) {
+    outEvents = events->clone();
+  }
+
+  auto &componentInfo = outEvents->mutableComponentInfo();
+  CalibrationHelpers::adjustUpSampleAndSourcePositions(
+      componentInfo.l1(), newSamplePos, componentInfo);
+
+  setProperty("OutputWorkspace", outEvents);
+} // exec
+
+} // namespace Crystal
+
+} // namespace Mantid
diff --git a/Framework/Crystal/test/OptimizeCrystalPlacementTest.h b/Framework/Crystal/test/OptimizeCrystalPlacementTest.h
index 4a17be0b0bad8265a7b8b5f0c349136a8b2982d7..f76e3532a14fc36e05b612f956ab3416224ed03c 100644
--- a/Framework/Crystal/test/OptimizeCrystalPlacementTest.h
+++ b/Framework/Crystal/test/OptimizeCrystalPlacementTest.h
@@ -187,9 +187,10 @@ public:
 
   void test_SamplePosition() {
     auto modPeaksNoFix = calculateBasicPlacement();
-    const auto &peak = modPeaksNoFix->getPeak(0);
+    auto peak = modPeaksNoFix->getPeak(0);
     auto inst = peak.getInstrument();
     const V3D sampPos(.0003, -.00025, .00015);
+    peak.setSamplePos(sampPos);
 
     auto pmap = inst->getParameterMap();
     auto sample = inst->getSample();
@@ -208,9 +209,9 @@ public:
         modPeaksNoFix, {{"KeepGoniometerFixedfor", "5637, 5638"},
                         {"AdjustSampleOffsets", "1"}});
     const auto table = resultsSamplePos.second;
-    TS_ASSERT_DELTA(table->Double(0, 1), 0, .0004);
-    TS_ASSERT_DELTA(table->Double(1, 1), 0, .00024);
-    TS_ASSERT_DELTA(table->Double(2, 1), 0, .0003);
+    TS_ASSERT_DELTA(table->Double(0, 1), 0, 0.0000773);
+    TS_ASSERT_DELTA(table->Double(1, 1), 0, 0.00004575);
+    TS_ASSERT_DELTA(table->Double(2, 1), 0, 0.00006745);
   }
 
 private:
diff --git a/Framework/Crystal/test/SCDCalibratePanelsTest.h b/Framework/Crystal/test/SCDCalibratePanelsTest.h
index 92e273852585beceef3a4017f997ee1c78ff2595..f1b53e34d72d53391a738125dc7a256cf3c9c6fb 100644
--- a/Framework/Crystal/test/SCDCalibratePanelsTest.h
+++ b/Framework/Crystal/test/SCDCalibratePanelsTest.h
@@ -64,7 +64,7 @@ public:
     TS_ASSERT_DELTA(0.0, results->cell<double>(3, 1), 1.2);
     TS_ASSERT_DELTA(0.0, results->cell<double>(4, 1), 1.1);
     TS_ASSERT_DELTA(0.1133, results->cell<double>(5, 1), 0.36);
-    TS_ASSERT_DELTA(1.0024, results->cell<double>(6, 1), 5e-3);
+    TS_ASSERT_DELTA(0.9953, results->cell<double>(6, 1), 1e-2);
     TS_ASSERT_DELTA(0.9986, results->cell<double>(7, 1), 1e-2);
     TS_ASSERT_DELTA(0.2710, results->cell<double>(9, 1), 0.2);
     ITableWorkspace_sptr resultsL1 =
diff --git a/Framework/Crystal/test/SetCrystalLocationTest.h b/Framework/Crystal/test/SetCrystalLocationTest.h
new file mode 100644
index 0000000000000000000000000000000000000000..109be075372f7c53ba5b8bf19c090d9e81b60306
--- /dev/null
+++ b/Framework/Crystal/test/SetCrystalLocationTest.h
@@ -0,0 +1,112 @@
+// Mantid Repository : https://github.com/mantidproject/mantid
+//
+// Copyright &copy; 2018 ISIS Rutherford Appleton Laboratory UKRI,
+//     NScD Oak Ridge National Laboratory, European Spallation Source
+//     & Institut Laue - Langevin
+// SPDX - License - Identifier: GPL - 3.0 +
+#ifndef SETCRYSTALLOCATIONTEST_H_
+#define SETCRYSTALLOCATIONTEST_H_
+#include "MantidAPI/AnalysisDataService.h"
+#include "MantidAPI/ITableWorkspace.h"
+#include "MantidCrystal/LoadIsawUB.h"
+#include "MantidCrystal/SetCrystalLocation.h"
+#include "MantidCrystal/ShowPeakHKLOffsets.h"
+#include "MantidDataHandling/Load.h"
+#include "MantidDataObjects/EventWorkspace.h"
+#include "MantidDataObjects/PeaksWorkspace.h"
+#include <cxxtest/TestSuite.h>
+
+using Mantid::DataHandling::Load;
+using namespace Mantid::DataObjects;
+using Mantid::Crystal::LoadIsawUB;
+using Mantid::Crystal::SetCrystalLocation;
+using Mantid::Crystal::ShowPeakHKLOffsets;
+using Mantid::DataObjects::TableWorkspace;
+using Mantid::Kernel::V3D;
+using namespace Mantid::API;
+
+class SetCrystalLocationTest : public CxxTest::TestSuite {
+public:
+  void test_algo() {
+    std::string WSName = "events";
+    std::string file_name = "BSS_11841_event.nxs";
+    Load loader;
+    TS_ASSERT_THROWS_NOTHING(loader.initialize());
+    TS_ASSERT(loader.isInitialized());
+    loader.setPropertyValue("OutputWorkspace", WSName);
+    loader.setPropertyValue("Filename", file_name);
+    TS_ASSERT(loader.execute());
+    TS_ASSERT(loader.isExecuted());
+
+    auto workspace = AnalysisDataService::Instance().retrieve(WSName);
+    EventWorkspace_sptr events =
+        boost::dynamic_pointer_cast<EventWorkspace>(workspace);
+    TS_ASSERT(events);
+    auto inst = events->getInstrument();
+    TS_ASSERT(inst);
+    auto sample = inst->getSample();
+    TS_ASSERT(sample);
+
+    SetCrystalLocation algo;
+    TS_ASSERT_THROWS_NOTHING(algo.initialize());
+    TS_ASSERT(algo.isInitialized());
+    algo.setProperty("InputWorkspace", WSName);
+    algo.setProperty("OutputWorkspace", WSName);
+    algo.setProperty("NewX", 1.0);
+    algo.setProperty("NewY", -0.30);
+    algo.setProperty("NewZ", 10.0);
+
+    // Check the sample is at the origin by default
+    V3D sampPos0 = sample->getPos();
+    TS_ASSERT_DELTA(sampPos0.X(), 0.0, 1.e-3);
+    TS_ASSERT_DELTA(sampPos0.Y(), 0.0, 1.e-3);
+    TS_ASSERT_DELTA(sampPos0.Z(), 0.0, 1.e-3);
+
+    // Move the sample to (1.0, -0.3, 10.0)
+    TS_ASSERT(algo.execute());
+    TS_ASSERT(algo.isExecuted());
+
+    // Check that the sample moved
+    V3D sampPos1 = sample->getPos();
+    TS_ASSERT_DELTA(sampPos1.X(), 1.0, 1.e-3);
+    TS_ASSERT_DELTA(sampPos1.Y(), -0.30, 1.e-3);
+    TS_ASSERT_DELTA(sampPos1.Z(), 10.0, 1.e-3);
+
+    // Try it on a separate workspace
+    SetCrystalLocation algo2;
+    TS_ASSERT_THROWS_NOTHING(algo2.initialize());
+    TS_ASSERT(algo2.isInitialized());
+    algo2.setProperty("InputWorkspace", WSName);
+    algo2.setProperty("OutputWorkspace", "events_new");
+    algo2.setProperty("NewX", 2.0);
+    algo2.setProperty("NewY", 4.0);
+    algo2.setProperty("NewZ", 0.0);
+    TS_ASSERT(algo2.execute());
+    TS_ASSERT(algo2.isExecuted());
+
+    // Check that the original is unchanged.  "Events" should be at
+    // the same sampPos1 = (1.0, -0.3, 10.0)
+    V3D sampPos2 = sample->getPos();
+    TS_ASSERT_DELTA(sampPos2.X(), 1.0, 1.e-3);
+    TS_ASSERT_DELTA(sampPos2.Y(), -0.30, 1.e-3);
+    TS_ASSERT_DELTA(sampPos2.Z(), 10.0, 1.e-3);
+
+    // Get pointers to the new workspace
+    auto workspace_new = AnalysisDataService::Instance().retrieve("events_new");
+    EventWorkspace_sptr events_new =
+        boost::dynamic_pointer_cast<EventWorkspace>(workspace_new);
+    TS_ASSERT(events_new)
+    auto inst_new = events_new->getInstrument();
+    TS_ASSERT(inst_new);
+    auto sample_new = inst_new->getSample();
+    TS_ASSERT(sample_new);
+
+    // the new workspace should be at (2.,4.,0.,)
+    V3D sampPos3 = sample_new->getPos();
+    TS_ASSERT_DELTA(sampPos3.X(), 2.0, 1.e-3);
+    TS_ASSERT_DELTA(sampPos3.Y(), 4.0, 1.e-3);
+    TS_ASSERT_DELTA(sampPos3.Z(), 0.0, 1.e-3);
+  }
+};
+
+#endif /* SETCRYSTALLOCATIONTEST_H_ */
diff --git a/Framework/DataHandling/CMakeLists.txt b/Framework/DataHandling/CMakeLists.txt
index 3482694e01f61bd9f5e999c17576ceb7151c330d..a98c182bb788f91fab377124470df242c084ed02 100644
--- a/Framework/DataHandling/CMakeLists.txt
+++ b/Framework/DataHandling/CMakeLists.txt
@@ -141,7 +141,6 @@
 	src/RenameLog.cpp
 	src/RotateInstrumentComponent.cpp
 	src/RotateSource.cpp
-	src/SNSDataArchive.cpp
 	src/SaveANSTOAscii.cpp
 	src/SaveAscii.cpp
 	src/SaveAscii2.cpp
@@ -335,7 +334,6 @@ set ( INC_FILES
 	inc/MantidDataHandling/RenameLog.h
 	inc/MantidDataHandling/RotateInstrumentComponent.h
 	inc/MantidDataHandling/RotateSource.h
-	inc/MantidDataHandling/SNSDataArchive.h
 	inc/MantidDataHandling/SaveANSTOAscii.h
 	inc/MantidDataHandling/SaveAscii.h
 	inc/MantidDataHandling/SaveAscii2.h
@@ -518,7 +516,6 @@ set ( TEST_FILES
 	RenameLogTest.h
 	RotateInstrumentComponentTest.h
 	RotateSourceTest.h
-	SNSDataArchiveTest.h
 	SaveANSTOAsciiTest.h
 	SaveAscii2Test.h
 	SaveAsciiTest.h
diff --git a/Framework/DataHandling/inc/MantidDataHandling/SNSDataArchive.h b/Framework/DataHandling/inc/MantidDataHandling/SNSDataArchive.h
deleted file mode 100644
index d31f5a113f1e2058b035b5c9ed659863e8ba12cf..0000000000000000000000000000000000000000
--- a/Framework/DataHandling/inc/MantidDataHandling/SNSDataArchive.h
+++ /dev/null
@@ -1,35 +0,0 @@
-// Mantid Repository : https://github.com/mantidproject/mantid
-//
-// Copyright &copy; 2010 ISIS Rutherford Appleton Laboratory UKRI,
-//     NScD Oak Ridge National Laboratory, European Spallation Source
-//     & Institut Laue - Langevin
-// SPDX - License - Identifier: GPL - 3.0 +
-#ifndef MANTID_DATAHANDLING_SNSDATAARCHIVE_H_
-#define MANTID_DATAHANDLING_SNSDATAARCHIVE_H_
-
-//----------------------------------------------------------------------
-// Includes
-//----------------------------------------------------------------------
-#include "MantidAPI/IArchiveSearch.h"
-#include "MantidKernel/System.h"
-
-#include <string>
-
-namespace Mantid {
-namespace DataHandling {
-/**
- This class is for searching the SNS data archive
- @date 02/22/2012
- */
-
-class DLLExport SNSDataArchive : public API::IArchiveSearch {
-public:
-  /// Find the archive location of a set of files.
-  std::string
-  getArchivePath(const std::set<std::string> &filenames,
-                 const std::vector<std::string> &exts) const override;
-};
-} // namespace DataHandling
-} // namespace Mantid
-
-#endif /* MANTID_DATAHANDLING_SNSDATAARCHIVE_H_ */
diff --git a/Framework/DataHandling/src/LoadDiffCal.cpp b/Framework/DataHandling/src/LoadDiffCal.cpp
index 9c3e3ed525154cad64e496a2fe03935f0871d082..a0a61cfd7d6326433b6e1ff299e123a114a81e6d 100644
--- a/Framework/DataHandling/src/LoadDiffCal.cpp
+++ b/Framework/DataHandling/src/LoadDiffCal.cpp
@@ -18,6 +18,7 @@
 #include "MantidDataObjects/TableWorkspace.h"
 #include "MantidDataObjects/Workspace2D.h"
 #include "MantidKernel/Diffraction.h"
+#include "MantidKernel/Exception.h"
 #include "MantidKernel/OptionalBool.h"
 
 #include <H5Cpp.h>
@@ -36,6 +37,7 @@ using Mantid::DataObjects::GroupingWorkspace_sptr;
 using Mantid::DataObjects::MaskWorkspace_sptr;
 using Mantid::DataObjects::Workspace2D;
 using Mantid::Kernel::Direction;
+using Mantid::Kernel::Exception::FileError;
 using Mantid::Kernel::PropertyWithValue;
 
 using namespace H5;
@@ -397,8 +399,13 @@ void LoadDiffCal::exec() {
   }
 
   // read in everything from the file
-  H5File file(m_filename, H5F_ACC_RDONLY);
   H5::Exception::dontPrint();
+  H5File file;
+  try {
+    file = H5File(m_filename, H5F_ACC_RDONLY);
+  } catch (FileIException &) {
+    throw FileError("Failed to open file using HDF5", m_filename);
+  }
   getInstrument(file);
 
   Progress progress(this, 0.1, 0.4, 8);
@@ -412,7 +419,8 @@ void LoadDiffCal::exec() {
 #else
     e.printError(stderr);
 #endif
-    throw std::runtime_error("Did not find group \"/calibration\"");
+    file.close();
+    throw FileError("Did not find group \"/calibration\"", m_filename);
   }
 
   progress.report("Reading detid");
diff --git a/Framework/DataHandling/src/ORNLDataArchive.cpp b/Framework/DataHandling/src/ORNLDataArchive.cpp
index c3ec9081bb3543a801f757d18aa4862aaccaca5f..8eadaf1ceb8c66824fe8cc32caae83c2f9a787cb 100644
--- a/Framework/DataHandling/src/ORNLDataArchive.cpp
+++ b/Framework/DataHandling/src/ORNLDataArchive.cpp
@@ -48,6 +48,7 @@ namespace Mantid {
 namespace DataHandling {
 
 DECLARE_ARCHIVESEARCH(ORNLDataArchive, ORNLDataSearch)
+DECLARE_ARCHIVESEARCH(ORNLDataArchive, SNSDataSearch)
 
 /**
  * ****************
diff --git a/Framework/DataHandling/src/SNSDataArchive.cpp b/Framework/DataHandling/src/SNSDataArchive.cpp
deleted file mode 100644
index af276e1d0bed47629c23c5f54701eb371428fb7f..0000000000000000000000000000000000000000
--- a/Framework/DataHandling/src/SNSDataArchive.cpp
+++ /dev/null
@@ -1,110 +0,0 @@
-// Mantid Repository : https://github.com/mantidproject/mantid
-//
-// Copyright &copy; 2018 ISIS Rutherford Appleton Laboratory UKRI,
-//     NScD Oak Ridge National Laboratory, European Spallation Source
-//     & Institut Laue - Langevin
-// SPDX - License - Identifier: GPL - 3.0 +
-//----------------------------------------------------------------------
-// Includes
-//----------------------------------------------------------------------
-#include <sstream>
-
-#include "MantidAPI/ArchiveSearchFactory.h"
-#include "MantidDataHandling/SNSDataArchive.h"
-#include "MantidKernel/Exception.h"
-#include "MantidKernel/InternetHelper.h"
-#include "MantidKernel/Logger.h"
-
-#include <boost/algorithm/string.hpp>
-#include <boost/algorithm/string/predicate.hpp>
-
-#include <Poco/AutoPtr.h>
-#include <Poco/DOM/DOMParser.h>
-#include <Poco/DOM/Document.h>
-#include <Poco/DOM/Element.h>
-#include <Poco/DOM/NodeList.h>
-#include <Poco/SAX/InputSource.h>
-
-namespace Mantid {
-namespace DataHandling {
-namespace {
-// Get a reference to the logger
-Kernel::Logger g_log("SNSDataArchive");
-/// Base url for restful web survice
-const std::string
-    BASE_URL("http://icat.sns.gov:2080/icat-rest-ws/datafile/filename/");
-} // namespace
-
-DECLARE_ARCHIVESEARCH(SNSDataArchive, SNSDataSearch)
-
-/**
- * @param filenames : List of files to search
- * @param exts : List of extensions to check against
- * @return list of archive locations
- */
-std::string
-SNSDataArchive::getArchivePath(const std::set<std::string> &filenames,
-                               const std::vector<std::string> &exts) const {
-  g_log.debug() << "getArchivePath([ ";
-  for (const auto &iter : filenames)
-    g_log.debug() << iter << " ";
-  g_log.information() << "], [ ";
-  for (const auto &iter : exts)
-    g_log.debug() << iter << " ";
-  g_log.debug() << "])\n";
-
-  auto iter = filenames.cbegin();
-  std::string filename = *iter;
-
-  // ICAT4 web service take upper case filename such as HYSA_2662
-  std::transform(filename.begin(), filename.end(), filename.begin(), toupper);
-
-  for (const auto &ext : exts) {
-    g_log.debug() << ext << ";";
-  }
-  g_log.debug() << "\n";
-
-  const std::string URL(BASE_URL + filename);
-  g_log.debug() << "URL: " << URL << "\n";
-
-  Kernel::InternetHelper inetHelper;
-  std::ostringstream rs;
-
-  int status = inetHelper.sendRequest(URL, rs);
-
-  // Create a DOM document from the response.
-  Poco::XML::DOMParser parser;
-  std::istringstream istrsource(rs.str());
-  Poco::XML::InputSource source(istrsource);
-  Poco::AutoPtr<Poco::XML::Document> pDoc = parser.parse(&source);
-
-  std::vector<std::string> locations;
-
-  // Everything went fine, return the XML document.
-  // Otherwise look for an error message in the XML document.
-  if (status == Kernel::InternetHelper::HTTP_OK) {
-    std::string location;
-    Poco::AutoPtr<Poco::XML::NodeList> pList =
-        pDoc->getElementsByTagName("location");
-    for (unsigned long i = 0; i < pList->length(); i++) {
-      location = pList->item(i)->innerText();
-      g_log.debug() << "location: " << location << "\n";
-      locations.push_back(location);
-    }
-  }
-
-  for (const auto &ext : exts) {
-    std::string datafile = filename + ext;
-    std::vector<std::string>::const_iterator iter = locations.begin();
-    for (; iter != locations.end(); ++iter) {
-      if (boost::algorithm::ends_with((*iter), datafile)) {
-        return *iter;
-      } // end if
-    }   // end for iter
-
-  } // end for ext
-  return "";
-}
-
-} // namespace DataHandling
-} // namespace Mantid
diff --git a/Framework/DataHandling/test/LoadDetectorsGroupingFileTest.h b/Framework/DataHandling/test/LoadDetectorsGroupingFileTest.h
index 3df375aa8c0de8a7676a81a10b80c47372bd3a06..915aefc77d284bd554b9c7510978b9215f175844 100644
--- a/Framework/DataHandling/test/LoadDetectorsGroupingFileTest.h
+++ b/Framework/DataHandling/test/LoadDetectorsGroupingFileTest.h
@@ -372,8 +372,8 @@ public:
     TS_ASSERT(load.setProperty("InputFile", file.getFileName()));
     TS_ASSERT(load.setProperty("OutputWorkspace", ws));
 
-    std::string errorMsg =
-        "Bad number of spectra list in " + file.getFileName() + " on line 4";
+    std::string errorMsg = "Bad number of spectra list in \"" +
+                           file.getFileName() + "\" on line 4";
 
     TS_ASSERT_THROWS_EQUALS(load.execute(), const Exception::ParseError &e,
                             e.what(), errorMsg);
@@ -398,8 +398,9 @@ public:
     TS_ASSERT(load.setProperty("InputFile", file.getFileName()));
     TS_ASSERT(load.setProperty("OutputWorkspace", ws));
 
-    std::string errorMsg = "Premature end of file, expecting spectra list in " +
-                           file.getFileName() + " on line 4";
+    std::string errorMsg =
+        "Premature end of file, expecting spectra list in \"" +
+        file.getFileName() + "\" on line 4";
 
     TS_ASSERT_THROWS_EQUALS(load.execute(), const Exception::ParseError &e,
                             e.what(), errorMsg);
@@ -426,8 +427,8 @@ public:
     TS_ASSERT(load.setProperty("OutputWorkspace", ws));
 
     std::string errorMsg =
-        "Expected a single int for the number of group spectra in " +
-        file.getFileName() + " on line 3";
+        "Expected a single int for the number of group spectra in \"" +
+        file.getFileName() + "\" on line 3";
 
     TS_ASSERT_THROWS_EQUALS(load.execute(), const Exception::ParseError &e,
                             e.what(), errorMsg);
diff --git a/Framework/DataHandling/test/LoadInstrumentTest.h b/Framework/DataHandling/test/LoadInstrumentTest.h
index 017c36b6653da182ed84bf5a534a07f9ca12bb82..7522db4caa6125999ad0e382ed123655b4462aad 100644
--- a/Framework/DataHandling/test/LoadInstrumentTest.h
+++ b/Framework/DataHandling/test/LoadInstrumentTest.h
@@ -665,7 +665,7 @@ public:
         instLoader.execute(), Kernel::Exception::FileError & e,
         std::string(e.what()),
         "Either the InstrumentName or Filename property of LoadInstrument "
-        "must be specified to load an instrument in ");
+        "must be specified to load an instrument in \"\"");
     TS_ASSERT(!instLoader.isExecuted());
     TS_ASSERT_EQUALS(instLoader.getPropertyValue("Filename"), "");
   }
diff --git a/Framework/DataHandling/test/ReadMaterialTest.h b/Framework/DataHandling/test/ReadMaterialTest.h
index 59a1109bf431ea2ad543c02c70ef8d0bbb92400e..2f7158665844d8c141e91dcbccbced4fbd646166 100644
--- a/Framework/DataHandling/test/ReadMaterialTest.h
+++ b/Framework/DataHandling/test/ReadMaterialTest.h
@@ -34,7 +34,7 @@ public:
   }
 
   void testSuccessfullValidateInputsAtomicNumber() {
-    const ReadMaterial::MaterialParameters params = [this]() -> auto {
+    const ReadMaterial::MaterialParameters params = []() -> auto {
       ReadMaterial::MaterialParameters setMaterial;
       setMaterial.atomicNumber = 1;
       setMaterial.massNumber = 1;
@@ -64,7 +64,7 @@ public:
   }
 
   void testFailureValidateInputsNoMaterial() {
-    const ReadMaterial::MaterialParameters params = [this]() -> auto {
+    const ReadMaterial::MaterialParameters params = []() -> auto {
       ReadMaterial::MaterialParameters setMaterial;
       setMaterial.atomicNumber = 0;
       setMaterial.massNumber = 0;
@@ -78,7 +78,7 @@ public:
   }
 
   void testSuccessfullValidateInputsSampleNumber() {
-    const ReadMaterial::MaterialParameters params = [this]() -> auto {
+    const ReadMaterial::MaterialParameters params = []() -> auto {
       ReadMaterial::MaterialParameters setMaterial;
       setMaterial.atomicNumber = 1;
       setMaterial.massNumber = 1;
@@ -93,7 +93,7 @@ public:
   }
 
   void testSuccessfullValidateInputsZParam() {
-    const ReadMaterial::MaterialParameters params = [this]() -> auto {
+    const ReadMaterial::MaterialParameters params = []() -> auto {
       ReadMaterial::MaterialParameters setMaterial;
       setMaterial.atomicNumber = 1;
       setMaterial.massNumber = 1;
@@ -109,7 +109,7 @@ public:
   }
 
   void testSuccessfullValidateInputsSampleMass() {
-    const ReadMaterial::MaterialParameters params = [this]() -> auto {
+    const ReadMaterial::MaterialParameters params = []() -> auto {
       ReadMaterial::MaterialParameters setMaterial;
       setMaterial.atomicNumber = 1;
       setMaterial.massNumber = 1;
@@ -124,7 +124,7 @@ public:
   }
 
   void testFailureValidateInputsSampleNumberAndZParam() {
-    const ReadMaterial::MaterialParameters params = [this]() -> auto {
+    const ReadMaterial::MaterialParameters params = []() -> auto {
       ReadMaterial::MaterialParameters setMaterial;
       setMaterial.atomicNumber = 1;
       setMaterial.massNumber = 1;
@@ -142,7 +142,7 @@ public:
   }
 
   void testFailureValidateInputsZParamWithSampleMass() {
-    const ReadMaterial::MaterialParameters params = [this]() -> auto {
+    const ReadMaterial::MaterialParameters params = []() -> auto {
       ReadMaterial::MaterialParameters setMaterial;
       setMaterial.atomicNumber = 1;
       setMaterial.massNumber = 1;
@@ -160,7 +160,7 @@ public:
   }
 
   void testFailureValidateInputsZParamWithoutUnitCell() {
-    const ReadMaterial::MaterialParameters params = [this]() -> auto {
+    const ReadMaterial::MaterialParameters params = []() -> auto {
       ReadMaterial::MaterialParameters setMaterial;
       setMaterial.atomicNumber = 1;
       setMaterial.massNumber = 1;
@@ -176,7 +176,7 @@ public:
   }
 
   void testFailureValidateInputsSampleNumWithSampleMass() {
-    const ReadMaterial::MaterialParameters params = [this]() -> auto {
+    const ReadMaterial::MaterialParameters params = []() -> auto {
       ReadMaterial::MaterialParameters setMaterial;
       setMaterial.atomicNumber = 1;
       setMaterial.massNumber = 1;
@@ -290,4 +290,4 @@ private:
                      materialFormula[0].multiplicity);
     TS_ASSERT_EQUALS(checkFormula.size(), materialFormula.size())
   }
-};
\ No newline at end of file
+};
diff --git a/Framework/DataHandling/test/SNSDataArchiveTest.h b/Framework/DataHandling/test/SNSDataArchiveTest.h
deleted file mode 100644
index 3d8d7d588b44351e168cbd82760b51852ebcf6e4..0000000000000000000000000000000000000000
--- a/Framework/DataHandling/test/SNSDataArchiveTest.h
+++ /dev/null
@@ -1,61 +0,0 @@
-// Mantid Repository : https://github.com/mantidproject/mantid
-//
-// Copyright &copy; 2018 ISIS Rutherford Appleton Laboratory UKRI,
-//     NScD Oak Ridge National Laboratory, European Spallation Source
-//     & Institut Laue - Langevin
-// SPDX - License - Identifier: GPL - 3.0 +
-#ifndef SNSDATAARCHIVETEST_H_
-#define SNSDATAARCHIVETEST_H_
-
-#include <cxxtest/TestSuite.h>
-
-#include "MantidAPI/ArchiveSearchFactory.h"
-#include "MantidDataHandling/SNSDataArchive.h"
-
-using namespace Mantid::DataHandling;
-using namespace Mantid::API;
-
-class SNSDataArchiveTest : public CxxTest::TestSuite {
-public:
-  void xtestSearch() {
-    SNSDataArchive arch;
-
-    // PG3 Test case
-    std::set<std::string> filename;
-    filename.insert("PG3_7390");
-    std::vector<std::string> extension =
-        std::vector<std::string>(1, "_event.nxs");
-    std::string path = arch.getArchivePath(filename, extension);
-    TS_ASSERT_EQUALS(path,
-                     "/SNS/PG3/IPTS-2767/0/7390/NeXus/PG3_7390_histo.nxs");
-
-    // BSS Test case
-    filename.clear();
-    filename.insert("BSS_18339");
-    path = arch.getArchivePath(filename, extension);
-    TS_ASSERT_EQUALS(path,
-                     "/SNS/BSS/IPTS-6817/0/18339/NeXus/BSS_18339_event.nxs");
-
-    // HYSA Test case
-    filename.clear();
-    filename.insert("HYSA_2411");
-    extension = std::vector<std::string>(1, ".nxs.h5");
-    path = arch.getArchivePath(filename, extension);
-    TS_ASSERT_EQUALS(path, "/SNS/HYSA/IPTS-8004/nexus/HYSA_2411.nxs.h5");
-
-    // Test a non-existent file
-    filename.clear();
-    filename.insert("mybeamline_666");
-    extension = std::vector<std::string>(1, ".nxs");
-    path = arch.getArchivePath(filename, extension);
-    TS_ASSERT(path.empty());
-  }
-
-  void testFactory() {
-    boost::shared_ptr<IArchiveSearch> arch =
-        ArchiveSearchFactory::Instance().create("SNSDataSearch");
-    TS_ASSERT(arch);
-  }
-};
-
-#endif /*SNSDATAARCHIVETEST_H_*/
diff --git a/Framework/Kernel/src/Exception.cpp b/Framework/Kernel/src/Exception.cpp
index 6d6f56528af921bdb6e7cdbc58362858607a91cc..5919ced3d8c3c19fcb85063ba4c85793b09606cf 100644
--- a/Framework/Kernel/src/Exception.cpp
+++ b/Framework/Kernel/src/Exception.cpp
@@ -19,7 +19,8 @@ namespace Exception {
 */
 FileError::FileError(const std::string &Desc, const std::string &FName)
     : std::runtime_error(Desc), fileName(FName) {
-  outMessage = std::string(std::runtime_error::what()) + " in " + fileName;
+  outMessage =
+      std::string(std::runtime_error::what()) + " in \"" + fileName + "\"";
 }
 
 /// Copy constructor
diff --git a/Framework/PythonInterface/inc/MantidPythonInterface/api/AnalysisDataServiceObserverAdapter.h b/Framework/PythonInterface/inc/MantidPythonInterface/api/AnalysisDataServiceObserverAdapter.h
new file mode 100644
index 0000000000000000000000000000000000000000..ff96d44dc8f5706627964ee49c7be9896aaca28d
--- /dev/null
+++ b/Framework/PythonInterface/inc/MantidPythonInterface/api/AnalysisDataServiceObserverAdapter.h
@@ -0,0 +1,58 @@
+// Mantid Repository : https://github.com/mantidproject/mantid
+//
+// Copyright &copy; 2013 ISIS Rutherford Appleton Laboratory UKRI,
+//     NScD Oak Ridge National Laboratory, European Spallation Source
+//     & Institut Laue - Langevin
+// SPDX - License - Identifier: GPL - 3.0 +
+#ifndef MANTID_PYTHONINTERFACE_ANALYSISDATASERVICEOBSERVERADAPTER_H_
+#define MANTID_PYTHONINTERFACE_ANALYSISDATASERVICEOBSERVERADAPTER_H_
+
+#include "MantidAPI/AnalysisDataServiceObserver.h"
+#include <boost/python/wrapper.hpp>
+
+namespace Mantid {
+namespace PythonInterface {
+
+/**
+A wrapper class helping to export AnalysisDataServiceObserver to python.
+It provides access from the C++ side to methods defined in python
+on subclasses of AnalysisDataServiceObserver.
+This allows the virtual methods to be overriden by python subclasses.
+ */
+
+class DLLExport AnalysisDataServiceObserverAdapter
+    : public API::AnalysisDataServiceObserver {
+public:
+  explicit AnalysisDataServiceObserverAdapter(PyObject *self);
+  AnalysisDataServiceObserverAdapter(
+      const AnalysisDataServiceObserverAdapter &) = delete;
+  AnalysisDataServiceObserverAdapter &
+  operator=(const AnalysisDataServiceObserverAdapter &) = delete;
+
+  void anyChangeHandle() override;
+  void addHandle(const std::string &wsName, const Workspace_sptr &ws) override;
+  void replaceHandle(const std::string &wsName,
+                     const Workspace_sptr &ws) override;
+  void deleteHandle(const std::string &wsName,
+                    const Workspace_sptr &ws) override;
+  void clearHandle() override;
+  void renameHandle(const std::string &wsName,
+                    const std::string &newName) override;
+  void groupHandle(const std::string &wsName,
+                   const Workspace_sptr &ws) override;
+  void unGroupHandle(const std::string &wsName,
+                     const Workspace_sptr &ws) override;
+  void groupUpdateHandle(const std::string &wsName,
+                         const Workspace_sptr &ws) override;
+
+private:
+  /// Return the PyObject that owns this wrapper, i.e. self
+  inline PyObject *getSelf() const { return m_self; }
+  /// Value of "self" used by python to refer to an instance of this class
+  PyObject *m_self;
+};
+
+} // namespace PythonInterface
+} // namespace Mantid
+
+#endif /*MANTID_PYTHONINTERFACE_ANALYSISDATASERVICEOBSERVERADAPTER_H_*/
\ No newline at end of file
diff --git a/Framework/PythonInterface/mantid/api/CMakeLists.txt b/Framework/PythonInterface/mantid/api/CMakeLists.txt
index 0555fbc2c9f40f3c29ad3d37d03b077a572977c5..fb4911e15d17e25b6b1e441769adb3a0c26e0294 100644
--- a/Framework/PythonInterface/mantid/api/CMakeLists.txt
+++ b/Framework/PythonInterface/mantid/api/CMakeLists.txt
@@ -81,6 +81,7 @@ set ( EXPORT_FILES
   src/Exports/SpectrumInfoItem.cpp
   src/Exports/SpectrumInfoIterator.cpp
   src/Exports/SpectrumInfoPythonIterator.cpp
+  src/Exports/AnalysisDataServiceObserver.cpp
 )
 
 set ( MODULE_DEFINITION ${CMAKE_CURRENT_BINARY_DIR}/api.cpp )
@@ -99,6 +100,7 @@ set ( SRC_FILES
   src/PythonAlgorithm/DataProcessorAdapter.cpp
   src/CloneMatrixWorkspace.cpp
   src/ExtractWorkspace.cpp
+  src/Exports/AnalysisDataServiceObserverAdapter.cpp
 )
 
 set ( INC_FILES
@@ -109,6 +111,7 @@ set ( INC_FILES
   ${HEADER_DIR}/api/FitFunctions/IPeakFunctionAdapter.h
   ${HEADER_DIR}/api/PythonAlgorithm/AlgorithmAdapter.h
   ${HEADER_DIR}/api/PythonAlgorithm/DataProcessorAdapter.h
+  ${HEADER_DIR}/api/AnalysisDataServiceObserverAdapter.h
   ${HEADER_DIR}/api/BinaryOperations.h
   ${HEADER_DIR}/api/CloneMatrixWorkspace.h
   ${HEADER_DIR}/api/ExtractWorkspace.h
diff --git a/Framework/PythonInterface/mantid/api/src/Exports/AlgorithmObserver.cpp b/Framework/PythonInterface/mantid/api/src/Exports/AlgorithmObserver.cpp
index ac33395dcd6cf409acead24c46f1ce8aff625f71..3c70f0f155b9d874be80191f507db11a9f05ada4 100644
--- a/Framework/PythonInterface/mantid/api/src/Exports/AlgorithmObserver.cpp
+++ b/Framework/PythonInterface/mantid/api/src/Exports/AlgorithmObserver.cpp
@@ -9,7 +9,6 @@
 
 #include <boost/python/class.hpp>
 #include <boost/python/register_ptr_to_python.hpp>
-#include <iostream>
 
 using namespace Mantid::API;
 using namespace Mantid::PythonInterface;
diff --git a/Framework/PythonInterface/mantid/api/src/Exports/AnalysisDataService.cpp b/Framework/PythonInterface/mantid/api/src/Exports/AnalysisDataService.cpp
index 23213e94b6b6e0792f1d055be77f1737a44d3f35..bd12d58ac65f4f04f19695dad1f800c961871e71 100644
--- a/Framework/PythonInterface/mantid/api/src/Exports/AnalysisDataService.cpp
+++ b/Framework/PythonInterface/mantid/api/src/Exports/AnalysisDataService.cpp
@@ -77,5 +77,11 @@ void export_AnalysisDataService() {
       .def("retrieveWorkspaces", retrieveWorkspaces,
            AdsRetrieveWorkspacesOverloads(
                "Retrieve a list of workspaces by name",
-               (arg("self"), arg("names"), arg("unrollGroups") = false)));
+               (arg("self"), arg("names"), arg("unrollGroups") = false)))
+      .def("addToGroup", &AnalysisDataServiceImpl::addToGroup,
+           (arg("groupName"), arg("wsName")),
+           "Add a workspace in the ADS to a group in the ADS")
+      .def("removeFromGroup", &AnalysisDataServiceImpl::removeFromGroup,
+           (arg("groupName"), arg("wsName")),
+           "Remove a workspace from a group in the ADS");
 }
diff --git a/Framework/PythonInterface/mantid/api/src/Exports/AnalysisDataServiceObserver.cpp b/Framework/PythonInterface/mantid/api/src/Exports/AnalysisDataServiceObserver.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..e19291316cf7fdb49e8051c87770d0e101aff580
--- /dev/null
+++ b/Framework/PythonInterface/mantid/api/src/Exports/AnalysisDataServiceObserver.cpp
@@ -0,0 +1,55 @@
+// Mantid Repository : https://github.com/mantidproject/mantid
+//
+// Copyright &copy; 2007 ISIS Rutherford Appleton Laboratory UKRI,
+//     NScD Oak Ridge National Laboratory, European Spallation Source
+//     & Institut Laue - Langevin
+// SPDX - License - Identifier: GPL - 3.0 +
+#include "MantidAPI/AnalysisDataServiceObserver.h"
+#include "MantidPythonInterface/api/AnalysisDataServiceObserverAdapter.h"
+#include "MantidPythonInterface/kernel/GetPointer.h"
+
+#include <boost/python/bases.hpp>
+#include <boost/python/class.hpp>
+
+using namespace Mantid::API;
+using namespace Mantid::PythonInterface;
+using namespace boost::python;
+
+void export_AnalysisDataServiceObserver() {
+  boost::python::class_<AnalysisDataServiceObserver, bases<>,
+                        AnalysisDataServiceObserverAdapter, boost::noncopyable>(
+      "AnalysisDataServiceObserver",
+      "Observes AnalysisDataService notifications: all only")
+      .def("observeAll", &AnalysisDataServiceObserverAdapter::observeAll,
+           (arg("self"), arg("on")),
+           "Observe AnalysisDataService for any changes")
+      .def("observeAdd", &AnalysisDataServiceObserverAdapter::observeAdd,
+           (arg("self"), arg("on")),
+           "Observe AnalysisDataService for a workspace being added")
+      .def("observeReplace",
+           &AnalysisDataServiceObserverAdapter::observeReplace,
+           (arg("self"), arg("on")),
+           "Observe AnalysisDataService for a workspace being replaced")
+      .def("observeDelete", &AnalysisDataServiceObserverAdapter::observeDelete,
+           (arg("self"), arg("on")),
+           "Observe AnalysisDataService for a workspace being deleted")
+      .def("observeClear", &AnalysisDataServiceObserverAdapter::observeClear,
+           (arg("self"), arg("on")),
+           "Observe AnalysisDataService for it being cleared")
+      .def("observeRename", &AnalysisDataServiceObserverAdapter::observeRename,
+           (arg("self"), arg("on")),
+           "Observe AnalysisDataService for a workspace being renamed")
+      .def(
+          "observeGroup", &AnalysisDataServiceObserverAdapter::observeGroup,
+          (arg("self"), arg("on")),
+          "Observe AnalysisDataService for a group being added/made in the ADS")
+      .def("observeUnGroup",
+           &AnalysisDataServiceObserverAdapter::observeUnGroup,
+           (arg("self"), arg("on")),
+           "Observe AnalysisDataService for a group being removed from the ADS")
+      .def("observeGroupUpdate",
+           &AnalysisDataServiceObserverAdapter::observeGroupUpdate,
+           (arg("self"), arg("on")),
+           "Observe AnalysisDataService for a group being updated by being "
+           "added to or removed from");
+}
\ No newline at end of file
diff --git a/Framework/PythonInterface/mantid/api/src/Exports/AnalysisDataServiceObserverAdapter.cpp b/Framework/PythonInterface/mantid/api/src/Exports/AnalysisDataServiceObserverAdapter.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..d610d38daced038a523c373f20cfd35b5deecffe
--- /dev/null
+++ b/Framework/PythonInterface/mantid/api/src/Exports/AnalysisDataServiceObserverAdapter.cpp
@@ -0,0 +1,98 @@
+// Mantid Repository : https://github.com/mantidproject/mantid
+//
+// Copyright &copy; 2018 ISIS Rutherford Appleton Laboratory UKRI,
+//     NScD Oak Ridge National Laboratory, European Spallation Source
+//     & Institut Laue - Langevin
+// SPDX - License - Identifier: GPL - 3.0 +
+#include "MantidPythonInterface/api/AnalysisDataServiceObserverAdapter.h"
+#include "MantidAPI/AnalysisDataServiceObserver.h"
+#include "MantidPythonInterface/core/CallMethod.h"
+
+namespace Mantid {
+namespace PythonInterface {
+
+AnalysisDataServiceObserverAdapter::AnalysisDataServiceObserverAdapter(
+    PyObject *self)
+    : API::AnalysisDataServiceObserver(), m_self(self) {}
+
+void AnalysisDataServiceObserverAdapter::anyChangeHandle() {
+  try {
+    return callMethod<void>(getSelf(), "anyChangeHandle");
+  } catch (UndefinedAttributeError &) {
+    return;
+  }
+}
+
+void AnalysisDataServiceObserverAdapter::addHandle(const std::string &wsName,
+                                                   const Workspace_sptr &ws) {
+  try {
+    return callMethod<void>(getSelf(), "addHandle", wsName, ws);
+  } catch (UndefinedAttributeError &) {
+    return;
+  }
+}
+
+void AnalysisDataServiceObserverAdapter::replaceHandle(
+    const std::string &wsName, const Workspace_sptr &ws) {
+  try {
+    return callMethod<void>(getSelf(), "replaceHandle", wsName, ws);
+  } catch (UndefinedAttributeError &) {
+    return;
+  }
+}
+
+void AnalysisDataServiceObserverAdapter::deleteHandle(
+    const std::string &wsName, const Workspace_sptr &ws) {
+  try {
+    return callMethod<void>(getSelf(), "deleteHandle", wsName, ws);
+  } catch (UndefinedAttributeError &) {
+    return;
+  }
+}
+
+void AnalysisDataServiceObserverAdapter::clearHandle() {
+  try {
+    return callMethod<void>(getSelf(), "clearHandle");
+  } catch (UndefinedAttributeError &) {
+    return;
+  }
+}
+
+void AnalysisDataServiceObserverAdapter::renameHandle(
+    const std::string &wsName, const std::string &newName) {
+  try {
+    return callMethod<void>(getSelf(), "renameHandle", wsName, newName);
+  } catch (UndefinedAttributeError &) {
+    return;
+  }
+}
+
+void AnalysisDataServiceObserverAdapter::groupHandle(const std::string &wsName,
+                                                     const Workspace_sptr &ws) {
+  try {
+    return callMethod<void>(getSelf(), "groupHandle", wsName, ws);
+  } catch (UndefinedAttributeError &) {
+    return;
+  }
+}
+
+void AnalysisDataServiceObserverAdapter::unGroupHandle(
+    const std::string &wsName, const Workspace_sptr &ws) {
+  try {
+    return callMethod<void>(getSelf(), "unGroupHandle", wsName, ws);
+  } catch (UndefinedAttributeError &) {
+    return;
+  }
+}
+
+void AnalysisDataServiceObserverAdapter::groupUpdateHandle(
+    const std::string &wsName, const Workspace_sptr &ws) {
+  try {
+    return callMethod<void>(getSelf(), "groupUpdateHandle", wsName, ws);
+  } catch (UndefinedAttributeError &) {
+    return;
+  }
+}
+
+} // namespace PythonInterface
+} // namespace Mantid
diff --git a/Framework/PythonInterface/plugins/algorithms/AlignAndFocusPowderFromFiles.py b/Framework/PythonInterface/plugins/algorithms/AlignAndFocusPowderFromFiles.py
index 718e5a7e6cddc3bc9805f95bb20351c94c9e21c8..af00e21923bef09c0e05361f9bf3ab8570bb0365 100644
--- a/Framework/PythonInterface/plugins/algorithms/AlignAndFocusPowderFromFiles.py
+++ b/Framework/PythonInterface/plugins/algorithms/AlignAndFocusPowderFromFiles.py
@@ -128,7 +128,9 @@ class AlignAndFocusPowderFromFiles(DistributedDataProcessorAlgorithm):
         args = {}
         for name in PROPS_FOR_ALIGN:
             prop = self.getProperty(name)
-            if name == 'PreserveEvents' or not prop.isDefault:
+            name_list = ['PreserveEvents', 'CompressTolerance',
+                         'CompressWallClockTolerance', 'CompressStartTime']
+            if name in name_list or not prop.isDefault:
                 if 'Workspace' in name:
                     args[name] = prop.valueAsStr
                 else:
@@ -269,8 +271,11 @@ class AlignAndFocusPowderFromFiles(DistributedDataProcessorAlgorithm):
                          startProgress=prog_start, endProgress=prog_start + prog_per_chunk_step)
                     DeleteWorkspace(Workspace=unfocusname_chunk)
 
-                if self.kwargs['PreserveEvents']:
-                    CompressEvents(InputWorkspace=wkspname, OutputWorkspace=wkspname)
+                if self.kwargs['PreserveEvents'] and self.kwargs['CompressTolerance'] > 0.:
+                    CompressEvents(InputWorkspace=wkspname, OutputWorkspace=wkspname,
+                                   WallClockTolerance=self.kwargs['CompressWallClockTolerance'],
+                                   Tolerance= self.kwargs['CompressTolerance'],
+                                   StartTime=self.kwargs['CompressStartTime'])
         # end of inner loop
 
     def PyExec(self):
@@ -353,8 +358,11 @@ class AlignAndFocusPowderFromFiles(DistributedDataProcessorAlgorithm):
                          ClearRHSWorkspace=self.kwargs['PreserveEvents'])
                     DeleteWorkspace(Workspace=unfocusname_file)
 
-                if self.kwargs['PreserveEvents']:
-                    CompressEvents(InputWorkspace=finalname, OutputWorkspace=finalname)
+                if self.kwargs['PreserveEvents'] and self.kwargs['CompressTolerance'] > 0.:
+                    CompressEvents(InputWorkspace=finalname, OutputWorkspace=finalname,
+                                   WallClockTolerance=self.kwargs['CompressWallClockTolerance'],
+                                   Tolerance= self.kwargs['CompressTolerance'],
+                                   StartTime=self.kwargs['CompressStartTime'])
                     # not compressing unfocussed workspace because it is in d-spacing
                     # and is likely to be from a different part of the instrument
 
diff --git a/Framework/PythonInterface/test/python/mantid/api/AnalysisDataServiceObserverTest.py b/Framework/PythonInterface/test/python/mantid/api/AnalysisDataServiceObserverTest.py
new file mode 100644
index 0000000000000000000000000000000000000000..d78bc85ca65a05f10182228a4ba7dab12009dab5
--- /dev/null
+++ b/Framework/PythonInterface/test/python/mantid/api/AnalysisDataServiceObserverTest.py
@@ -0,0 +1,248 @@
+# Mantid Repository : https://github.com/mantidproject/mantid
+#
+# Copyright &copy; 2017 ISIS Rutherford Appleton Laboratory UKRI,
+#     NScD Oak Ridge National Laboratory, European Spallation Source
+#     & Institut Laue - Langevin
+# SPDX - License - Identifier: GPL - 3.0 +
+#  This file is part of the mantidqt package
+#
+from __future__ import (absolute_import, division, print_function, unicode_literals)
+
+import unittest
+import sys
+
+from mantid.api import AnalysisDataService as ADS, AnalysisDataServiceObserver
+from mantid.simpleapi import CreateSampleWorkspace, RenameWorkspace, GroupWorkspaces, UnGroupWorkspace, DeleteWorkspace
+
+if sys.version_info.major == 3:
+    from unittest import mock
+else:
+    import mock
+
+
+class FakeADSObserver(AnalysisDataServiceObserver):
+    # These methods are going to be mocked out but needs to be present to actually get the hook from the C++ side
+    def anyChangeHandle(self):
+        pass
+
+    def addHandle(self, wsName, ws):
+        pass
+
+    def replaceHandle(self, wsName, ws):
+        pass
+
+    def deleteHandle(self, wsName, ws):
+        pass
+
+    def clearHandle(self):
+        pass
+
+    def renameHandle(self, wsName, newName):
+        pass
+
+    def groupHandle(self, wsName, ws):
+        pass
+
+    def unGroupHandle(self, wsName, ws):
+        pass
+
+    def groupUpdateHandle(self, wsName, ws):
+        pass
+
+
+class AnalysisDataServiceObserverTest(unittest.TestCase):
+    def setUp(self):
+        self.fake_class = FakeADSObserver()
+        self.fake_class.anyChangeHandle = mock.MagicMock()
+
+    def tearDown(self):
+        self.fake_class.observeAll(False)
+        ADS.clear()
+
+    def test_observeAll_calls_anyChangeHandle_when_set_on_ads_add(self):
+        self.fake_class.observeAll(True)
+        CreateSampleWorkspace(OutputWorkspace="ws")
+        self.assertEqual(self.fake_class.anyChangeHandle.call_count, 1)
+
+    def test_observeAll_calls_anyChangeHandle_when_set_on_ads_replace(self):
+        self.fake_class.observeAll(True)
+        CreateSampleWorkspace(OutputWorkspace="ws")
+        expected_count = 1
+
+        # Will replace first workspace
+        CreateSampleWorkspace(OutputWorkspace="ws")
+        expected_count += 1
+
+        self.assertEqual(self.fake_class.anyChangeHandle.call_count, expected_count)
+
+    def test_observeAll_calls_anyChangeHandle_when_set_on_ads_delete(self):
+        self.fake_class.observeAll(True)
+        CreateSampleWorkspace(OutputWorkspace="ws")
+        expected_count = 1
+
+        # Will replace first workspace
+        DeleteWorkspace("ws")
+        expected_count += 1
+
+        self.assertEqual(self.fake_class.anyChangeHandle.call_count, expected_count)
+
+    def test_observeAll_calls_anyChangeHandle_when_set_on_ads_clear(self):
+        self.fake_class.observeAll(True)
+        CreateSampleWorkspace(OutputWorkspace="ws")
+        expected_count = 1
+
+        # Will replace first workspace
+        ADS.clear()
+        expected_count += 1
+
+        self.assertEqual(self.fake_class.anyChangeHandle.call_count, expected_count)
+
+    def test_observeAll_calls_anyChangeHandle_when_set_on_ads_rename(self):
+        self.fake_class.observeAll(True)
+        CreateSampleWorkspace(OutputWorkspace="ws")
+        expected_count = 1
+
+        # Will replace first workspace
+        RenameWorkspace(InputWorkspace="ws", OutputWorkspace="ws1")
+        # One for the rename
+        expected_count += 1
+        # One for replacing original named workspace
+        expected_count += 1
+
+        self.assertEqual(self.fake_class.anyChangeHandle.call_count, expected_count)
+
+    def test_observeAll_calls_anyChangeHandle_when_set_on_ads_group_made(self):
+        self.fake_class.observeAll(True)
+        CreateSampleWorkspace(OutputWorkspace="ws1")
+        expected_count = 1
+        CreateSampleWorkspace(OutputWorkspace="ws2")
+        expected_count += 1
+
+        # Will replace first workspace
+        GroupWorkspaces(InputWorkspaces="ws1,ws2", OutputWorkspace="NewGroup")
+        # One for grouping the workspace
+        expected_count += 1
+        # One for adding it to the ADS
+        expected_count += 1
+
+        self.assertEqual(self.fake_class.anyChangeHandle.call_count, expected_count)
+
+    def test_observeAll_calls_anyChangeHandle_when_set_on_ads_ungroup_performed(self):
+        self.fake_class.observeAll(True)
+        CreateSampleWorkspace(OutputWorkspace="ws1")
+        expected_count = 1
+        CreateSampleWorkspace(OutputWorkspace="ws2")
+        expected_count += 1
+        GroupWorkspaces(InputWorkspaces="ws1,ws2", OutputWorkspace="NewGroup")
+        # One for grouping the workspace
+        expected_count += 1
+        # One for adding it to the ADS
+        expected_count += 1
+
+        # Will replace first workspace
+        UnGroupWorkspace(InputWorkspace="NewGroup")
+        # One for ungrouping the workspace
+        expected_count += 1
+        # One for removing the grouped workspace object from the ADS
+        expected_count += 1
+
+        self.assertEqual(self.fake_class.anyChangeHandle.call_count, expected_count)
+
+    def test_observeAll_calls_anyChangeHandle_when_set_on_ads_group_updated(self):
+        self.fake_class.observeAll(True)
+        CreateSampleWorkspace(OutputWorkspace="ws1")
+        expected_count = 1
+        CreateSampleWorkspace(OutputWorkspace="ws2")
+        expected_count += 1
+        CreateSampleWorkspace(OutputWorkspace="ws3")
+        expected_count += 1
+        GroupWorkspaces(InputWorkspaces="ws1,ws2", OutputWorkspace="NewGroup")
+        # One for grouping the workspace
+        expected_count += 1
+        # One for adding it to the ADS
+        expected_count += 1
+
+        # Will update group
+        ADS.addToGroup("NewGroup", "ws3")
+        expected_count += 1
+
+        self.assertEqual(self.fake_class.anyChangeHandle.call_count, expected_count)
+
+    def test_observeAdd_calls_addHandle_when_set_on_ads_and_a_workspace_is_added(self):
+        self.fake_class.observeAdd(True)
+        self.fake_class.addHandle = mock.MagicMock()
+        CreateSampleWorkspace(OutputWorkspace="ws")
+        self.assertEqual(self.fake_class.addHandle.call_count, 1)
+
+    def test_observeReplace_calls_replaceHandle_when_set_on_ads_and_a_workspace_is_replaced(self):
+        CreateSampleWorkspace(OutputWorkspace="ws")
+
+        self.fake_class.observeReplace(True)
+        self.fake_class.replaceHandle = mock.MagicMock()
+        CreateSampleWorkspace(OutputWorkspace="ws")
+
+        self.assertEqual(self.fake_class.replaceHandle.call_count, 1)
+
+    def test_observeDelete_calls_deleteHandle_when_set_on_ads_and_a_workspace_is_deleted(self):
+        CreateSampleWorkspace(OutputWorkspace="ws")
+
+        self.fake_class.observeDelete(True)
+        self.fake_class.deleteHandle = mock.MagicMock()
+        ADS.remove("ws")
+
+        self.assertEqual(self.fake_class.deleteHandle.call_count, 1)
+
+    def test_observeClear_calls_clearHandle_when_set_on_ads_its_cleared(self):
+        CreateSampleWorkspace(OutputWorkspace="ws")
+
+        self.fake_class.observeClear(True)
+        self.fake_class.clearHandle = mock.MagicMock()
+        ADS.clear()
+
+        self.assertEqual(self.fake_class.clearHandle.call_count, 1)
+
+    def test_observeRename_calls_renameHandle_when_set_on_ads_and_a_workspace_is_renamed(self):
+        CreateSampleWorkspace(OutputWorkspace="ws")
+
+        self.fake_class.observeRename(True)
+        self.fake_class.renameHandle = mock.MagicMock()
+        RenameWorkspace(InputWorkspace="ws", OutputWorkspace="ws1")
+
+        self.assertEqual(self.fake_class.renameHandle.call_count, 1)
+
+    def test_observeGroup_calls_groupHandle_when_set_on_ads_and_a_group_workspace_is_made(self):
+        CreateSampleWorkspace(OutputWorkspace="ws1")
+        CreateSampleWorkspace(OutputWorkspace="ws2")
+
+        self.fake_class.observeGroup(True)
+        self.fake_class.groupHandle = mock.MagicMock()
+        GroupWorkspaces(InputWorkspaces="ws1,ws2", OutputWorkspace="NewGroup")
+
+        self.assertEqual(self.fake_class.groupHandle.call_count, 1)
+
+    def test_observeUnGroup_calls_unGroupHandle_when_set_on_ads_and_a_group_is_ungrouped(self):
+        CreateSampleWorkspace(OutputWorkspace="ws1")
+        CreateSampleWorkspace(OutputWorkspace="ws2")
+        GroupWorkspaces(InputWorkspaces="ws1,ws2", OutputWorkspace="NewGroup")
+
+        self.fake_class.observeUnGroup(True)
+        self.fake_class.unGroupHandle = mock.MagicMock()
+        UnGroupWorkspace(InputWorkspace="NewGroup")
+
+        self.assertEqual(self.fake_class.unGroupHandle.call_count, 1)
+
+    def test_observeGroupUpdated_calls_groupUpdateHandle_when_set_on_ads_and_a_group_in_the_ads_is_updated(self):
+        CreateSampleWorkspace(OutputWorkspace="ws1")
+        CreateSampleWorkspace(OutputWorkspace="ws2")
+        CreateSampleWorkspace(OutputWorkspace="ws3")
+        GroupWorkspaces(InputWorkspaces="ws1,ws2", OutputWorkspace="NewGroup")
+
+        self.fake_class.observeGroupUpdate(True)
+        self.fake_class.groupUpdateHandle = mock.MagicMock()
+        ADS.addToGroup("NewGroup", "ws3")
+
+        self.assertEqual(self.fake_class.groupUpdateHandle.call_count, 1)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/Framework/PythonInterface/test/python/mantid/api/AnalysisDataServiceTest.py b/Framework/PythonInterface/test/python/mantid/api/AnalysisDataServiceTest.py
index e2dc256c8c2126e608e833eb8ecdae64645adb3c..801550ebf3a71d3c0f62f2162b395d187244a0b9 100644
--- a/Framework/PythonInterface/test/python/mantid/api/AnalysisDataServiceTest.py
+++ b/Framework/PythonInterface/test/python/mantid/api/AnalysisDataServiceTest.py
@@ -6,6 +6,7 @@
 # SPDX - License - Identifier: GPL - 3.0 +
 from __future__ import (absolute_import, division, print_function)
 
+import six
 import unittest
 from testhelpers import run_algorithm
 from mantid.api import (AnalysisDataService, AnalysisDataServiceImpl,
@@ -20,7 +21,7 @@ class AnalysisDataServiceTest(unittest.TestCase):
         FrameworkManagerImpl.Instance()
 
     def tearDown(self):
-      AnalysisDataService.Instance().clear()
+        AnalysisDataService.Instance().clear()
 
     def test_len_returns_correct_value(self):
         self.assertEquals(len(AnalysisDataService), 0)
@@ -166,5 +167,34 @@ class AnalysisDataServiceTest(unittest.TestCase):
         for name in extra_names:
             mtd.remove(name)
 
+    def test_addToGroup_adds_workspace_to_group(self):
+        from mantid.simpleapi import CreateSampleWorkspace, GroupWorkspaces
+        CreateSampleWorkspace(OutputWorkspace="ws1")
+        CreateSampleWorkspace(OutputWorkspace="ws2")
+        GroupWorkspaces(InputWorkspaces="ws1,ws2", OutputWorkspace="NewGroup")
+        CreateSampleWorkspace(OutputWorkspace="ws3")
+
+        AnalysisDataService.addToGroup("NewGroup", "ws3")
+
+        group = mtd['NewGroup']
+
+        self.assertEquals(group.size(), 3)
+        six.assertCountEqual(self, group.getNames(), ["ws1", "ws2", "ws3"])
+
+    def test_removeFromGroup_removes_workspace_from_group(self):
+        from mantid.simpleapi import CreateSampleWorkspace, GroupWorkspaces
+        CreateSampleWorkspace(OutputWorkspace="ws1")
+        CreateSampleWorkspace(OutputWorkspace="ws2")
+        CreateSampleWorkspace(OutputWorkspace="ws3")
+        GroupWorkspaces(InputWorkspaces="ws1,ws2,ws3", OutputWorkspace="NewGroup")
+
+        AnalysisDataService.removeFromGroup("NewGroup", "ws3")
+
+        group = mtd['NewGroup']
+
+        self.assertEquals(group.size(), 2)
+        six.assertCountEqual(self, group.getNames(), ["ws1", "ws2"])
+
+
 if __name__ == '__main__':
     unittest.main()
diff --git a/Framework/PythonInterface/test/python/mantid/api/CMakeLists.txt b/Framework/PythonInterface/test/python/mantid/api/CMakeLists.txt
index 6a6816959dea5cd0d8bf275861851bb10d97ea22..5edd860be0586d0451f3ba5c0e168bf23e673a42 100644
--- a/Framework/PythonInterface/test/python/mantid/api/CMakeLists.txt
+++ b/Framework/PythonInterface/test/python/mantid/api/CMakeLists.txt
@@ -9,6 +9,7 @@ set ( TEST_PY_FILES
   AlgorithmManagerTest.py
   AlgorithmPropertyTest.py
   AnalysisDataServiceTest.py
+  AnalysisDataServiceObserverTest.py
   AxisTest.py
   CatalogManagerTest.py
   CompositeFunctionTest.py
diff --git a/Framework/WorkflowAlgorithms/CMakeLists.txt b/Framework/WorkflowAlgorithms/CMakeLists.txt
index 49c85f2b98968c7dc872893dcf93ee590ee5714c..26b72344fe602df0a9e24add8d656996bbcaebf9 100644
--- a/Framework/WorkflowAlgorithms/CMakeLists.txt
+++ b/Framework/WorkflowAlgorithms/CMakeLists.txt
@@ -86,6 +86,7 @@ set ( INC_FILES
 )
 
 set ( TEST_FILES
+	AlignAndFocusPowderTest.h
 	ExtractQENSMembersTest.h
 	IMuonAsymmetryCalculatorTest.h
 	LoadEventAndCompressTest.h
diff --git a/Framework/WorkflowAlgorithms/src/AlignAndFocusPowder.cpp b/Framework/WorkflowAlgorithms/src/AlignAndFocusPowder.cpp
index 412a6ec4cff4b07a311af7e4e55dd5e01c807cc2..bf042d3b297507fb62334dcf9b9fbe2be7b3f7e3 100644
--- a/Framework/WorkflowAlgorithms/src/AlignAndFocusPowder.cpp
+++ b/Framework/WorkflowAlgorithms/src/AlignAndFocusPowder.cpp
@@ -271,8 +271,12 @@ void AlignAndFocusPowder::exec() {
   m_inputW = getProperty("InputWorkspace");
   m_inputEW = boost::dynamic_pointer_cast<EventWorkspace>(m_inputW);
   m_instName = m_inputW->getInstrument()->getName();
-  m_instName =
-      Kernel::ConfigService::Instance().getInstrument(m_instName).shortName();
+  try {
+    m_instName =
+        Kernel::ConfigService::Instance().getInstrument(m_instName).shortName();
+  } catch (Exception::NotFoundError &) {
+    ; // not noteworthy
+  }
   std::string calFilename = getPropertyValue("CalFileName");
   std::string groupFilename = getPropertyValue("GroupFilename");
   m_calibrationWS = getProperty("CalibrationWorkspace");
@@ -379,7 +383,7 @@ void AlignAndFocusPowder::exec() {
   } else {
     // workspace2D
     if (m_outputW != m_inputW) {
-      m_outputW = WorkspaceFactory::Instance().create(m_inputW);
+      m_outputW = m_inputW->clone();
     }
   }
 
@@ -720,7 +724,6 @@ void AlignAndFocusPowder::exec() {
     API::IAlgorithm_sptr compressAlg = createChildAlgorithm("CompressEvents");
     compressAlg->setProperty("InputWorkspace", m_outputEW);
     compressAlg->setProperty("OutputWorkspace", m_outputEW);
-    compressAlg->setProperty("OutputWorkspace", m_outputEW);
     compressAlg->setProperty("Tolerance", tolerance);
     if (!isEmpty(wallClockTolerance)) {
       compressAlg->setProperty("WallClockTolerance", wallClockTolerance);
@@ -776,6 +779,13 @@ AlignAndFocusPowder::diffractionFocus(API::MatrixWorkspace_sptr ws) {
     return ws;
   }
 
+  if (m_maskWS) {
+    API::IAlgorithm_sptr maskAlg = createChildAlgorithm("MaskDetectors");
+    maskAlg->setProperty("Workspace", m_groupWS);
+    maskAlg->setProperty("MaskedWorkspace", m_maskWS);
+    maskAlg->executeAsChildAlg();
+  }
+
   g_log.information() << "running DiffractionFocussing started at "
                       << Types::Core::DateAndTime::getCurrentTime() << "\n";
 
diff --git a/Framework/WorkflowAlgorithms/test/AlignAndFocusPowderTest.h b/Framework/WorkflowAlgorithms/test/AlignAndFocusPowderTest.h
new file mode 100644
index 0000000000000000000000000000000000000000..15c4eb136223823e8008ef5df90b33594c7548e7
--- /dev/null
+++ b/Framework/WorkflowAlgorithms/test/AlignAndFocusPowderTest.h
@@ -0,0 +1,820 @@
+// Mantid Repository : https://github.com/mantidproject/mantid
+//
+// Copyright &copy; 2018 ISIS Rutherford Appleton Laboratory UKRI,
+//     NScD Oak Ridge National Laboratory, European Spallation Source
+//     & Institut Laue - Langevin
+// SPDX - License - Identifier: GPL - 3.0 +
+#ifndef ALIGNANDFOCUSPOWDERTEST_H_
+#define ALIGNANDFOCUSPOWDERTEST_H_
+
+#include "MantidTestHelpers/WorkspaceCreationHelper.h"
+#include <cxxtest/TestSuite.h>
+
+#include "MantidAPI/Axis.h"
+#include "MantidAlgorithms/AddSampleLog.h"
+#include "MantidAlgorithms/AddTimeSeriesLog.h"
+#include "MantidAlgorithms/ConvertUnits.h"
+#include "MantidAlgorithms/CreateGroupingWorkspace.h"
+#include "MantidAlgorithms/CreateSampleWorkspace.h"
+#include "MantidAlgorithms/ResampleX.h"
+#include "MantidDataHandling/LoadDiffCal.h"
+#include "MantidDataHandling/LoadNexus.h"
+#include "MantidDataHandling/MaskDetectors.h"
+#include "MantidDataHandling/MoveInstrumentComponent.h"
+#include "MantidDataHandling/RotateInstrumentComponent.h"
+#include "MantidDataObjects/EventWorkspace.h"
+#include "MantidDataObjects/GroupingWorkspace.h"
+#include "MantidWorkflowAlgorithms/AlignAndFocusPowder.h"
+
+using namespace Mantid::API;
+using namespace Mantid::Algorithms;
+using namespace Mantid::DataHandling;
+using namespace Mantid::DataObjects;
+using namespace Mantid::Kernel;
+using namespace Mantid::WorkflowAlgorithms;
+
+class AlignAndFocusPowderTest : public CxxTest::TestSuite {
+public:
+  // This pair of boilerplate methods prevent the suite being created statically
+  // This means the constructor isn't called when running other tests
+  static AlignAndFocusPowderTest *createSuite() {
+    return new AlignAndFocusPowderTest();
+  }
+  static void destroySuite(AlignAndFocusPowderTest *suite) { delete suite; }
+
+  /* Test AlignAndFocusPowder basics */
+  void testTheBasics() {
+    AlignAndFocusPowder align_and_focus;
+    TS_ASSERT_EQUALS(align_and_focus.name(), "AlignAndFocusPowder");
+    TS_ASSERT_EQUALS(align_and_focus.version(), 1);
+  }
+
+  void testInit() {
+    AlignAndFocusPowder align_and_focus;
+    TS_ASSERT_THROWS_NOTHING(align_and_focus.initialize());
+    TS_ASSERT(align_and_focus.isInitialized());
+  }
+
+  void testException() {
+    AlignAndFocusPowder align_and_focus;
+    align_and_focus.initialize();
+    TS_ASSERT_THROWS(align_and_focus.execute(), std::runtime_error);
+  }
+
+  /* Test AlignAndFocusPowder for HRP38692 raw data */
+  void testHRP38692_useCalfile() { doTestHRP38692(true, false, false, false); }
+
+  void testHRP38692_useCalfile_useGroupfile() {
+    doTestHRP38692(true, false, true, false);
+  }
+
+  void testHRP38692_useCalfile_useGroupWorkspace() {
+    doTestHRP38692(true, false, false, true);
+  }
+
+  void testHRP38692_useCalWorkspace_useGroupfile() {
+    doTestHRP38692(false, true, true, false);
+  }
+
+  void testHRP38692_useCalWorkspace_useGroupWorkspace() {
+    doTestHRP38692(false, true, false, true);
+  }
+
+  /* Test AlignAndFocusPowder for Event Workspace*/
+  void testEventWksp_preserveEvents() {
+    // Setup the event workspace
+    setUp_EventWorkspace();
+
+    // Set the inputs for doTestEventWksp
+    m_preserveEvents = true;
+    m_useGroupAll = false;
+    m_useResamplex = true;
+
+    // Run the main test function
+    doTestEventWksp();
+
+    // Test the input
+    docheckEventInputWksp();
+
+    // Test the output
+    TS_ASSERT_DELTA(m_outWS->x(0)[80], 1609.2800, 0.0001);
+    TS_ASSERT_EQUALS(m_outWS->y(0)[80], 20);
+    TS_ASSERT_DELTA(m_outWS->x(0)[880], 14702.0800, 0.0001);
+    TS_ASSERT_EQUALS(m_outWS->y(0)[880], 587);
+  }
+
+  void testEventWksp_preserveEvents_useGroupAll() {
+    // Setup the event workspace
+    setUp_EventWorkspace();
+
+    // Set the inputs for doTestEventWksp
+    m_preserveEvents = true;
+    m_useGroupAll = true;
+    m_useResamplex = true;
+
+    // Run the main test function
+    doTestEventWksp();
+
+    // Test the input
+    docheckEventInputWksp();
+
+    // Test the output
+    TS_ASSERT_DELTA(m_outWS->x(0)[423], 1634.3791, 0.0001);
+    TS_ASSERT_EQUALS(m_outWS->y(0)[423], 2702);
+    TS_ASSERT_DELTA(m_outWS->x(0)[970], 14719.8272, 0.0001);
+    TS_ASSERT_EQUALS(m_outWS->y(0)[970], 149165);
+  }
+
+  void testEventWksp_doNotPreserveEvents() {
+    // Setup the event workspace
+    setUp_EventWorkspace();
+
+    // Set the inputs for doTestEventWksp
+    m_preserveEvents = false;
+    m_useGroupAll = false;
+    m_useResamplex = true;
+
+    // Run the main test function
+    doTestEventWksp();
+
+    // Test the input
+    docheckEventInputWksp();
+
+    // Test the output
+    TS_ASSERT_DELTA(m_outWS->x(0)[80], 1609.2800, 0.0001);
+    TS_ASSERT_EQUALS(m_outWS->y(0)[80], 20);
+    TS_ASSERT_DELTA(m_outWS->x(0)[880], 14702.0800, 0.0001);
+    TS_ASSERT_EQUALS(m_outWS->y(0)[880], 587);
+  }
+
+  void testEventWksp_doNotPreserveEvents_useGroupAll() {
+    // Setup the event workspace
+    setUp_EventWorkspace();
+
+    // Set the inputs for doTestEventWksp
+    m_preserveEvents = false;
+    m_useGroupAll = true;
+    m_useResamplex = true;
+
+    // Run the main test function
+    doTestEventWksp();
+
+    // Test the input
+    docheckEventInputWksp();
+
+    // Test the output
+    TS_ASSERT_DELTA(m_outWS->x(0)[423], 1634.3791, 0.0001);
+    TS_ASSERT_DELTA(m_outWS->y(0)[423], 2419.5680, 0.0001);
+    TS_ASSERT_DELTA(m_outWS->x(0)[970], 14719.8272, 0.0001);
+    TS_ASSERT_DELTA(m_outWS->y(0)[970], 148503.3853, 0.0001);
+  }
+
+  void testEventWksp_rebin_preserveEvents() {
+    // Setup the event workspace
+    setUp_EventWorkspace();
+
+    // Set the inputs for doTestEventWksp
+    m_preserveEvents = true;
+    m_useGroupAll = false;
+    m_useResamplex = false;
+
+    // Run the main test function
+    doTestEventWksp();
+
+    // Test the input
+    TS_ASSERT_DELTA(m_inWS->x(0)[170], 1628.3764, 0.0001);
+    TS_ASSERT_EQUALS(m_inWS->y(0)[170], 48);
+    TS_ASSERT_DELTA(m_inWS->x(0)[391], 14681.7696, 0.0001);
+    TS_ASSERT_EQUALS(m_inWS->y(0)[391], 2540);
+
+    // Test the output
+    TS_ASSERT_DELTA(m_outWS->x(0)[1693], 1629.3502, 0.0001);
+    TS_ASSERT_EQUALS(m_outWS->y(0)[1693], 6);
+    TS_ASSERT_DELTA(m_outWS->x(0)[3895], 14718.1436, 0.0001);
+    TS_ASSERT_EQUALS(m_outWS->y(0)[3895], 612);
+  }
+
+  void testEventWksp_preserveEvents_dmin_dmax() {
+    // Setup the event workspace
+    setUp_EventWorkspace();
+
+    // Set the inputs for doTestEventWksp
+    m_preserveEvents = true;
+    m_useGroupAll = false;
+    m_useResamplex = true;
+    m_dmin = createArgForNumberHistograms(0.5, m_inWS);
+    m_dmax = createArgForNumberHistograms(1.5, m_inWS);
+
+    // Run the main test function
+    doTestEventWksp();
+
+    // Reset inputs to default values
+    m_dmin = "0";
+    m_dmax = "0";
+
+    // Test the input
+    docheckEventInputWksp();
+
+    // Test the output
+    TS_ASSERT_DELTA(m_outWS->x(0)[116], 3270.3908, 0.0001);
+    TS_ASSERT_EQUALS(m_outWS->y(0)[116], 37);
+    TS_ASSERT_DELTA(m_outWS->x(0)[732], 6540.7817, 0.0001);
+    TS_ASSERT_EQUALS(m_outWS->y(0)[732], 25);
+  }
+
+  void testEventWksp_preserveEvents_tmin_tmax() {
+    // Setup the event workspace
+    setUp_EventWorkspace();
+
+    // Set the inputs for doTestEventWksp
+    m_preserveEvents = true;
+    m_useGroupAll = false;
+    m_useResamplex = true;
+    m_tmin = "2000.0";
+    m_tmax = "10000.0";
+
+    // Run the main test function
+    doTestEventWksp();
+
+    // Reset inputs to default values
+    m_tmin = "0";
+    m_tmax = "0";
+
+    // Test the input
+    docheckEventInputWksp();
+
+    // Test the output
+    TS_ASSERT_DELTA(m_outWS->x(0)[149], 3270.7563, 0.0001);
+    TS_ASSERT_EQUALS(m_outWS->y(0)[149], 51);
+    TS_ASSERT_DELTA(m_outWS->x(0)[982], 9814.5378, 0.0001);
+    TS_ASSERT_EQUALS(m_outWS->y(0)[982], 138);
+  }
+
+  void testEventWksp_preserveEvents_lambdamin_lambdamax() {
+    // Setup the event workspace
+    setUp_EventWorkspace();
+
+    // Set the inputs for doTestEventWksp
+    m_preserveEvents = true;
+    m_useGroupAll = false;
+    m_useResamplex = true;
+    m_lambdamin = "0.5";
+    m_lambdamax = "3.0";
+
+    // Run the main test function
+    doTestEventWksp();
+
+    // Reset inputs to default values
+    m_lambdamin = "0";
+    m_lambdamax = "0";
+
+    // Test the input
+    docheckEventInputWksp();
+
+    // Test the output
+    TS_ASSERT_DELTA(m_outWS->x(0)[181], 3262.2460, 0.0001);
+    TS_ASSERT_EQUALS(m_outWS->y(0)[181], 105);
+    TS_ASSERT_DELTA(m_outWS->x(0)[581], 9808.6460, 0.0001);
+    TS_ASSERT_EQUALS(m_outWS->y(0)[581], 290);
+    TS_ASSERT_DELTA(m_outWS->x(0)[880], 14702.0800, 0.0001);
+    TS_ASSERT_EQUALS(m_outWS->y(0)[880], 0);
+  }
+
+  void testEventWksp_preserveEvents_maskbins() {
+    // Setup the event workspace
+    setUp_EventWorkspace();
+
+    // Set the inputs for doTestEventWksp
+    m_preserveEvents = true;
+    m_useGroupAll = false;
+    m_useResamplex = true;
+    m_maskBinTableWS = createMaskBinTable();
+
+    // Run the main test function
+    doTestEventWksp();
+
+    // Reset inputs to default values
+    m_maskBinTableWS = nullptr;
+
+    // Test the input
+    docheckEventInputWksp();
+
+    // Test the output
+    TS_ASSERT_DELTA(m_outWS->x(0)[181], 3262.2460, 0.0001);
+    TS_ASSERT_EQUALS(m_outWS->y(0)[181], 105);
+    TS_ASSERT_DELTA(m_outWS->x(0)[581], 9808.6460, 0.0001);
+    TS_ASSERT_EQUALS(m_outWS->y(0)[581], 290);
+    TS_ASSERT_DELTA(m_outWS->x(0)[880], 14702.0800, 0.0001);
+    TS_ASSERT_EQUALS(m_outWS->y(0)[880], 0);
+  }
+
+  void testEventWksp_preserveEvents_noCompressTolerance() {
+    // Setup the event workspace
+    setUp_EventWorkspace();
+
+    // Set the inputs for doTestEventWksp
+    m_preserveEvents = true;
+    m_useGroupAll = false;
+    m_useResamplex = true;
+    m_compressTolerance = "0.0";
+
+    // Run the main test function
+    doTestEventWksp();
+
+    // Reset inputs to default values
+    m_compressTolerance = "0";
+
+    // Test the input
+    docheckEventInputWksp();
+
+    // Test the output
+    TS_ASSERT_DELTA(m_outWS->x(0)[181], 3262.2460, 0.0001);
+    TS_ASSERT_EQUALS(m_outWS->y(0)[181], 105);
+    TS_ASSERT_DELTA(m_outWS->x(0)[581], 9808.6460, 0.0001);
+    TS_ASSERT_EQUALS(m_outWS->y(0)[581], 290);
+    TS_ASSERT_DELTA(m_outWS->x(0)[880], 14702.0800, 0.0001);
+    TS_ASSERT_EQUALS(m_outWS->y(0)[880], 587);
+  }
+
+  void testEventWksp_preserveEvents_highCompressTolerance() {
+    // Setup the event workspace
+    setUp_EventWorkspace();
+
+    // Set the inputs for doTestEventWksp
+    m_preserveEvents = true;
+    m_useGroupAll = false;
+    m_useResamplex = true;
+    m_compressTolerance = "5.0";
+
+    // Run the main test function
+    doTestEventWksp();
+
+    // Reset inputs to default values
+    m_compressTolerance = "0";
+
+    // Test the input
+    docheckEventInputWksp();
+
+    // Test the output
+    TS_ASSERT_DELTA(m_outWS->x(0)[181], 3262.2460, 0.0001);
+    TS_ASSERT_EQUALS(m_outWS->y(0)[181], 96);
+    TS_ASSERT_DELTA(m_outWS->x(0)[581], 9808.6460, 0.0001);
+    TS_ASSERT_EQUALS(m_outWS->y(0)[581], 427);
+    TS_ASSERT_DELTA(m_outWS->x(0)[880], 14702.0800, 0.0001);
+    TS_ASSERT_EQUALS(m_outWS->y(0)[880], 672);
+  }
+
+  void testEventWksp_preserveEvents_compressWallClockTolerance() {
+    // Setup the event workspace
+    setUp_EventWorkspace();
+
+    // Set the inputs for doTestEventWksp
+    m_preserveEvents = true;
+    m_useGroupAll = false;
+    m_useResamplex = true;
+    m_compressWallClockTolerance = "50.0";
+    addPulseTimesForLogs();
+
+    // Run the main test function
+    doTestEventWksp();
+
+    // Reset inputs to default values
+    m_compressWallClockTolerance = "0";
+
+    // Test the input
+    docheckEventInputWksp();
+
+    // Test the output
+    TS_ASSERT_DELTA(m_outWS->x(0)[181], 3262.2460, 0.0001);
+    TS_ASSERT_EQUALS(m_outWS->y(0)[181], 105);
+    TS_ASSERT_DELTA(m_outWS->x(0)[581], 9808.6460, 0.0001);
+    TS_ASSERT_EQUALS(m_outWS->y(0)[581], 290);
+    TS_ASSERT_DELTA(m_outWS->x(0)[880], 14702.0800, 0.0001);
+    TS_ASSERT_EQUALS(m_outWS->y(0)[880], 587);
+  }
+
+  void testEventWksp_preserveEvents_removePromptPulse() {
+    // Setup the event workspace
+    setUp_EventWorkspace();
+
+    // Set the inputs for doTestEventWksp
+    m_preserveEvents = true;
+    m_useGroupAll = false;
+    m_useResamplex = true;
+    m_removePromptPulse = true;
+    addFrequencyForLogs();
+
+    // Run the main test function
+    doTestEventWksp();
+
+    // Reset inputs to default values
+    m_removePromptPulse = false;
+
+    // Test the input
+    docheckEventInputWksp();
+
+    // Test the output
+    TS_ASSERT_DELTA(m_outWS->x(0)[181], 3262.2460, 0.0001);
+    TS_ASSERT_EQUALS(m_outWS->y(0)[181], 105);
+    TS_ASSERT_DELTA(m_outWS->x(0)[581], 9808.6460, 0.0001);
+    TS_ASSERT_EQUALS(m_outWS->y(0)[581], 0);
+  }
+
+  void testEventWksp_preserveEvents_compressStartTime() {
+    // Setup the event workspace
+    setUp_EventWorkspace();
+
+    // Set the inputs for doTestEventWksp
+    m_preserveEvents = true;
+    m_useGroupAll = false;
+    m_useResamplex = true;
+    // require both inside AlignAndFocusPowder
+    m_compressStartTime =
+        "2010-01-01T00:20:00"; // start time is "2010-01-01T00:00:00"
+    m_compressWallClockTolerance =
+        "50.0"; // require both inside AlignAndFocusPowder
+
+    // Run the main test function
+    doTestEventWksp();
+
+    // Reset inputs to default values
+    m_compressStartTime = "0";
+    m_compressWallClockTolerance = "0";
+
+    // Test the input
+    docheckEventInputWksp();
+
+    // Test the output
+    TS_ASSERT_DELTA(m_outWS->x(0)[181], 3262.2460, 0.0001);
+    TS_ASSERT_EQUALS(m_outWS->y(0)[181], 72);
+    TS_ASSERT_DELTA(m_outWS->x(0)[581], 9808.6460, 0.0001);
+    TS_ASSERT_EQUALS(m_outWS->y(0)[581], 197);
+  }
+
+  /** Setup for testing HRPD NeXus data */
+  void setUp_HRP38692() {
+
+    LoadNexus loader;
+    loader.initialize();
+    loader.setPropertyValue("Filename", "HRP38692a.nxs");
+    loader.setPropertyValue("OutputWorkspace", m_inputWS);
+    loader.execute();
+    TS_ASSERT_THROWS_NOTHING(loader.execute());
+    TS_ASSERT(loader.isExecuted());
+  }
+
+  void doTestHRP38692(bool useCalfile, bool useCalWksp, bool useGroupfile,
+                      bool useGroupWksp) {
+
+    setUp_HRP38692();
+
+    AlignAndFocusPowder align_and_focus;
+    align_and_focus.initialize();
+
+    align_and_focus.setPropertyValue("InputWorkspace", m_inputWS);
+    align_and_focus.setPropertyValue("OutputWorkspace", m_outputWS);
+    align_and_focus.setProperty("ResampleX", 1000);
+    align_and_focus.setProperty("Dspacing", false);
+
+    TS_ASSERT_THROWS_NOTHING(align_and_focus.execute());
+    TS_ASSERT(align_and_focus.isExecuted());
+
+    std::string calfilename("hrpd_new_072_01.cal");
+    if (useCalfile)
+      align_and_focus.setPropertyValue("CalFilename", calfilename);
+    else if (useCalWksp) {
+      loadDiffCal(calfilename, false, true, true);
+      align_and_focus.setPropertyValue("GroupingWorkspace",
+                                       m_loadDiffWSName + "_group");
+      align_and_focus.setPropertyValue("CalibrationWorkspace",
+                                       m_loadDiffWSName + "_cal");
+      align_and_focus.setPropertyValue("MaskWorkspace",
+                                       m_loadDiffWSName + "_mask");
+    }
+
+    if (useGroupfile)
+      align_and_focus.setPropertyValue("GroupFilename", calfilename);
+    else if (useGroupWksp) {
+      loadDiffCal(calfilename, true, false, true);
+      align_and_focus.setPropertyValue("MaskWorkspace",
+                                       m_loadDiffWSName + "_mask");
+      align_and_focus.setPropertyValue("GroupingWorkspace",
+                                       m_loadDiffWSName + "_group");
+    }
+
+    TS_ASSERT_THROWS_NOTHING(align_and_focus.execute());
+    TS_ASSERT(align_and_focus.isExecuted());
+
+    m_inWS =
+        AnalysisDataService::Instance().retrieveWS<MatrixWorkspace>(m_inputWS);
+    m_outWS =
+        AnalysisDataService::Instance().retrieveWS<MatrixWorkspace>(m_outputWS);
+
+    TS_ASSERT_EQUALS(m_inWS->size(), 263857);
+    TS_ASSERT_EQUALS(m_inWS->blocksize(), 23987);
+
+    TS_ASSERT_EQUALS(m_outWS->getAxis(0)->unit()->unitID(), "TOF");
+    TS_ASSERT_EQUALS(m_outWS->size(), 1000);
+    TS_ASSERT_EQUALS(m_outWS->blocksize(), m_outWS->size());
+    TS_ASSERT_EQUALS(m_outWS->getNumberHistograms(), 1);
+
+    // Maximum of peak near TOF approx. equal to 22,000 (micro-seconds)
+    TS_ASSERT_DELTA(m_outWS->x(0)[333], 21990.0502, 0.0001);
+    TS_ASSERT_DELTA(m_outWS->y(0)[333], 770.2515, 0.0001);
+
+    // Maximum of peak near TOF approx. equal to 25,800 (micro-seconds)
+    TS_ASSERT_DELTA(m_outWS->x(0)[398], 25750.3113, 0.0001);
+    TS_ASSERT_DELTA(m_outWS->y(0)[398], 1522.3778, 0.0001);
+
+    // Maximum of peak near TOF approx. equal to 42,000 (micro-seconds)
+    TS_ASSERT_DELTA(m_outWS->x(0)[600], 42056.6091, 0.0001);
+    TS_ASSERT_DELTA(m_outWS->y(0)[600], 7283.29652, 0.0001);
+  }
+
+  /* Setup for event data */
+
+  void setUp_EventWorkspace() {
+    m_inputWS = "eventWS";
+
+    CreateSampleWorkspace createSampleAlg;
+    createSampleAlg.initialize();
+    createSampleAlg.setPropertyValue("WorkspaceType", "Event");
+    createSampleAlg.setPropertyValue("Function", "Powder Diffraction");
+    createSampleAlg.setProperty("XMin", m_xmin); // first frame
+    createSampleAlg.setProperty("XMax", m_xmax);
+    createSampleAlg.setProperty("BinWidth", 1.0);
+    createSampleAlg.setProperty("NumBanks", m_numBanks); // detIds = [100,200)
+    createSampleAlg.setProperty("BankPixelWidth", m_numPixels);
+    createSampleAlg.setProperty("NumEvents", m_numEvents);
+    createSampleAlg.setPropertyValue("OutputWorkspace", m_inputWS);
+    createSampleAlg.execute();
+
+    for (int i = 1; i <= m_numBanks; i++) {
+      std::string bank = "bank" + std::to_string(i);
+      RotateInstrumentComponent rotateInstr;
+      rotateInstr.initialize();
+      rotateInstr.setPropertyValue("Workspace", m_inputWS);
+      rotateInstr.setPropertyValue("ComponentName", bank);
+      rotateInstr.setProperty("Y", 1.);
+      rotateInstr.setProperty("Angle", 90.);
+      rotateInstr.execute();
+
+      MoveInstrumentComponent moveInstr;
+      moveInstr.initialize();
+      moveInstr.setPropertyValue("Workspace", m_inputWS);
+      moveInstr.setPropertyValue("ComponentName", bank);
+      moveInstr.setProperty("X", 5.);
+      moveInstr.setProperty("Y", -.1);
+      moveInstr.setProperty("Z", .1);
+      moveInstr.setProperty("RelativePosition", false);
+      moveInstr.execute();
+    }
+  }
+
+  void docheckEventInputWksp() {
+    TS_ASSERT_DELTA(m_inWS->x(0)[8], 1609.2800, 0.0001);
+    TS_ASSERT_EQUALS(m_inWS->y(0)[8], 97);
+    TS_ASSERT_DELTA(m_inWS->x(0)[18], 3245.8800, 0.0001);
+    TS_ASSERT_EQUALS(m_inWS->y(0)[18], 237);
+    TS_ASSERT_DELTA(m_inWS->x(0)[38], 6519.0800, 0.0001);
+    TS_ASSERT_EQUALS(m_inWS->y(0)[38], 199);
+    TS_ASSERT_DELTA(m_inWS->x(0)[58], 9792.2800, 0.0001);
+    TS_ASSERT_EQUALS(m_inWS->y(0)[58], 772);
+    TS_ASSERT_DELTA(m_inWS->x(0)[88], 14702.0800, 0.0001);
+    TS_ASSERT_EQUALS(m_inWS->y(0)[88], 2162);
+  }
+
+  void doTestEventWksp() {
+    // Bin events using either ResampleX or Rebin
+    int inputHistoBins{100};
+    int numHistoBins{1000};
+    std::string input_params{"-0.01"};
+    std::string params{"-0.001"};
+    if (m_useResamplex) {
+      resamplex(inputHistoBins);
+    } else {
+      rebin(params);
+      m_inWS = AnalysisDataService::Instance().retrieveWS<MatrixWorkspace>(
+          m_inputWS);
+      numHistoBins = int(m_inWS->blocksize());
+
+      rebin(input_params);
+      m_inWS = AnalysisDataService::Instance().retrieveWS<MatrixWorkspace>(
+          m_inputWS);
+      inputHistoBins = int(m_inWS->blocksize());
+    }
+
+    // Initialize AlignAndFocusPowder
+    AlignAndFocusPowder align_and_focus;
+    align_and_focus.initialize();
+    align_and_focus.setPropertyValue("InputWorkspace", m_inputWS);
+    align_and_focus.setPropertyValue("OutputWorkspace", m_outputWS);
+    align_and_focus.setProperty("Dspacing", false);
+    align_and_focus.setProperty("PreserveEvents", m_preserveEvents);
+
+    // Use a Mask TableWorkspace created from createMaskBinTable
+    if (m_maskBinTableWS)
+      align_and_focus.setProperty("MaskBinTable", m_maskBinTableWS);
+
+    // Compress tolerance for events
+    if (m_compressTolerance != "0")
+      align_and_focus.setProperty("CompressTolerance", m_compressTolerance);
+
+    // Compression for the wall clock time; controls whether all pulses are
+    // compressed together
+    if (m_compressWallClockTolerance != "0")
+      align_and_focus.setProperty("CompressWallClockTolerance",
+                                  m_compressWallClockTolerance);
+
+    // Filtering for the start wall clock time; cuts off events before start
+    // time
+    if (m_compressStartTime != "0")
+      align_and_focus.setProperty("CompressStartTime", m_compressStartTime);
+
+    // Remove prompt pulse; will cutoff last 6 long-TOF peaks (freq is 200 Hz)
+    if (m_removePromptPulse)
+      align_and_focus.setProperty("RemovePromptPulseWidth", 1e4);
+
+    // Setup the binning type
+    if (m_useResamplex) {
+      align_and_focus.setProperty("ResampleX", numHistoBins);
+    } else {
+      align_and_focus.setProperty("Params", params);
+    }
+
+    // Crop each histogram using dSpacing
+    if (m_dmin != "0") {
+      align_and_focus.setProperty("Dspacing", true);
+      align_and_focus.setPropertyValue("DMin", m_dmin);
+    }
+    if (m_dmax != "0") {
+      align_and_focus.setProperty("Dspacing", true);
+      align_and_focus.setPropertyValue("DMax", m_dmax);
+    }
+
+    // Crop entire workspace by TOF
+    if (m_tmin != "0")
+      align_and_focus.setPropertyValue("TMin", m_tmin);
+    if (m_tmax != "0")
+      align_and_focus.setPropertyValue("TMax", m_tmax);
+
+    // Crop entire workspace by Wavelength
+    if (m_lambdamin != "0")
+      align_and_focus.setPropertyValue("CropWavelengthMin", m_lambdamin);
+    if (m_lambdamax != "0")
+      align_and_focus.setPropertyValue("CropWavelengthMax", m_lambdamax);
+
+    int numGroups{m_numBanks * m_numPixels * m_numPixels};
+    if (m_useGroupAll) {
+      groupAllBanks(m_inputWS);
+      auto group_wksp =
+          AnalysisDataService::Instance().retrieveWS<MatrixWorkspace>(
+              m_groupWS);
+      align_and_focus.setProperty("GroupingWorkspace", group_wksp->getName());
+      numGroups = (int)group_wksp->blocksize();
+    }
+
+    TS_ASSERT_THROWS_NOTHING(align_and_focus.execute());
+    TS_ASSERT(align_and_focus.isExecuted());
+
+    m_inWS =
+        AnalysisDataService::Instance().retrieveWS<MatrixWorkspace>(m_inputWS);
+    m_outWS =
+        AnalysisDataService::Instance().retrieveWS<MatrixWorkspace>(m_outputWS);
+
+    TS_ASSERT_EQUALS(m_inWS->size(),
+                     m_numBanks * m_numPixels * m_numPixels * inputHistoBins);
+    TS_ASSERT_EQUALS(m_inWS->blocksize(), inputHistoBins);
+
+    TS_ASSERT_EQUALS(m_outWS->getAxis(0)->unit()->unitID(), "TOF");
+    TS_ASSERT_EQUALS(m_outWS->size(), numGroups * numHistoBins);
+    TS_ASSERT_EQUALS(m_outWS->blocksize(), numHistoBins);
+    TS_ASSERT_EQUALS(m_outWS->getNumberHistograms(), numGroups);
+  }
+
+  /* Utility functions */
+  void loadDiffCal(std::string calfilename, bool group, bool cal, bool mask) {
+    LoadDiffCal loadDiffAlg;
+    loadDiffAlg.initialize();
+    loadDiffAlg.setPropertyValue("Filename", calfilename);
+    loadDiffAlg.setPropertyValue("InstrumentName", "HRPD");
+    loadDiffAlg.setProperty("MakeGroupingWorkspace", group);
+    loadDiffAlg.setProperty("MakeCalWorkspace", cal);
+    loadDiffAlg.setProperty("MakeMaskWorkspace", mask);
+    loadDiffAlg.setPropertyValue("WorkspaceName", m_loadDiffWSName);
+    loadDiffAlg.execute();
+  }
+
+  void groupAllBanks(std::string m_inputWS) {
+    CreateGroupingWorkspace groupAlg;
+    groupAlg.initialize();
+    groupAlg.setPropertyValue("InputWorkspace", m_inputWS);
+    groupAlg.setPropertyValue("GroupDetectorsBy", "All");
+    groupAlg.setPropertyValue("OutputWorkspace", m_groupWS);
+    groupAlg.execute();
+  }
+
+  void rebin(std::string params, bool preserveEvents = true) {
+    Rebin rebin;
+    rebin.initialize();
+    rebin.setPropertyValue("InputWorkspace", m_inputWS);
+    rebin.setPropertyValue("OutputWorkspace", m_inputWS);
+    rebin.setPropertyValue("Params", params);
+    rebin.setProperty("PreserveEvents", preserveEvents);
+    rebin.execute();
+    rebin.isExecuted();
+  }
+
+  void resamplex(int numHistoBins, bool preserveEvents = true) {
+    ResampleX resamplexAlg;
+    resamplexAlg.initialize();
+    resamplexAlg.setPropertyValue("InputWorkspace", m_inputWS);
+    resamplexAlg.setPropertyValue("OutputWorkspace", m_inputWS);
+    resamplexAlg.setProperty("NumberBins", numHistoBins);
+    resamplexAlg.setProperty("PreserveEvents", preserveEvents);
+    resamplexAlg.execute();
+  }
+
+  std::string createArgForNumberHistograms(double val, MatrixWorkspace_sptr ws,
+                                           std::string delimiter = ",") {
+    std::vector<std::string> vec;
+    for (size_t i = 0; i < ws->getNumberHistograms(); i++)
+      vec.push_back(boost::lexical_cast<std::string>(val));
+    std::string joined = boost::algorithm::join(vec, delimiter + " ");
+    return joined;
+  }
+
+  ITableWorkspace_sptr createMaskBinTable() {
+    m_maskBinTableWS = WorkspaceFactory::Instance().createTable();
+    m_maskBinTableWS->addColumn("str", "SpectraList");
+    m_maskBinTableWS->addColumn("double", "XMin");
+    m_maskBinTableWS->addColumn("double", "XMax");
+    TableRow row1 = m_maskBinTableWS->appendRow();
+    row1 << "" << 0.0 << 2000.0;
+    TableRow row2 = m_maskBinTableWS->appendRow();
+    row2 << "" << 10000.0 << m_xmax + 1000.0;
+    return m_maskBinTableWS;
+  }
+
+  void addPulseTimesForLogs() {
+    AddTimeSeriesLog logAlg;
+    std::string time, minute;
+    std::string prefix{"2010-01-01T00:"};
+    for (int i = 0; i < 60; i++) {
+      minute =
+          std::string(2 - std::to_string(i).length(), '0') + std::to_string(i);
+      time = prefix + minute + "00";
+      logAlg.initialize();
+      logAlg.setPropertyValue("Workspace", m_inputWS);
+      logAlg.setPropertyValue("Name", "proton_charge");
+      logAlg.setPropertyValue("Time", time);
+      logAlg.setPropertyValue("Value", "100");
+    }
+    logAlg.execute();
+  }
+
+  void addFrequencyForLogs() {
+    AddSampleLog freqAlg;
+    freqAlg.initialize();
+    freqAlg.setPropertyValue("LogName", "Frequency");
+    freqAlg.setPropertyValue("LogText", "200.0");
+    freqAlg.setPropertyValue("LogUnit", "Hz");
+    freqAlg.setPropertyValue("LogType", "Number Series");
+    freqAlg.setPropertyValue("NumberType", "Double");
+    freqAlg.setPropertyValue("Workspace", m_inputWS);
+    freqAlg.execute();
+  }
+
+private:
+  std::string m_inputWS{"nexusWS"};
+  std::string m_outputWS{"align_and_focused"};
+  MatrixWorkspace_sptr m_inWS;
+  MatrixWorkspace_sptr m_outWS;
+  ITableWorkspace_sptr m_maskBinTableWS;
+
+  std::string m_loadDiffWSName{"AlignAndFocusPowderTest_diff"};
+  std::string m_groupWS{"AlignAndFocusPowderTest_groupWS"};
+  std::string m_maskBinTableWSName{"AlignAndFocusPowderTest_maskBinTable"};
+
+  int m_numEvents{10000};
+  int m_numBanks{1};
+  int m_numPixels{12};
+  double m_xmin{300.0};
+  double m_xmax{16666.0};
+
+  std::string m_dmin{"0"};
+  std::string m_dmax{"0"};
+  std::string m_tmin{"0"};
+  std::string m_tmax{"0"};
+  std::string m_lambdamin{"0"};
+  std::string m_lambdamax{"0"};
+  std::string m_compressTolerance{"0"};
+  std::string m_compressWallClockTolerance{"0"};
+  std::string m_compressStartTime{"0"};
+  bool m_removePromptPulse{false};
+  bool m_preserveEvents{true};
+  bool m_useGroupAll{true};
+  bool m_useResamplex{true};
+};
+
+#endif /*ALIGNANDFOCUSPOWDERTEST_H_*/
diff --git a/MantidPlot/make_package.rb.in b/MantidPlot/make_package.rb.in
index f780cbb95e7ccc09e585c94c4c32b2e607b1f63b..c59b0561d220a28f391aa48bbd081b648b6db44a 100755
--- a/MantidPlot/make_package.rb.in
+++ b/MantidPlot/make_package.rb.in
@@ -339,8 +339,8 @@ system_python_extras = "/System/Library/Frameworks/Python.framework/Versions/2.7
 pip_site_packages = "/Library/Python/2.7/site-packages"
 directories = ["sphinx","sphinx_bootstrap_theme","IPython","zmq","pygments","backports", "qtawesome", "qtpy",
                "certifi","tornado","markupsafe","matplotlib","mpl_toolkits", "jinja2","jsonschema","functools32",
-               "ptyprocess","CifFile","yaml","requests","cloudpickle","dask","networkx","PIL","dateutil","pytz",
-               "pywt","toolz","skimage"]
+               "ptyprocess","CifFile","yaml","requests","networkx","PIL","dateutil","pytz",
+               "pywt","skimage"]
 directories.each do |directory|
   module_dir = "#{pip_site_packages}/#{directory}"
   if !File.exist?(module_dir)
diff --git a/Testing/PerformanceTests/make_report.py b/Testing/PerformanceTests/make_report.py
index 99332b10bba0ceeb96ad9beb051ae99c90ad50e4..8c766e84b23dbce0b678bdf616037b00d7ba65a9 100755
--- a/Testing/PerformanceTests/make_report.py
+++ b/Testing/PerformanceTests/make_report.py
@@ -64,14 +64,18 @@ if __name__ == "__main__":
                         default=["./MantidSystemTests.db"],
                         help='Required: Path to the SQL database file(s).')
 
+    parser.add_argument('--plotting', dest='plotting',
+                        default="plotly",
+                        help='Plotting toolkit to generate the plots. Options=["plotly", "matplotlib"]')
+
     args = parser.parse_args()
 
-    # Import the manager definition
-    try:
+    if args.plotting == 'plotly':
         import analysis
-    except:
-        # plotly not available, use matplotlib fallback
+    elif args.plotting == 'matplotlib':
         import analysis_mpl as analysis
+    else:
+        raise RuntimeError("Unknown plotting toolkit '{}'".format(args.plotting))
 
     import sqlresults
 
diff --git a/Testing/SystemTests/scripts/InstallerTests.py b/Testing/SystemTests/scripts/InstallerTests.py
index 5822173ca4ca89d603f12b3b7c3e9d0af818cb0a..1e4a1547f3b0a21fc0c59e6f309ef141a1afaf7d 100644
--- a/Testing/SystemTests/scripts/InstallerTests.py
+++ b/Testing/SystemTests/scripts/InstallerTests.py
@@ -33,7 +33,7 @@ parser.add_argument('--archivesearch', dest='archivesearch', action='store_true'
 parser.add_argument('--exclude-in-pull-requests', dest="exclude_in_pr_builds",action="store_true",
                     help="Skip tests that are not run in pull request builds")
 log_levels = ['error', 'warning', 'notice', 'information', 'debug']
-parser.add_argument('-l', dest='log_level', metavar='level', default='notice',
+parser.add_argument('-l', dest='log_level', metavar='level', default='information',
                     choices=log_levels, help='Log level '+str(log_levels))
 options = parser.parse_args()
 
diff --git a/Testing/SystemTests/tests/analysis/GUIStartupTest.py b/Testing/SystemTests/tests/analysis/GUIStartupTest.py
index 8b0eea635a971f9a447d6b01ceebb6820cf165df..fb2e24e8e261ae735fc3448106a7fb60825b56cf 100644
--- a/Testing/SystemTests/tests/analysis/GUIStartupTest.py
+++ b/Testing/SystemTests/tests/analysis/GUIStartupTest.py
@@ -58,7 +58,7 @@ class GUIStartupTest(systemtesting.MantidSystemTest):
         # good startup
         p = subprocess.Popen(self.cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
         out, err = p.communicate()
-        self.assertEquals(out, b'Hello Mantid\n')
+        self.assertTrue(b'Hello Mantid\n' in out)
 
         # failing script
         with open(self.script, 'a') as f:
diff --git a/Testing/SystemTests/tests/analysis/reference/PG3_9829_sum_reference.gsa.md5 b/Testing/SystemTests/tests/analysis/reference/PG3_9829_sum_reference.gsa.md5
index d5ea4e051aa8223bae162d5464e41160bf20dfca..e3db4c237afddfb25a0e78a62f185f1803b3d35a 100644
--- a/Testing/SystemTests/tests/analysis/reference/PG3_9829_sum_reference.gsa.md5
+++ b/Testing/SystemTests/tests/analysis/reference/PG3_9829_sum_reference.gsa.md5
@@ -1 +1 @@
-13f233609f78d30c6cbdb228a8c1b1b3
+0a6f1170aa0baca89f766337fe50b687
diff --git a/buildconfig/CMake/Packaging/launch_mantidplot.sh.in b/buildconfig/CMake/Packaging/launch_mantidplot.sh.in
index 0ba17b9ad15420e63018f7fd27d534876159d876..daae071f08fdd71b6fc44acf1905e0ead3506c0c 100644
--- a/buildconfig/CMake/Packaging/launch_mantidplot.sh.in
+++ b/buildconfig/CMake/Packaging/launch_mantidplot.sh.in
@@ -18,4 +18,4 @@ INSTALLDIR=$(dirname $INSTALLDIR) # root install directory
 # Launch
 LD_PRELOAD=${LOCAL_PRELOAD} TCMALLOC_RELEASE_RATE=${TCM_RELEASE} \
     TCMALLOC_LARGE_ALLOC_REPORT_THRESHOLD=${TCM_REPORT} \
-    @WRAPPER_PREFIX@$VGLRUN $GDB $INSTALLDIR/bin/@MANTIDPLOT_EXEC@ $*@WRAPPER_POSTFIX@ || @PYTHON_EXECUTABLE@ @SCRIPTSDIR@/@ERROR_CMD@
+    @WRAPPER_PREFIX@$VGLRUN $GDB $INSTALLDIR/bin/@MANTIDPLOT_EXEC@ "$@"@WRAPPER_POSTFIX@ || @PYTHON_EXECUTABLE@ @SCRIPTSDIR@/@ERROR_CMD@
diff --git a/buildconfig/CMake/Packaging/launch_mantidworkbench.sh.in b/buildconfig/CMake/Packaging/launch_mantidworkbench.sh.in
index 476e533daac4a1a18b2615862db746548e937faf..ffe62a59252757c331116a79f06f5638f6b9c5a2 100644
--- a/buildconfig/CMake/Packaging/launch_mantidworkbench.sh.in
+++ b/buildconfig/CMake/Packaging/launch_mantidworkbench.sh.in
@@ -24,4 +24,4 @@ fi
 LD_PRELOAD=${LOCAL_PRELOAD} TCMALLOC_RELEASE_RATE=${TCM_RELEASE} \
     TCMALLOC_LARGE_ALLOC_REPORT_THRESHOLD=${TCM_REPORT} \
     PYTHONPATH=${LOCAL_PYTHONPATH} \
-    @WRAPPER_PREFIX@$VGLRUN $GDB @PYTHON_EXECUTABLE@ $INSTALLDIR/bin/@MANTIDWORKBENCH_EXEC@ $*@WRAPPER_POSTFIX@ || @PYTHON_EXECUTABLE@ @SCRIPTSDIR@/@ERROR_CMD@
+    @WRAPPER_PREFIX@$VGLRUN $GDB @PYTHON_EXECUTABLE@ $INSTALLDIR/bin/@MANTIDWORKBENCH_EXEC@ "$@"@WRAPPER_POSTFIX@ || @PYTHON_EXECUTABLE@ @SCRIPTSDIR@/@ERROR_CMD@
diff --git a/buildconfig/CMake/Packaging/mantidpython.in b/buildconfig/CMake/Packaging/mantidpython.in
index c96304ffa464f047d88ec95a74f6dde1d20230bd..51d8d737d8b19839270f382c94114e0a94c477ab 100755
--- a/buildconfig/CMake/Packaging/mantidpython.in
+++ b/buildconfig/CMake/Packaging/mantidpython.in
@@ -34,14 +34,14 @@ fi
 
 if [ -n "$1" ] && [ "$1" = "--classic" ]; then
     shift
-    set -- @WRAPPER_PREFIX@@PYTHON_EXECUTABLE@ $*@WRAPPER_POSTFIX@
+    set -- @WRAPPER_PREFIX@@PYTHON_EXECUTABLE@ "$@"@WRAPPER_POSTFIX@
 elif [ -n "$1" ] && [ -n "$2" ] && [ "$1" = "-n" ]; then
     ranks=$2
     shift 2
-    set -- mpirun -n $ranks @WRAPPER_PREFIX@@PYTHON_EXECUTABLE@ $*@WRAPPER_POSTFIX@
+    set -- mpirun -n $ranks @WRAPPER_PREFIX@@PYTHON_EXECUTABLE@ "$@"@WRAPPER_POSTFIX@
 else
     IPYTHON_STARTUP="import IPython;IPython.start_ipython()"
-    set -- @WRAPPER_PREFIX@@PYTHON_EXECUTABLE@ -c "${IPYTHON_STARTUP}" $*@WRAPPER_POSTFIX@
+    set -- @WRAPPER_PREFIX@@PYTHON_EXECUTABLE@ -c "${IPYTHON_STARTUP}" "$@"@WRAPPER_POSTFIX@
 fi
 
 LD_PRELOAD=${LOCAL_PRELOAD} TCMALLOC_RELEASE_RATE=${TCM_RELEASE} \
diff --git a/buildconfig/Jenkins/buildscript b/buildconfig/Jenkins/buildscript
index 2bdc3fb258f0a12518c68ad58f82da60eb3ac3a9..145830fb7135f6b3b574e07f478bac5d86bc47e0 100755
--- a/buildconfig/Jenkins/buildscript
+++ b/buildconfig/Jenkins/buildscript
@@ -363,6 +363,16 @@ fi
 # Prevent race conditions when creating the user config directory
 userconfig_dir=$HOME/.mantid
 rm -fr $userconfig_dir
+# Remove GUI qsettings files
+if [[ ${ON_MACOS} == true ]] ; then
+  rm -f $HOME/Library/Preferences/com.mantid.MantidPlot.plist
+  rm -f $HOME/Library/Preferences/org.mantidproject.MantidPlot.plist
+  rm -f "$HOME/Library/Saved Application State/org.mantidproject.MantidPlot.savedState/windows.plist"
+else
+  rm -f ~/.config/Mantid/MantidPlot.conf
+fi
+rm -f ~/.config/mantidproject/mantidworkbench.ini
+
 mkdir -p $userconfig_dir
 # use a fixed number of openmp threads to avoid overloading the system
 userprops_file=$userconfig_dir/Mantid.user.properties
diff --git a/buildconfig/Jenkins/buildscript.bat b/buildconfig/Jenkins/buildscript.bat
index 892b6c5b31d20b829b13dbbf1709240b671514b2..402a0db2907f86067cc4be73b1fc70a85aff3231 100755
--- a/buildconfig/Jenkins/buildscript.bat
+++ b/buildconfig/Jenkins/buildscript.bat
@@ -201,9 +201,10 @@ if ERRORLEVEL 1 exit /B %ERRORLEVEL%
 :: This prevents race conditions when creating the user config directory
 set USERPROPS=bin\%BUILD_CONFIG%\Mantid.user.properties
 del %USERPROPS%
-set CONFIGDIR=%APPDATA%\mantidproject\mantid
+set CONFIGDIR=%APPDATA%\mantidproject
 rmdir /S /Q %CONFIGDIR%
-mkdir %CONFIGDIR%
+:: Create the directory to avoid any race conditions
+mkdir %CONFIGDIR%\mantid
 :: use a fixed number of openmp threads to avoid overloading the system
 echo MultiThreaded.MaxCores=2 > %USERPROPS%
 
diff --git a/buildconfig/Jenkins/systemtests b/buildconfig/Jenkins/systemtests
index fe04694e0c2436bc9593f4188a4afb29beae0213..4fdda64ac74231a227b5113648b9b31eb2ff2496 100755
--- a/buildconfig/Jenkins/systemtests
+++ b/buildconfig/Jenkins/systemtests
@@ -50,6 +50,11 @@ fi
 [ -d $WORKSPACE/build ] || mkdir $WORKSPACE/build
 cd $WORKSPACE/build
 
+# Remove (possibly) stale files
+#   build/ExternalData/**: data files will change over time and removing
+#                          the links helps keep it fresh
+rm -rf $WORKSPACE/build/ExternalData
+
 ###############################################################################
 # CMake configuration if it has not already been configured.
 # We use the special flag that only creates the targets for the data
@@ -69,10 +74,22 @@ ${CMAKE_EXE} --build . -- SystemTestData
 ###############################################################################
 # Run the tests
 ###############################################################################
-# Remove any Mantid.user.properties file
-userprops=~/.mantid/Mantid.user.properties
-rm -f $userprops
+# Remove any user settings
+userconfig_dir=$HOME/.mantid
+rm -fr $userconfig_dir
+# Remove GUI qsettings files
+if [[ ${ON_MACOS} == true ]] ; then
+  rm -f $HOME/Library/Preferences/com.mantid.MantidPlot.plist
+  rm -f $HOME/Library/Preferences/org.mantidproject.MantidPlot.plist
+  rm -f "$HOME/Library/Saved Application State/org.mantidproject.MantidPlot.savedState/windows.plist"
+else
+  rm -f ~/.config/Mantid/MantidPlot.conf
+fi
+rm -f ~/.config/mantidproject/mantidworkbench.ini
+
 # Turn off any auto updating on startup
+mkdir -p $userconfig_dir
+userprops=$userconfig_dir/Mantid.user.properties
 echo "UpdateInstrumentDefinitions.OnStartup = 0" > $userprops
 echo "usagereports.enabled = 0" >> $userprops
 echo "CheckMantidVersion.OnStartup = 0" >> $userprops
diff --git a/buildconfig/Jenkins/systemtests.bat b/buildconfig/Jenkins/systemtests.bat
index 02c2fd894afe31157b2b62b15ac4aa4659ea31bb..1034ccc99c090427eb684375e04a07417871121b 100755
--- a/buildconfig/Jenkins/systemtests.bat
+++ b/buildconfig/Jenkins/systemtests.bat
@@ -34,6 +34,11 @@ if NOT DEFINED MANTID_DATA_STORE (
 md %WORKSPACE%\build
 cd %WORKSPACE%\build
 
+:: Remove (possibly) stale files
+::   build/ExternalData/**: data files will change over time and removing
+::                          the links helps keep it fresh
+rmdir /S /Q %WORKSPACE%\build\ExternalData
+
 :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 :: CMake configuration if it has not already been configured.
 :: We use the special flag that only creates the targets for the data
@@ -62,6 +67,12 @@ if ERRORLEVEL 1 exit /b %ERRORLEVEL%
 set USERPROPS_RELEASE=C:\MantidInstall\bin\Mantid.user.properties
 set USERPROPS_NIGHTLY=C:\MantidNightlyInstall\bin\Mantid.user.properties
 del /Q %USERPROPS_RELEASE% %USERPROPS_NIGHTLY%
+:: Remove user settings
+set CONFIGDIR=%APPDATA%\mantidproject
+rmdir /S /Q %CONFIGDIR%
+:: Create the directory to avoid any race conditions
+mkdir %CONFIGDIR%\mantid
+
 :: Turn off any auto updating on startup
 echo UpdateInstrumentDefinitions.OnStartup = 0 > %USERPROPS_RELEASE%
 echo usagereports.enabled = 0 >> %USERPROPS_RELEASE%
diff --git a/dev-docs/source/Python3.rst b/dev-docs/source/Python3.rst
index 55fc190b999744cd6a0383b834c8ba4689323200..85915a6d2ad75e2d9e1a1055742578ff3d79f80b 100644
--- a/dev-docs/source/Python3.rst
+++ b/dev-docs/source/Python3.rst
@@ -12,28 +12,12 @@ migration strategy for Mantid.
 Building Against Python 3
 #########################
 
-This is currently only possible on a Linux system with a pre-installed version of python 3. You need
-to install some additional packages as shown below:
-
-.. code-block:: sh
-
-   apt-get install python3-sip-dev python3-pyqt4  python3-numpy  python3-scipy  python3-sphinx \
-     python3-sphinx-bootstrap-theme  python3-dateutil python3-matplotlib ipython3-qtconsole \
-     python3-h5py python3-yaml
-
-or on fedora, with slightly different package names
-
-.. code-block:: sh
-
-   dnf install python3-sip-devel python3-PyQt4-devel python3-numpy python3-scipy python3-sphinx \
-     python3-sphinx-theme-bootstrap python3-dateutil python3-matplotlib python3-ipython-gui \
-     boost-python3-devel python3-h5py python3-yaml
-
-then set ``-DPYTHON_EXECUTABLE=/usr/bin/python3`` when running cmake before building.
+This is currently only possible on a Linux system with a pre-installed version of python 3 and you will need to have
+the latest version of the `mantid-developer` package installed. Once installed run cmake as standard but with the additional option ``-DPYTHON_EXECUTABLE=/usr/bin/python3``. Please note that
+reconfiguring an existing Python 2 build is not supported - a build in a fresh build directory is required.
 
 .. warning::
-   If any of these packages are installed via pip, this could cause conflicts.
-   Install as described here only.
+   Do not install python packages via ``pip``. Install packages only from the system repositories.
 
 Supporting Python 2 and 3
 #########################
diff --git a/dev-docs/source/RunningTheUnitTests.rst b/dev-docs/source/RunningTheUnitTests.rst
index fb93f74bca9bdb8def74fe9394435d0bd73f291d..1a34eb1dfa9f85afe61506506cc2b0bc34297a62 100644
--- a/dev-docs/source/RunningTheUnitTests.rst
+++ b/dev-docs/source/RunningTheUnitTests.rst
@@ -86,22 +86,27 @@ Starting in your build folder (e.g. Mantid/Code/debug):
    .. code-block:: sh
 
       ctest -j8 -R KernelTest
-      bin/KernelTest
+      ./bin/KernelTest
 
 -  Running a specific test class.
 
    .. code-block:: sh
 
       ctest -R MyTestClassName
-      bin/KernelTest MyTestClassName
+      ./bin/KernelTest MyTestClassName
 
--  Running a specific test.
+-  Running a specific test from a CxxTest test class (not possible via CTest).
 
    .. code-block:: sh
 
-      bin/KernelTest MyTestClassName MySingleTestName``
+      ./bin/KernelTest MyTestClassName MySingleTestName
 
-   -  Not possible with ctest.
+- Running a specific test from a Python ``unittest`` test class (not possible
+  via CTest).
+
+  .. code-block:: sh
+
+     ./bin/mantidpython /path/to/src/Framework/PythonInterface/test/python/plugins/algorithms/MeanTest.py MeanTest.test_mean
 
 Running Unit Tests With Visual Studio and ctest
 ###############################################
diff --git a/dev-docs/source/Testing/IndirectInelastic/IndirectInelasticAcceptanceTests.rst b/dev-docs/source/Testing/IndirectInelastic/IndirectInelasticAcceptanceTests.rst
index 3871e7e6832560c73df40965418ecf1b774c027e..cd985643dd1c227a548ed9e9059df2589b889d4d 100644
--- a/dev-docs/source/Testing/IndirectInelastic/IndirectInelasticAcceptanceTests.rst
+++ b/dev-docs/source/Testing/IndirectInelastic/IndirectInelasticAcceptanceTests.rst
@@ -92,7 +92,7 @@ Data analysis Elwin
 #. Click ``Run``
 #. This should result in three new workspaces again, this time with file ranges as their name
 #. In the main GUI right-click on ``irs26174-26176_graphite002_red_elwin_eq2`` and choose ``Plot Spectrum``, choose ``Plot All``
-#. This should plot two lines of :math:`A^{-2}` vs :math:`Q`
+#. This should plot two lines of A^2 vs Q
 #. Right-click on the ``irs26176_graphite002_elwin_eq`` workspace and ``Save Nexus``; save to a location of your choice; you will use this file in the next test
 
 Data analysis MSD
@@ -193,5 +193,3 @@ Data analysis I(Q, T) Fit
 #. Select Lifetime from the ``Plot Output`` drop-down
 #. Click ``Plot Result`` this should open a new plot with the lifetimes plotted
   
-
-
diff --git a/dev-docs/source/Testing/VSI/VSITesting.rst b/dev-docs/source/Testing/VSI/VSITesting.rst
new file mode 100644
index 0000000000000000000000000000000000000000..2063beb03abe647bf09e81c4e022901bc258c68c
--- /dev/null
+++ b/dev-docs/source/Testing/VSI/VSITesting.rst
@@ -0,0 +1,61 @@
+.. _vsi_testing:
+
+VSI Testing
+=============
+
+.. contents::
+   :local:
+
+
+*Preparation*
+
+- Get the scripts from the Mantid repository `this directory <https://github.com/mantidproject/mantid/tree/master/scripts/Vates>`_.
+- Make sure that you have the Mantid system test data. You can do this by building a SystemTestData target of Mantid.
+- Put the system test data directory in your Mantid user directories
+
+
+**Time required 20-30 minutes**
+
+--------------
+
+#. Load the script SXD_NaCl.py in MantidPlot. Run it.
+#. This should load a workspace called QLab. Right-click this workspace and choose 'Show Vates simple interface'.
+#. This should open the VSI window. Like the picture below. Check that all tabs are present.
+
+.. figure:: ../../images/vsi.png
+   :alt: alternate text
+   :align: right
+   
+#. Click the various tabs making sure that the interface does not crash. 
+#. Make sure all tabs have a tool tip when you hover over them.
+#. Go into three slice mode (the star shaped tab at the top).
+#. Play with the maximum and minimum parameters at the top of the plot, make sure the colours change in the plot.
+#. Hover on each of the upper two plots and the lower left plot. While hovering over a plot use the roll bar (or equivalent), the slices in the other plots should update. The lower right plot should show all three slices. Check that the text on the plots is legible and updates appropriately.
+#. Switch to multi-slice view (just to the left of three slice view).
+
+   #. In the properties tab (left of the plot) select the Display menu.
+   #.  Choose all options for Representation, make sure they work.
+   #. Change the Styling and Lighting sliders, make sure the picture updates.
+   #. Next to Data Axes Grid click edit, this should open a new dialog of settings for the plot.
+   #. Change some of the properties in this dialog and click apply, the plot should update accordingly.
+   #. Make sure that the sliders for the plot work. These are the triangles on the scales above and to the left of the plot. Slide them and move through the plot.
+   #. On the slider scale Shift+Right-click should add another slice
+   #. Select the View menu.
+   #. Turn on/off the orientation axes visibility, check that the little axes icon on the plot turns on and off.
+   #. Check and uncheck the parallel camera box, make sure the view switches between perspective on and off.
+
+#. Switch to standard view (left of multi-slice)
+
+   #. In the Rebin drop-down (above the plot) select Fast Rebin and click Slice - a new MDScaleWorkspace should appear in the list
+   #. In the Properties tab select Properties and change the X/Y/Z scaling factors, click Apply. The plot should distort accordingly. 
+   #. In the properties tab click Delete and the plot alterations should be removed from the plot.
+   #. Repeat the previous three steps for the different Rebin options.  
+
+#. Close the VSI interface and then reopen as before. You are now in scatter plot mode.
+
+   #. Under Properties, change the Number of Points and Top Percentile settings and see that the plot changes.
+   #. Drag the peaks_qLab workspace from the main GUI onto VSI. This should show the peak position in the plot as points.
+   #. Drag the peaks_qLab_integrated workspace from the main GUI onto VSI. This should show the peak positions in the plot as spheres.
+
+#. Close the VSI GUI.
+
diff --git a/dev-docs/source/Testing/index.rst b/dev-docs/source/Testing/index.rst
index 8b5446cd991f14a97fe028c5f40199b194155336..679a911759d4e1d92204f3127a8ff49acc8a798d 100644
--- a/dev-docs/source/Testing/index.rst
+++ b/dev-docs/source/Testing/index.rst
@@ -24,4 +24,5 @@ creation is outlined in :ref:`issue_tracking`.
    IndirectInelastic/IndirectInelasticAcceptanceTests
    ErrorReporter-ProjectRecovery/ErrorReporterTesting
    ErrorReporter-ProjectRecovery/ProjectRecoveryTesting
+   VSI/VSITesting
 
diff --git a/dev-docs/source/images/vsi.png b/dev-docs/source/images/vsi.png
new file mode 100644
index 0000000000000000000000000000000000000000..28685e6c7c23cca0890ccb37de18b9d17dbbd182
Binary files /dev/null and b/dev-docs/source/images/vsi.png differ
diff --git a/docs/source/algorithms/AbsorptionCorrection-v1.rst b/docs/source/algorithms/AbsorptionCorrection-v1.rst
index 7cce5394108f305c9f80272d802b9ffcd95dbf1d..472811bea3ffdda3404c9fdd91e9da4f0d664562 100644
--- a/docs/source/algorithms/AbsorptionCorrection-v1.rst
+++ b/docs/source/algorithms/AbsorptionCorrection-v1.rst
@@ -112,3 +112,4 @@ Output:
 .. categories::
 
 .. sourcelink::
+   :filename: AnyShapeAbsorption
diff --git a/docs/source/algorithms/FilterByLogValue-v1.rst b/docs/source/algorithms/FilterByLogValue-v1.rst
index 6579b1d95d58e25908e113772d61d9159450c7be..8e5a6508db1fb9a3ba1b8d49c74049e1ddaf3614 100644
--- a/docs/source/algorithms/FilterByLogValue-v1.rst
+++ b/docs/source/algorithms/FilterByLogValue-v1.rst
@@ -38,6 +38,11 @@ will be included also. If a log has a single point in time, then that
 log value is assumed to be constant for all time and if it falls within
 the range, then all events will be kept.
 
+.. warning::
+
+   :ref:`FilterByLogValue <algm-FilterByLogValue>` is not suitable for
+   fast log filtering.
+
 PulseFilter (e.g. for Veto Pulses)
 ##################################
 
@@ -63,7 +68,7 @@ rejected. For example, this call will filter out veto pulses:
 .. testsetup:: VetoPulseTime
 
    ws=CreateSampleWorkspace("Event")
-   AddTimeSeriesLog(ws, Name="veto_pulse_time", Time="2010-01-01T00:00:00", Value=1) 
+   AddTimeSeriesLog(ws, Name="veto_pulse_time", Time="2010-01-01T00:00:00", Value=1)
    AddTimeSeriesLog(ws, Name="veto_pulse_time", Time="2010-01-01T00:10:00", Value=0)
    AddTimeSeriesLog(ws, Name="veto_pulse_time", Time="2010-01-01T00:20:00", Value=1)
    AddTimeSeriesLog(ws, Name="veto_pulse_time", Time="2010-01-01T00:30:00", Value=0)
@@ -77,20 +82,20 @@ rejected. For example, this call will filter out veto pulses:
 Comparing with other event filtering algorithms
 ###############################################
 
-Wiki page :ref:`EventFiltering` has a detailed
-introduction on event filtering in MantidPlot.
+The :ref:`EventFiltering` page has a detailed introduction on event
+filtering in mantid.
 
 
 Usage
 -----
 
-**Example - Filtering by a simple time series Log**  
+**Example - Filtering by a simple time series Log**
 
 .. testcode:: FilterByLogValue
 
    ws = CreateSampleWorkspace("Event",BankPixelWidth=1)
 
-   AddTimeSeriesLog(ws, Name="proton_charge", Time="2010-01-01T00:00:00", Value=100) 
+   AddTimeSeriesLog(ws, Name="proton_charge", Time="2010-01-01T00:00:00", Value=100)
    AddTimeSeriesLog(ws, Name="proton_charge", Time="2010-01-01T00:10:00", Value=100)
    AddTimeSeriesLog(ws, Name="proton_charge", Time="2010-01-01T00:20:00", Value=100)
    AddTimeSeriesLog(ws, Name="proton_charge", Time="2010-01-01T00:30:00", Value=100)
diff --git a/docs/source/algorithms/FilterEvents-v1.rst b/docs/source/algorithms/FilterEvents-v1.rst
index b45de531098b1640271f410a12797f88f29a7f4a..7063dcb65b05b1757d3ea993b6613e7474117a38 100644
--- a/docs/source/algorithms/FilterEvents-v1.rst
+++ b/docs/source/algorithms/FilterEvents-v1.rst
@@ -9,147 +9,120 @@
 Description
 -----------
 
-This algorithm filters events from an :ref:`EventWorkspace` to one or
-multiple :ref:`EventWorkspaces <EventWorkspace>` according to an input
-:ref:`SplittersWorkspace` containing a series of splitters (i.e.,
-:ref:`splitting intervals <SplittingInterval>`).
-
-Inputs
-######
-
-FilterEvents takes 2 mandatory input Workspaces and 1 optional
-Workspace.  One of mandatory workspace is the :ref:`EventWorkspace`
-where the events are filtered from.  The other mandatory workspace is
-workspace containing splitters.  It can be a MatrixWorkspace, a TableWorkspace or
-a :ref:`SplittersWorkspace <SplittersWorkspace>`.
-
-The optional workspace is a :ref:`TableWorkspace <Table Workspaces>`
-for information of splitters.
-
-Workspace containing splitters
-==============================
-
-This algorithm accepts three types of workspace that contains event splitters.
-- TableWorkspace: a general TableWorkspace with at three columns
-- MatrixWorkspace: a 1-spectrum MatrixWorkspace
-- SplittersWorkspace: an extended TableWorkspace with restrict definition on start and stop time.
-
-Event splitter
-++++++++++++++
-
-An event splitter contains three items, start time, stop time and splitting target (index).
-All the events belonged to the same splitting target will be saved to a same output EventWorkspace.
-
-Unit of input splitters
-+++++++++++++++++++++++
-
-- MatrixWorkspace:  the unit must be second.
-- TableWorkspace: the unit must be second.
-- SplittersWorkspace: by the definition of SplittersWorkspace, the unit has to be nanosecond.
-
-
-How to generate input workspace containing splitters
-++++++++++++++++++++++++++++++++++++++++++++++++++++
-
-There are two ways to generate
-
-Algorithm :ref:`GenerateEventsFilter <algm-GenerateEventsFilter>`
-creates both the :ref:`SplittersWorkspace <SplittersWorkspace>` and
-splitter information workspace.
-
-
-Splitters in relative time or absolute time
-+++++++++++++++++++++++++++++++++++++++++++
-
-As the SplittersWorkspace is in format of :ref:`MatrixWorkspace
-<MatrixWorkspace>`, its time, i.e., the value in X vector, can be
-relative time.
-
-Property ``RelativeTime`` flags that the splitters' time is relative.
-Property ``FilterStartTime`` specifies the starting time of the filter.
-Or the shift of time of the splitters.
-If it is not specified, then the algorithm will search for sample log ``run_start``.
-
-Outputs
-#######
-
-The output will be one or multiple workspaces according to the number of
-index in splitters. The output workspace name is the combination of
-parameter OutputWorkspaceBaseName and the index in splitter.
-
-Calibration File
-################
-
-The calibration, or say correction, from the detector to sample must be
-consider in fast log. Thus a calibration file is required. The math is
-
-``TOF_calibrated = TOF_raw * correction(detector ID).``
-
-The calibration is in column data format.
-
-A reasonable approximation of the correction is
-
-``correction(detector_ID) = L1/(L1+L2(detector_ID))``
+This algorithm filters events from a single :ref:`EventWorkspace` to
+one or multiple :ref:`EventWorkspaces <EventWorkspace>` according to
+the ``SplittersWorkspace`` property. The :ref:`EventFiltering` concept
+page has a detailed introduction to event filtering.
+
+Specifying the splitting strategy
+---------------------------------
+
+The ``SplittersWorkspace`` describes much of the information for
+splitting the ``InputWorkspace`` into the various output
+workspaces. It can have one of three types
+
++--------------------------------------------------------------+-------------+----------+
+| workspace class                                              | units       | rel/abs  |
++==============================================================+=============+==========+
+| :ref:`MatrixWorkspace <MatrixWorkspace>`                     | seconds     | either   |
++--------------------------------------------------------------+-------------+----------+
+| :class:`SplittersWorkspace <mantid.api.ISplittersWorkspace>` | nanoseconds | absolute |
++--------------------------------------------------------------+-------------+----------+
+| :ref:`TableWorkspace <Table Workspaces>`                     | seconds     | either   |
++--------------------------------------------------------------+-------------+----------+
+
+Whether the values in :ref:`MatrixWorkspace <MatrixWorkspace>` and
+:ref:`TableWorkspace <Table Workspaces>` is treated as relative or
+absolute time is dependent on the value of ``RelativeTime``. In the
+case of ``RelativeTime=True``, the time is relative to the start of
+the run (in the ``ws.run()['run_start']``) or, if specified, the
+``FilterStartTime``. In the case of ``RelativeTime=False``, the times
+are relative to the :class:`GPS epoch <mantid.kernel.DateAndTime>`.
+
+Both :ref:`TableWorkspace <Table Workspaces>` and
+:class:`SplittersWorkspace <mantid.api.ISplittersWorkspace>` have 3
+colums, ``start``, ``stop``, and ``target`` which should be a float,
+float, and string. The :ref:`event filtering <EventFiltering>` concept
+page has details on creating the :ref:`TableWorkspace <Table
+Workspaces>` by hand.
+
+If the ``SplittersWorkspace`` is a :ref:`MatrixWorkspace
+<MatrixWorkspace>`, it must have a single spectrum with the x-value is
+the time boundaries and the y-value is the workspace group index.
+
+The optional ``InformationWorkspace`` is a :ref:`TableWorkspace <Table
+Workspaces>` for information of splitters.
 
 Unfiltered Events
-#################
+-----------------
 
 Some events are not inside any splitters. They are put to a workspace
-name ended with '\_unfiltered'.
-
-If input property 'OutputWorkspaceIndexedFrom1' is set to True, then
-this workspace shall not be outputted.
+name ended with ``_unfiltered``. If
+``OutputWorkspaceIndexedFrom1=True``, then this workspace will not be
+created.
 
 Using FilterEvents with fast-changing logs
-##########################################
+------------------------------------------
+
+There are a few parameters to consider when the log filtering is
+expected to produce a large splitter table. An example of such a case
+would be a data file for which the events need to be split according
+to a log with two or more states changing in the kHz range. To reduce
+the filtering time, one may do the following:
+
+- Make sure the ``SplitterWorkspace`` input is a :ref:`MatrixWorkspace
+  <MatrixWorkspace>`. Such a workspace can be produced by using the
+  ``FastLog = True`` option when calling :ref:`GenerateEventsFilter
+  <algm-GenerateEventsFilter>`.
+- Choose the logs to split. Filtering the logs can take a substantial
+  amount of time. To save time, you may want to split only the logs
+  you will need for analysis. To do so, set ``ExcludeSpecifiedLogs =
+  False`` and list the logs you need in
+  ``TimeSeriesPropertyLogs``. For example, if we only need to know the
+  accumulated proton charge for each filtered workspace, we would set
+  ``TimeSeriesPropertyLogs = proton_charge``.
+
+Correcting time neutron was at the sample
+#########################################
+
+When filtering fast logs, the time to filter by is the time that the
+neutron was at the sample. This can be specified using the
+``CorrectionToSample`` parameter. Either the user specifies the
+correction parameter for every pixel, or one is calculated. The
+correction parameters are applied as
+
+.. math::
+
+   TOF_{sample} = TOF_{detector} * scale[detectorID] + shift[detectorID]
+
+and stored in the ``OutputTOFCorrectionWorkspace``.
+
+* ``CorrectionToSample="None"`` applies no correction
+* ``CorrectionToSample="Elastic"`` applies :math:`shift = 0` with
+  :math:`scale = L1/(L1+L2)` for detectors and :math:`scale = L1/L_{monitor}`
+  for monitors
+* ``CorrectionToSample="Direct"`` applies :math:`scale = 0` and
+  :math:`shift = L1 / \sqrt{2 E_{fix} / m_n}`.  The value supplied in
+  ``IncidentEnergy`` will override the value found in the workspace's
+  value of ``Ei``.
+* ``CorrectionToSample="Indirect"`` applies :math:`scale = 1` and
+  :math:`shift = -1 * L2 / \sqrt{2 E_{fix} / m_n}` for detectors. For
+  monitors, uses the same corrections as ``Elastic``.
+
+* ``CorrectionToSample="Customized"`` applies the correction supplied
+  in the ``DetectorTOFCorrectionWorkspace``.
 
-There are a few parameters to consider when the log filtering is expected to produce a large
-splitter table. An example of such a case would be a data file for which the events need to be split
-according to a log with two or more states changing in the kHz range. To reduce the filtering time,
-one may do the following:
-
-- Make sure the ``SplitterWorkspace`` input is a :ref:`MatrixWorkspace <MatrixWorkspace>`. Such a workspace can be produced by using the ``FastLog = True`` option when calling :ref:`GenerateEventsFilter <algm-GenerateEventsFilter>`.
-- Choose the logs to split. Filtering the logs can take a substantial amount of time. To save time, you may want to split only the logs you will need for analysis. To do so, set ``ExcludeSpecifiedLogs = False`` and list the logs you need in ``TimeSeriesPropertyLogs``. For example, if we only need to know the accumulated proton charge for each filtered workspace, we would set ``TimeSeriesPropertyLogs = proton_charge``.
 
 Difference from FilterByLogValue
-################################
-
-In FilterByLogValue(), EventList.splitByTime() is used.
-
-In FilterEvents, if FilterByPulse is selected true,
-EventList.SplitByTime is called; otherwise, EventList.SplitByFullTime()
-is called instead.
-
-The difference between splitByTime and splitByFullTime is that
-splitByTime filters events by pulse time, and splitByFullTime considers
-both pulse time and TOF.
-
-Therefore, FilterByLogValue is not suitable for fast log filtering.
-
-Comparing with other event filtering algorithms
-###############################################
-
-Wiki page :ref:`EventFiltering` has a detailed introduction on event
-filtering in MantidPlot.
-
-
-Developer's Note
-----------------
-
-Splitters given by TableWorkspace
-#################################
-
-- The ``start/stop time`` is converted to ``m_vecSplitterTime``.
-- The splitting target (in string) is mapped to a set of continuous integers that are stored in ``m_vecSplitterGroup``.
-  - The mapping will be recorded in ``m_targetIndexMap`` and ``m_wsGroupIndexTargetMap``.
-  - Class variable ``m_maxTargetIndex`` is set up to record the highest target group/index,i.e., the max value of ``m_vecSplitterGroup``.
-
-
-Undefined splitting target
-##########################
-
-Indexed as ``0`` in m_vecSplitterGroup.
-
+--------------------------------
+
+In :ref:`FilterByLogValue <algm-FilterByLogValue>`,
+``EventList.splitByTime()`` is used. In FilterEvents, it only uses
+this when ``FilterByPulse=True``. Otherwise,
+``EventList.splitByFullTime()`` is used. The difference between
+``splitByTime`` and ``splitByFullTime`` is that ``splitByTime``
+filters events by pulse time, and ``splitByFullTime`` considers both
+pulse time and TOF.
 
 Usage
 -----
diff --git a/docs/source/algorithms/OptimizeCrystalPlacement-v1.rst b/docs/source/algorithms/OptimizeCrystalPlacement-v1.rst
index d4527d6fdbfba4fe4c41c28e33c416768f54787e..7b48ff9752ce23a87403270148a40a1d299a2b02 100644
--- a/docs/source/algorithms/OptimizeCrystalPlacement-v1.rst
+++ b/docs/source/algorithms/OptimizeCrystalPlacement-v1.rst
@@ -54,16 +54,16 @@ Usage
 .. testcode:: ExOptimizeCrystalPlacement
 
     ws=LoadIsawPeaks("TOPAZ_3007.peaks")
-    LoadIsawUB(ws,"ls5637.mat")
+    LoadIsawUB(ws,"TOPAZ_3007.mat")
     wsd = OptimizeCrystalPlacement(ws)
-    (wsPeakOut,fitInfoTable,chi2overDoF,nPeaks,nParams,nIndexed,covrianceInfoTable) = OptimizeCrystalPlacement(ws)
+    (wsPeakOut,fitInfoTable,chi2overDoF,nPeaks,nParams,nIndexed,covrianceInfoTable) = OptimizeCrystalPlacement(ws,AdjustSampleOffsets=True)
     print("Chi2: {:.4f}".format(chi2overDoF))
 
 Output:
 
 .. testoutput:: ExOptimizeCrystalPlacement
 
-    Chi2: 0.0203
+    Chi2: 0.0003
 
 .. categories::
 
diff --git a/docs/source/algorithms/SetCrystalLocation-v1.rst b/docs/source/algorithms/SetCrystalLocation-v1.rst
new file mode 100644
index 0000000000000000000000000000000000000000..114fb8d9e3297278a3e242ce56eaf29bd0867a1b
--- /dev/null
+++ b/docs/source/algorithms/SetCrystalLocation-v1.rst
@@ -0,0 +1,36 @@
+.. algorithm::
+
+.. summary::
+
+.. relatedalgorithms::
+
+.. properties::
+
+Description
+-----------
+
+This algorithm changes the sample location for an events workspace.  If the InputWorkspace and OutputWorkspace are the same, the position is simply changed.  If the InputWorkspace and OutputWorkspace are different, the InputWorkspace is cloned then the clone's position is changed.  The former is faster, especially for large workspaces. 
+
+Usage
+-----
+
+**Example:**
+
+.. testcode:: ExSetCrystalLocation
+
+  events = Load('BSS_11841_event.nxs')
+  sample = mtd['events'].getInstrument().getSample()
+  print('Sample position before SetCrystalLocation: {}'.format(sample.getPos()))
+  SetCrystalLocation(InputWorkspace=events, OutputWorkspace=events, NewX=0.1, NewY=0.1, NewZ=0.1)
+  print('Sample position after SetCrystalLocation: {}'.format(sample.getPos()))
+
+Output:
+
+.. testoutput:: ExSetCrystalLocation
+
+  Sample position before SetCrystalLocation: [0,0,0]
+  Sample position after SetCrystalLocation: [0.1,0.1,0.1]
+
+.. categories::
+
+.. sourcelink::
diff --git a/docs/source/concepts/EventFiltering.rst b/docs/source/concepts/EventFiltering.rst
index d86f072e1dbd1578c81e99ca95cd0bd21b04c47a..c77137bbefa98cf7fbd1e2437740ac4bec866820 100644
--- a/docs/source/concepts/EventFiltering.rst
+++ b/docs/source/concepts/EventFiltering.rst
@@ -7,86 +7,117 @@ Event Filtering
 .. contents::
    :local:
 
-In MantidPlot, there are a few algorithms working with event
-filtering.  These algorithms are :ref:`algm-FilterByTime`,
-:ref:`algm-FilterByLogValue`, :ref:`algm-FilterEvents`, and
-:ref:`algm-GenerateEventsFilter`.
+In mantid, there are a variety of ways to filter events that are in an
+:ref:`EventWorkspace`. They are :ref:`FilterByTime
+<algm-FilterByTime>` and :ref:`FilterByLogValue
+<algm-FilterByLogValue>` which will create a filter and apply it in a
+single step. The other way to filter events is to use
+:ref:`FilterEvents <algm-FilterEvents>` which allows for a variety of
+workspaces to specify how an :ref:`EventWorkspace` is split. This
+document focuses on how the create these workspaces and will largely
+ignore :ref:`FilterByTime <algm-FilterByTime>` and
+:ref:`FilterByLogValue <algm-FilterByLogValue>`.
 
 How to generate event filters
 =============================
 
-Generating filters explicitly
------------------------------
-
-:ref:`algm-FilterEvents` reads and parses a
-:class:`mantid.api.ISplittersWorkspace` object to generate a list of
-:ref:`SplittingIntervals <SplittingInterval>`, which are used to split
-neutron events to specified output workspaces according to the times
-that they arrive detectors.
-
-There can be two approaches to create a
-:class:`mantid.api.ISplittersWorkspace`.
-
-* :ref:`algm-GenerateEventsFilter` generate event filters by either by
-  time or log value.  The output filters are stored in a
-  :ref:`SplittersWorkspace`, which is taken as an input property of
-  :ref:`algm-FilterEvents`.
-
-* Users can create a :class:`mantid.api.ISplittersWorkspace` from scrach from Python
-  script, because :class:`mantid.api.ISplittersWorkspace` inherits from
-  :ref:`TableWorkspace <Table Workspaces>`.
-
-Generating inexplicit filters
------------------------------
-
-:ref:`algm-FilterByTime` and :ref:`algm-FilterByLogValue` generate event filters during execution.
-
-* :ref:`algm-FilterByTime` generates a set of :ref:`SplittingInterval`
-  according to user-specified setup for time splicing;
-
-* :ref:`algm-FilterByLogValue` generates a set of
-  :ref:`SplittingInterval` according to the value of a specific sample
-  log.
-
-:ref:`algm-GenerateEventsFilter` and :ref:`algm-FilterEvents` vs :ref:`algm-FilterByTime` and :ref:`algm-FilterByLogValue`
---------------------------------------------------------------------------------------------------------------------------
-
-* If :ref:`algm-GenerateEventsFilter` and :ref:`algm-FilterEvents` are
-  set up correctly, they can have the same functionality as
-  :ref:`algm-FilterByTime` and :ref:`algm-FilterByLogValue`.
-
-* :ref:`algm-FilterEvents` is able to filter neutron events by either
-  their pulse times or their absolute times.  An neutron event's
-  absolute time is the summation of its pulse time and TOF.
-
-* :ref:`algm-FilterByLogValue` and :ref:`algm-FilterByTime` can only
-  split neutron events by their pulse time.
-
-Types of events filters
-=======================
-
-Filtering by :ref:`SplittingInterval`
--------------------------------------
-
-:ref:`SplittingInterval` is an individual class to indicate an
-independent time splitter.  Any event can be filtered by a
-:ref:`SplittingInterval` object.
-
-:ref:`SplittersWorkspace` is a :ref:`TableWorkspace <Table
-Workspaces>` that stors a set of :ref:`SplittingInterval`.
-
-Filtering by duplicate entries/booleans
----------------------------------------
-
-Duplicate entries in a :ref:`TimeSeriesProperty` and boolean type of
-:ref:`TimeSeriesProperty` are used in MantidPlot too to serve as time
-splitters.
-
-These two are applied in the MantidPlot log viewing functionality and
-unfortunately intrudes into :ref:`TimeSeriesProperty`.
-
-As time splitters are better to be isolated from logs, which are
-recorded in :ref:`TimeSeriesProperty`, it is not
-recommended to set up event filters by this approach.
+Implicit filters
+----------------
+
+:ref:`algm-FilterByTime` and :ref:`algm-FilterByLogValue` internally
+generate event filters during execution that are not exposed to the
+user. These algorithms can only split the neutron events by pulse
+time and do not provide the equivalent of a ``FastLog=True`` option.
+
+Explicit filters
+----------------
+
+:ref:`algm-FilterEvents` takes either a :class:`SplittersWorkspace
+<mantid.api.ISplittersWorkspace>`, :ref:`TableWorkspace <Table
+Workspaces>`, or :ref:`MatrixWorkspace <MatrixWorkspace>` as the
+``SplittersWorkspace``. The events are split into output workspaces
+according to the times that they arrive detectors.
+
+:ref:`GenerateEventsFilter <algm-GenerateEventsFilter>` will create a
+:class:`SplittersWorkspace <mantid.api.ISplittersWorkspace>` based on
+its various options. This result can be supplied as the
+``SplittersWorkspace`` input property of ref:`algm-FilterEvents`. It
+will also generate an ``InformationWorkspace`` which can be passed
+along to :ref:`GenerateEventsFilter <algm-GenerateEventsFilter>`.
+Depending on the parameters in :ref:`GenerateEventsFilter
+<algm-GenerateEventsFilter>`, the events will be filtered based on
+their pulse times or their absolute times.  An neutron event's
+absolute time is the summation of its pulse time and TOF.
+
+Custom event filters
+====================
+
+Sometimes one wants to filter events based on arbitrary conditions. In
+this case, one needs to go beyond what existing algorithms can do. For
+this, one must generate their own splitters workspace. The workspace
+is generally 3 columns, with the first two being start and stop times
+and the third being the workspace index to put the events into. For
+filtering with time relative to the start of the run, the first two
+columns are ``float``. To specify the times as absolute, in the case
+of filtering files that will be summed together, the first two columns
+should be ``int64``. For both of the examples below, the filter
+workspaces are created using the following function:
+
+.. code-block:: python
+
+   def create_table_workspace(table_ws_name, column_def_list):
+      CreateEmptyTableWorkspace(OutputWorkspace=table_ws_name)
+      table_ws = mtd[table_ws_name]
+      for col_tup in column_def_list:
+          data_type = col_tup[0]
+          col_name = col_tup[1]
+          table_ws.addColumn(data_type, col_name)
+
+      return table_ws
+
+Relative time
+-------------
+
+The easiest way to generate a custom event filter is to make one
+relative to the start time of the run or relative to a specified
+epoch. As the times in the table are seconds, a table can be created
+and used
+
+.. code-block:: python
+
+   filter_rel = create_table_workspace('custom_relative', [('float', 'start'), ('float', 'stop'), ('str', 'target')])
+   filter_rel.addRow((0,9500, '0'))
+   filter_rel.addRow((9500,19000, '1'))
+   FilterEvents(InputWorkspace='ws', SplitterWorkspace=filter_rel,
+                GroupWorkspaces=True, OutputWorkspaceBaseName='relative', RelativeTime=True)
+
+This will generate an event filter relative to the start of the
+run. Specifying the ``FilterStartTime`` in :ref:`FilterEvents
+<algm-FilterEvents>`, one can specify a different time that filtering
+will be relative to.
+
+Absolute time
+-------------
+
+If instead a custom filter is to be created with absolute time, the
+time must be processed somewhat to go into the table workspace. Much of the
+
+.. code-block:: python
+
+   abs_times = [datetime64('2014-12-12T09:11:22.538096666'), datetime64('2014-12-12T11:45:00'), datetime64('2014-12-12T14:14:00')]
+   # convert to time relative to GPS epoch
+   abs_times = [time - datetime64('1990-01-01T00:00') for time in abs_times]
+   # convert to number of seconds
+   abs_times = [float(time / timedelta64(1, 's')) for time in abs_times]
+
+   filter_abs = create_table_workspace('custom_absolute', [('float', 'start'), ('float', 'stop'), ('str', 'target')])
+   filter_abs.addRow((abs_times[0], abs_times[1], '0'))
+   filter_abs.addRow((abs_times[1], abs_times[2], '1'))
+   FilterEvents(InputWorkspace='PG3_21638', SplitterWorkspace=filter_abs,
+                GroupWorkspaces=True, OutputWorkspaceBaseName='absolute', RelativeTime=False)
+
+Be warned that specifying ``RelativeTime=True`` with a table full of
+absolute times will almost certainly generate output workspaces
+without any events in them.
 
 .. categories:: Concepts
diff --git a/docs/source/concepts/FitFunctionsInPython.rst b/docs/source/concepts/FitFunctionsInPython.rst
index af1f53d3fe63fb4bd48b2e6daa661137673f10b7..6ba7ef2b30e8bff89806910a1582ded4f996ceca 100644
--- a/docs/source/concepts/FitFunctionsInPython.rst
+++ b/docs/source/concepts/FitFunctionsInPython.rst
@@ -204,6 +204,21 @@ Also one can put parameters into the function when evaluating.
 
 This enables one to fit the functions with ``scipy.optimize.curve_fit``.  
 
+Errors
+------
+
+The errors assoicated with a given parameter can be accessed using the ``getError`` method.
+``getError`` takes either the parameter name or index as input. For example to get the error
+on ``A1`` in the above polynomial, the code is:
+
+.. code:: python
+
+    # Find the parameter error by index
+    error_A1 = p.getError(1)
+    # Find the parameter error by name
+    error_A1 = p.getError('A1')
+
+
 Plotting
 --------
 Functions may be plotted by calling the ``plot`` method of the function.
diff --git a/docs/source/interfaces/Indirect Data Analysis.rst b/docs/source/interfaces/Indirect Data Analysis.rst
index 93183a1838f8d8be13a8a1dc7b213d02dba30fde..1efbf2321cacda09c42657353435521d1675049a 100644
--- a/docs/source/interfaces/Indirect Data Analysis.rst	
+++ b/docs/source/interfaces/Indirect Data Analysis.rst	
@@ -1,4 +1,4 @@
-Indirect Data Analysis
+Indirect Data Analysis
 ======================
 
 .. contents:: Table of Contents
@@ -93,8 +93,10 @@ SE log value
   specified value in the instrument parameters file, and in the absence of such
   specification, defaults to "last value")
 
-Plot Result
-  If enabled will plot the result as a spectra plot.
+Plot Spectrum
+  If enabled it will plot the spectrum represented by the workspace index in the 
+  neighbouring spin box. This workspace index is the index of the spectrum within the 
+  workspace selected in the combobox.
 
 Save Result
   If enabled the result will be saved as a NeXus file in the default save
@@ -178,11 +180,16 @@ Save Result
   directory.
   
 Tiled Plot
-  Produces a tiled plot of the output workspaces generated.
+  Produces a tiled plot of spectra included within the range for the output workspaces 
+  generated. There is a maximum of 18 spectra allowed for a tiled plot. 
 
 Monte Carlo Error Calculation - Number Of Iterations
-  The number of iterations to perform in the Monte Carlo routine for error
-  calculation in I(Q,t)
+  The number of iterations to perform in the Monte Carlo routine for error calculation 
+  in I(Q,t). 
+
+Monte Carlo Error Calculation - Calculate Errors
+  The calculation of errors using a Monte Carlo implementation can be skipped by ticking 
+  the Calculate Errors checkbox.
 
 A note on Binning
 ~~~~~~~~~~~~~~~~~
@@ -271,11 +278,11 @@ Options
 
 Sample
   Either a reduced file (*_red.nxs*) or workspace (*_red*) or an :math:`S(Q,
-  \omega)` file (*_sqw.nxs*) or workspace (*_sqw*).
+  \omega)` file (*_sqw.nxs*, *_sqw.dave*) or workspace (*_sqw*).
 
 Resolution
   Either a resolution file (_res.nxs) or workspace (_res) or an :math:`S(Q,
-  \omega)` file (*_sqw.nxs*) or workspace (*_sqw*).
+  \omega)` file (*_sqw.nxs*, *_sqw.dave*) or workspace (*_sqw*).
 
 Use Delta Function
   Found under 'Custom Function Groups'. Enables use of a delta function.
@@ -424,10 +431,11 @@ The 'Plot Guess' check-box can be used to enable/disable the guess curve in the
 Output
 ~~~~~~
 
-The results of the fit may be plot and saved under the 'Output' section of the fitting interfaces.
+The results of the fit may be plotted and saved under the 'Output' section of the fitting interfaces.
 
 Next to the 'Plot Output' label, you can select a parameter to plot and then click 'Plot' to plot it across the
-fit spectra (if multiple data-sets have been used, a separate plot will be produced for each data-set).
+fit spectra (if multiple data-sets have been used, a separate plot will be produced for each data-set). 
+The 'Plot Output' options will be disabled after a fit if there is only one data point for the parameters.
 
 Clicking the 'Save Result' button will save the result of the fit to your default save location.
 
@@ -481,9 +489,9 @@ input workspace, using the fitted values from the previous spectrum as input
 values for fitting the next. This is done by means of the
 :ref:`IqtFitSequential <algm-IqtFitSequential>` algorithm.
 
-A sequential fit is run by clicking the Run button at the bottom of the tab, a
-single fit can be done using the Fit Single Spectrum button underneath the
-preview plot.
+A sequential fit is run by clicking the Run button seen just above the output 
+options, a single fit can be done using the Fit Single Spectrum button underneath 
+the preview plot.
 
 Spectrum Selection
 ~~~~~~~~~~~~~~~~~~
diff --git a/docs/source/release/v3.14.0/diffraction.rst b/docs/source/release/v3.14.0/diffraction.rst
index ba61cc82b62e7a67f4a6db354a82645ff150940a..d246621cd187cdb6be9a77c3e129e98fef3bf5e1 100644
--- a/docs/source/release/v3.14.0/diffraction.rst
+++ b/docs/source/release/v3.14.0/diffraction.rst
@@ -40,6 +40,7 @@ Improvements
 - :ref:`SaveIsawPeaks <algm-SaveIsawPeaks>` now has option to renumber peaks sequentially.
 - SCD Event Data Reduction Diffraction Interface now has option to create MD HKL workspace.
 - :ref:`IntegratePeaksUsingClusters <algm-IntegratePeaksUsingClusters>` will now treat NaN's as background.
+- :ref:`SetCrystalLocation <algm-SetCrystalLocation>` is a new algorithm to set the sample location in events workspaces.
 
 Bugfixes
 ########
@@ -50,7 +51,7 @@ Bugfixes
 - :ref:`SaveIsawPeaks <algm-SaveIsawPeaks>` does not have duplicate peak numbers when saving PeaksWorkspaces with more than one RunNumber.
 - :ref:`LoadIsawPeaks <algm-LoadIsawPeaks>` now loads the calibration from the peaks file correctly.
 
-- :ref:`OptimizeCrystalPlacement <algm-OptimizeCrystalPlacement>` now updates the sample location used by peaks.  Previously, the sample was effectively left unmoved.
+- :ref:`OptimizeCrystalPlacement <algm-OptimizeCrystalPlacement>` now updates the sample location used by peaks.  Previously, the sample was effectively left unmoved. Default for indexing tolerance was lowered to 0.15.
 
 Powder Diffraction
 ------------------
diff --git a/docs/source/release/v3.14.0/framework.rst b/docs/source/release/v3.14.0/framework.rst
index 91970dd044d0280a2f8b25e84ecf3776542ff319..cb09c9f08cd405141067812468a618e5bf6166ee 100644
--- a/docs/source/release/v3.14.0/framework.rst
+++ b/docs/source/release/v3.14.0/framework.rst
@@ -38,7 +38,7 @@ Archive Searching
 SNS / ONCat
 ###########
 
-- SNS file searching has been moved to `ONCAT <https://oncat.ornl.gov/>`_
+- SNS file searching has been moved to `ONCAT <https://oncat.ornl.gov/>`_. Due to auto-updating of the ``Facilities.xml``, this was done by directing ``SNSDataSearch`` and ``ORNLDataSearch`` to both use ONCAT.
 - For HFIR instruments that write out raw files with run numbers, we have enabled functionality that allows for the searching of file locations by making calls to ONCat.  To use this, make sure that the "Search Data Archive" option is checked in your "Manage User Directories" settings.  The ``FileFinder`` and algorithms such as :ref:`Load <algm-Load>`  will then accept inputs such as "``HB2C_143210``".
 
 ISIS / ICat
@@ -84,6 +84,8 @@ Improvements
 - :ref:`GroupDetectors <algm-GroupDetectors>` now takes masked bins correctly into account when processing histogram workspaces.
 - :ref:`SaveNexusProcessed <algm-SaveNexusProcessed>` and :ref:`LoadNexusProcessed <algm-LoadNexusProcessed>` can now save and load a ``MaskWorkspace``.
 - :ref:`FitPeaks <algm-FitPeaks>` can output parameters' uncertainty (fitting error) in an optional workspace.
+- The documentation in :ref:`EventFiltering` and :ref:`FilterEvents <algm-FilterEvents>` have been extensively rewritten to aid in understanding what the code does.
+- All of the numerical integration based absorption corrections which use :ref:`AbsorptionCorrection <algm-AbsorptionCorrection>` will generate an exception when they fail to generate a gauge volume. Previously, they would silently generate a correction workspace that was all not-a-number (``NAN``).
 
 Bugfixes
 ########
@@ -100,8 +102,10 @@ Bugfixes
 - The output workspace now keeps the units of the input workspace for all sample log entries of algorithms :ref:`MergeRuns <algm-MergeRuns>` and :ref:`ConjoinXRuns <algm-ConjoinXRuns>`.
 - History for algorithms that took groups sometimes would get incorrect history causing history to be incomplete, so now full group history is saved for all items belonging to the group.
 - Fixed a bug in `SetGoniometer <algm-SetGoniometer>` where it would use the mean log value rather than the time series average value for goniometer angles.
+- Fixed a bug in `AlignAndFocusPowderFromFiles <algm-AlignAndFocusPowderFromFiles>` for using the passed on CompressTolerance and CompressWallClockTolerance in the child `CompressEvents <algm-CompressEvents>` algorithm instead of just in the child `AlignAndFocusPowder <algm-AlignAndFocusPowder>` algorithm.
 - `ConvertToMD <algm-ConvertToMD>` now uses the time-average value for logs when using them as ``OtherDimensions``
 - The input validator is fixed in :ref:`MostLikelyMean <algm-MostLikelyMean>` avoiding a segmentation fault.
+- Fixed a bug in `AlignAndFocusPowder <algm-AlignAndFocusPowder>` where a histogram input workspace did not clone propertly to the output workspace and properly masking a grouping workspace passed to `DiffractionFocussing <algm-DiffractionFocussing>`. Also adds initial unit tests for `AlignAndFocusPowder <algm-AlignAndFocusPowder>`.
 - Fixed a bug in :ref:`ExtractSpectra <algm-ExtractSpectra>` which was causing a wrong last value in the output's vertical axis if the axis type was ``BinEdgeAxis``.
 
 Python
diff --git a/docs/source/release/v3.14.0/indirect_inelastic.rst b/docs/source/release/v3.14.0/indirect_inelastic.rst
index df1cd2d56ce6bb9a19f7367781c6f01e3cc8534a..ed3b69dfa71e577ef24ee234458b5929af15ae16 100644
--- a/docs/source/release/v3.14.0/indirect_inelastic.rst
+++ b/docs/source/release/v3.14.0/indirect_inelastic.rst
@@ -35,7 +35,7 @@ Improvements
 - When the InelasticDiffSphere, InelasticDiffRotDiscreteCircle, ElasticDiffSphere or ElasticDiffRotDiscreteCircle
   Fit Types are selected in the ConvFit Tab, the Q values are retrieved from the workspaces, preventing a crash
   when plotting a guess.
-- The Plot buttons in MSDFit, I(Q,t)Fit, ConvFit and F(Q)Fit are disabled after a Run when the result workspace only 
+- The Plot buttons in MSDFit, I(Q,t)Fit, ConvFit and F(Q)Fit are disabled after a Run when the result workspace only
   has one data point to plot.
 - There is now an option to choose which output parameter to plot in MSDFit.
 - An option to skip the calculation of Monte Carlo Errors on the I(Q,t) Tab has been added.
@@ -46,6 +46,9 @@ Improvements
 - The WorkspaceIndex and Q value in the FitPropertyBrowser are now updated when the Plot Spectrum number is changed.
   This improvement can be seen in ConvFit when functions which depend on Q value are selected.
 - Fit and Fit Sequential in the Fit combobox above the FitPropertyBrowser are now disabled while fitting is taking place.
+- The option to choose which workspace index to Plot Spectrum for and from which output workspace is now given in Elwin.
+- ConvFit now allows the loading of Dave ASCII files which end with '_sqw.dave'.
+
 
 Bugfixes
 ########
@@ -62,7 +65,7 @@ Bugfixes
 - A bug where fixed parameters don't remain fixed when using the FABADA minimizer in ConvFit has been corrected.
 - The expression for the Fit type Yi in MSDFit was incorrect and has now been corrected.
 - The x-axis labels in the output plots for MSDFit are now correct.
-- An unexpected error is now prevented when clicking Plot Guess from the Display combo box in ConvFit without first loading 
+- An unexpected error is now prevented when clicking Plot Guess from the Display combo box in ConvFit without first loading
   a reduced file.
 
 
diff --git a/docs/source/release/v3.14.0/ui.rst b/docs/source/release/v3.14.0/ui.rst
index 144282194067b0940c97c670cc64ad88b20bdf90..01c92e3a433d5d0378a2b31fb13857931391bd38 100644
--- a/docs/source/release/v3.14.0/ui.rst
+++ b/docs/source/release/v3.14.0/ui.rst
@@ -80,5 +80,16 @@ BugFixes
 
 - Fixed issue where an open set of data from ITableWorkspace wouldn't update if the data was changed via python
 - Fixed an issue where MantidPlot would crash when renaming workspaces.
+- Fixed issue with filenames containing spaces that are passed to Mantid when launched from the command line
+
+MantidWorkbench
+---------------
+
+Changes
+#######
+- Colorfill plots with uniform bin widths were made more responsive by resampling to 4K resolution and using :func:`~mantid.plots.MantidAxes.imshow`.
+
+BugFixes
+########
 
 :ref:`Release 3.14.0 <v3.14.0>`
diff --git a/instrument/D22_Parameters.xml b/instrument/D22_Parameters.xml
index 1e06905a671c9172b4b599b03340229c01d66666..b2bc05852637c405b732b91f10e773bbfe6a3ec5 100644
--- a/instrument/D22_Parameters.xml
+++ b/instrument/D22_Parameters.xml
@@ -21,7 +21,7 @@
     <component-link name="detector">
 
     <parameter name="parallax" type="string">
-		  <value val="1+0.14*exp(-4*log(2.)*((t-0.588)/0.414)^2)"/>
+		  <value val="1+0.14*exp(-4*ln(2.)*((t-0.588)/0.414)^2)"/>
 		</parameter>
 
     <parameter name="direction" type="string">
diff --git a/instrument/D22lr_Parameters.xml b/instrument/D22lr_Parameters.xml
index 79ef430cdd8cc93a351e32e4fde7bdd620e32aa0..4c91ab7050f99f8dc7a2d044118c78ac724f99ba 100644
--- a/instrument/D22lr_Parameters.xml
+++ b/instrument/D22lr_Parameters.xml
@@ -20,7 +20,7 @@
     <component-link name="detector">
 
     <parameter name="parallax" type="string">
-		  <value val="1+0.14*exp(-4*log(2.)*((abs(t)-0.588)/0.414)^2)"/>
+		  <value val="1+0.14*exp(-4*ln(2.)*((abs(t)-0.588)/0.414)^2)"/>
 		</parameter>
 
     <parameter name="direction" type="string">
diff --git a/instrument/Facilities.xml b/instrument/Facilities.xml
index d97a9656c7a3d7030ea37143a19ca76dbdca0cc0..bf7f57180884cc1f94eb37c15773169f7c383941 100644
--- a/instrument/Facilities.xml
+++ b/instrument/Facilities.xml
@@ -442,7 +442,7 @@
 <facility name="SNS" delimiter="_" FileExtensions=".nxs.h5,_event.nxs,.nxs,.dat,_runinfo.xml,_histo.nxs">
 
    <archive>
-      <archiveSearch plugin="ORNLDataSearch" />
+      <archiveSearch plugin="SNSDataSearch" />
    </archive>
 
    <computeResource name="Fermi">
diff --git a/qt/applications/workbench/workbench/app/mainwindow.py b/qt/applications/workbench/workbench/app/mainwindow.py
index 4022774d702953148e9a21622b3091707ec4d710..32c3715bdf637f14fc4869e57ea067618db866d4 100644
--- a/qt/applications/workbench/workbench/app/mainwindow.py
+++ b/qt/applications/workbench/workbench/app/mainwindow.py
@@ -52,6 +52,7 @@ from mantidqt.widgets.manageuserdirectories import ManageUserDirectories  # noqa
 from mantidqt.widgets.codeeditor.execution import PythonCodeExecution  # noqa
 from mantidqt.utils.qt import (add_actions, create_action, plugins,
                                widget_updates_disabled)  # noqa
+from mantidqt.project.project import Project  # noqa
 
 # Pre-application setup
 plugins.setup_library_paths()
@@ -164,6 +165,9 @@ class MainWindow(QMainWindow):
         # Layout
         self.setDockOptions(self.DOCKOPTIONS)
 
+        # Project
+        self.project = None
+
     def setup(self):
         # menus must be done first so they can be filled by the
         # plugins in register_plugin
@@ -207,6 +211,9 @@ class MainWindow(QMainWindow):
         self.workspacewidget.register_plugin()
         self.widgets.append(self.workspacewidget)
 
+        # Set up the project object
+        self.project = Project()
+
         # uses default configuration as necessary
         self.readSettings(CONF)
 
@@ -238,26 +245,29 @@ class MainWindow(QMainWindow):
     def create_actions(self):
         # --- general application menu options --
         # file menu
-        action_open = create_action(self, "Open",
+        action_open = create_action(self, "Open Script",
                                     on_triggered=self.open_file,
                                     shortcut="Ctrl+O",
-                                    shortcut_context=Qt.ApplicationShortcut,
-                                    icon_name="fa.folder-open")
-        action_save = create_action(self, "Save",
-                                    on_triggered=self.save_file,
-                                    shortcut="Ctrl+S",
-                                    shortcut_context=Qt.ApplicationShortcut,
-                                    icon_name="fa.save")
+                                    shortcut_context=Qt.ApplicationShortcut)
+        action_load_project = create_action(self, "Open Project",
+                                            on_triggered=self.load_project)
+        action_save_script = create_action(self, "Save Script",
+                                           on_triggered=self.save_script,
+                                           shortcut="Ctrl+S",
+                                           shortcut_context=Qt.ApplicationShortcut)
+        action_save_project = create_action(self, "Save Project",
+                                            on_triggered=self.save_project)
+        action_save_project_as = create_action(self, "Save Project as...",
+                                               on_triggered=self.save_project_as)
+
         action_manage_directories = create_action(self, "Manage User Directories",
-                                                  on_triggered=self.open_manage_directories,
-                                                  icon_name="fa.folder")
+                                                  on_triggered=self.open_manage_directories)
 
         action_quit = create_action(self, "&Quit", on_triggered=self.close,
                                     shortcut="Ctrl+Q",
-                                    shortcut_context=Qt.ApplicationShortcut,
-                                    icon_name="fa.power-off")
-        self.file_menu_actions = [action_open, action_save, action_manage_directories, None, action_quit]
-
+                                    shortcut_context=Qt.ApplicationShortcut)
+        self.file_menu_actions = [action_open, action_load_project, None, action_save_script, action_save_project,
+                                  action_save_project_as, None, action_manage_directories, None, action_quit]
         # view menu
         action_restore_default = create_action(self, "Restore Default Layout",
                                                on_triggered=self.prep_window_for_reset,
@@ -401,6 +411,14 @@ class MainWindow(QMainWindow):
 
     # ----------------------- Events ---------------------------------
     def closeEvent(self, event):
+        # Check whether or not to save project
+        if not self.project.saved:
+            # Offer save
+            if self.project.offer_save(self):
+                # Cancel has been clicked
+                event.ignore()
+                return
+
         # Close editors
         if self.editor.app_closing():
             self.writeSettings(CONF)  # write current window information to global settings object
@@ -428,10 +446,18 @@ class MainWindow(QMainWindow):
             return
         self.editor.open_file_in_new_tab(filepath)
 
-    def save_file(self):
-        # todo: how should this interact with project saving and workspaces when they are implemented?
+    def save_script(self):
         self.editor.save_current_file()
 
+    def save_project(self):
+        self.project.save()
+
+    def save_project_as(self):
+        self.project.save_as()
+
+    def load_project(self):
+        self.project.load()
+
     def open_manage_directories(self):
         ManageUserDirectories(self).exec_()
 
diff --git a/qt/applications/workbench/workbench/plotting/functions.py b/qt/applications/workbench/workbench/plotting/functions.py
index f62784dba10b936a7933c82909a59a2d9762c5fe..51b071094d87089e529525d740ec7fb95278f5af 100644
--- a/qt/applications/workbench/workbench/plotting/functions.py
+++ b/qt/applications/workbench/workbench/plotting/functions.py
@@ -202,12 +202,12 @@ def use_imshow(ws):
 
     x = ws.dataX(0)
     difference = np.diff(x)
-    if not np.all(np.isclose(difference, difference[0])):
+    if not np.all(np.isclose(difference[:-1], difference[0])):
         return False
 
     y = ws.getAxis(1).extractValues()
     difference = np.diff(y)
-    if not np.all(np.isclose(difference, difference[0])):
+    if not np.all(np.isclose(difference[:-1], difference[0])):
         return False
 
     return True
diff --git a/qt/python/CMakeLists.txt b/qt/python/CMakeLists.txt
index 68e6e4ef408e8f17119fbff8b417137ddae2a7a0..e7743c40a34e7defc208a1f63fd97246b64ebc47 100644
--- a/qt/python/CMakeLists.txt
+++ b/qt/python/CMakeLists.txt
@@ -69,6 +69,12 @@ endif ()
     mantidqt/dialogs/test/test_algorithm_dialog.py
     mantidqt/dialogs/test/test_spectraselectiondialog.py
 
+    mantidqt/project/test/test_project.py
+    mantidqt/project/test/test_projectloader.py
+    mantidqt/project/test/test_projectsaver.py
+    mantidqt/project/test/test_workspaceloader.py
+    mantidqt/project/test/test_workspacesaver.py
+
     mantidqt/utils/test/test_async.py
     mantidqt/utils/test/test_modal_tester.py
     mantidqt/utils/test/test_qt_utils.py
diff --git a/qt/python/mantidqt/io.py b/qt/python/mantidqt/io.py
index c4736787b2419f0b4f65bf94975dea17250d6515..26ca99d3d6ef867ae77d13e97b45a2f4b7a98f2b 100644
--- a/qt/python/mantidqt/io.py
+++ b/qt/python/mantidqt/io.py
@@ -8,9 +8,10 @@
 #
 
 import os.path
-from qtpy.QtWidgets import QFileDialog
+from qtpy.QtWidgets import QFileDialog, QDialog
 from qtpy.QtCore import QDir
 
+# For storing a persistent directory of where the last file was saved to.
 _LAST_SAVE_DIRECTORY = None
 
 
@@ -24,7 +25,7 @@ def open_a_file_dialog(parent=None,  default_suffix=None, directory=None, file_f
     :param file_filter: String; The filter name and file type e.g. "Python files (*.py)"
     :param accept_mode: enum AcceptMode; Defines the AcceptMode of the dialog, check QFileDialog Class for details
     :param file_mode: enum FileMode; Defines the FileMode of the dialog, check QFileDialog Class for details
-    :return: String; The filename that was selected, it is possible to return a directory so look out for that.
+    :return: String; The filename that was selected, it is possible to return a directory so look out for that
     """
     global _LAST_SAVE_DIRECTORY
     dialog = QFileDialog(parent)
@@ -55,10 +56,12 @@ def open_a_file_dialog(parent=None,  default_suffix=None, directory=None, file_f
     dialog.fileSelected.connect(_set_last_save)
 
     # Wait for dialog to finish before allowing continuation of code
-    dialog.exec_()
+    if dialog.exec_() == QDialog.Rejected:
+        return None
 
     filename = _LAST_SAVE_DIRECTORY
-    # Make sure that the _LAST_SAVE_DIRECTORY is set
+
+    # Make sure that the _LAST_SAVE_DIRECTORY is set as a directory
     if _LAST_SAVE_DIRECTORY is not None and not os.path.isdir(_LAST_SAVE_DIRECTORY):
         # Remove the file for last directory
         _LAST_SAVE_DIRECTORY = os.path.dirname(os.path.abspath(_LAST_SAVE_DIRECTORY))
@@ -68,7 +71,7 @@ def open_a_file_dialog(parent=None,  default_suffix=None, directory=None, file_f
 
 def _set_last_save(filename):
     """
-    Uses the global _LOCAL_SAVE_DIRECTORY to store output from connected signal
+    Uses the global _LAST_SAVE_DIRECTORY to store output from connected signal
     :param filename: String; Value to set _LAST_SAVE_DIRECTORY
     """
     global _LAST_SAVE_DIRECTORY
diff --git a/qt/python/mantidqt/project/__init__.py b/qt/python/mantidqt/project/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..20b85731aa0c9565f89493f598ca368a49d707b3
--- /dev/null
+++ b/qt/python/mantidqt/project/__init__.py
@@ -0,0 +1,8 @@
+# Mantid Repository : https://github.com/mantidproject/mantid
+#
+# Copyright &copy; 2017 ISIS Rutherford Appleton Laboratory UKRI,
+#     NScD Oak Ridge National Laboratory, European Spallation Source
+#     & Institut Laue - Langevin
+# SPDX - License - Identifier: GPL - 3.0 +
+#  This file is part of the mantidqt package
+#
diff --git a/qt/python/mantidqt/project/project.py b/qt/python/mantidqt/project/project.py
new file mode 100644
index 0000000000000000000000000000000000000000..aa61a5546674f11265b9e73df3f53584d59432c6
--- /dev/null
+++ b/qt/python/mantidqt/project/project.py
@@ -0,0 +1,163 @@
+# Mantid Repository : https://github.com/mantidproject/mantid
+#
+# Copyright &copy; 2017 ISIS Rutherford Appleton Laboratory UKRI,
+#     NScD Oak Ridge National Laboratory, European Spallation Source
+#     & Institut Laue - Langevin
+# SPDX - License - Identifier: GPL - 3.0 +
+#  This file is part of the mantidqt package
+#
+from __future__ import (absolute_import, division, print_function, unicode_literals)
+
+import os
+from qtpy.QtWidgets import QFileDialog, QMessageBox
+from qtpy.QtGui import QIcon  # noqa
+
+from mantid.api import AnalysisDataService, AnalysisDataServiceObserver
+from mantidqt.io import open_a_file_dialog
+from mantidqt.project.projectloader import ProjectLoader
+from mantidqt.project.projectsaver import ProjectSaver
+
+
+class Project(AnalysisDataServiceObserver):
+    def __init__(self):
+        super(Project, self).__init__()
+        # Has the project been saved, to Access this call .saved
+        self.__saved = True
+
+        # Last save locations
+        self.last_project_location = None
+
+        self.observeAll(True)
+
+        self.project_file_ext = ".mtdproj"
+
+    def __get_saved(self):
+        return self.__saved
+
+    saved = property(__get_saved)
+
+    def save(self):
+        """
+        The function that is called if the save button is clicked on the mainwindow
+        :return: None; if the user cancels
+        """
+        if self.last_project_location is None:
+            return self.save_as()
+        else:
+            # Offer an are you sure? overwriting GUI
+            answer = self._offer_overwriting_gui()
+
+            if answer == QMessageBox.Yes:
+                # Actually save
+                self._save()
+            # Else do nothing
+
+    def save_as(self):
+        """
+        The function that is called if the save as... button is clicked on the mainwindow
+        :return: None; if the user cancels.
+        """
+        path = self._save_file_dialog()
+        if path is None:
+            # Cancel close dialogs
+            return
+
+        overwriting = False
+        # If the selected path is a project directory ask if overwrite is required?
+        if os.path.exists(os.path.join(path, (os.path.basename(path) + self.project_file_ext))):
+            answer = self._offer_overwriting_gui()
+            if answer == QMessageBox.No:
+                return
+            elif answer == QMessageBox.Yes:
+                overwriting = True
+
+        if not overwriting and os.path.exists(path) and os.listdir(path) != []:
+            QMessageBox.warning(None, "Empty directory or project required!",
+                                "Please choose either an new directory or an already saved project", QMessageBox.Ok)
+            return
+
+        # todo: get a list of workspaces but to be implemented on GUI implementation
+        self.last_project_location = path
+        self._save()
+
+    @staticmethod
+    def _offer_overwriting_gui():
+        """
+        Offers up a overwriting QMessageBox giving the option to overwrite a project, and returns the reply.
+        :return: QMessaageBox.Yes or QMessageBox.No; The value is the value selected by the user.
+        """
+        return QMessageBox.question(None, "Overwrite project?",
+                                    "Would you like to overwrite the selected project?",
+                                    QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
+
+    @staticmethod
+    def _save_file_dialog():
+        return open_a_file_dialog(accept_mode=QFileDialog.AcceptSave, file_mode=QFileDialog.Directory)
+
+    def _save(self):
+        workspaces_to_save = AnalysisDataService.getObjectNames()
+        project_saver = ProjectSaver(self.project_file_ext)
+        project_saver.save_project(directory=self.last_project_location, workspace_to_save=workspaces_to_save)
+        self.__saved = True
+
+    def load(self):
+        """
+        The event that is called when open project is clicked on the main window
+        :return: None; if the user cancelled.
+        """
+        file_name = self._load_file_dialog()
+        if file_name is None:
+            # Cancel close dialogs
+            return
+
+        # Sanity check
+        _, file_ext = os.path.splitext(file_name)
+
+        if file_ext != ".mtdproj":
+            QMessageBox.warning(None, "Wrong file type!", "Please select a valid project file", QMessageBox.Ok)
+
+        directory = os.path.dirname(file_name)
+
+        project_loader = ProjectLoader(self.project_file_ext)
+        project_loader.load_project(directory)
+        self.last_project_location = directory
+        self.__saved = True
+
+    def _load_file_dialog(self):
+        return open_a_file_dialog(accept_mode=QFileDialog.AcceptOpen, file_mode=QFileDialog.ExistingFile,
+                                  file_filter="Project files ( *" + self.project_file_ext + ")")
+
+    def offer_save(self, parent):
+        """
+        :param parent: QWidget; Parent of the QMessageBox that is popped up
+        :return: Bool; Returns false if no save needed/save complete. Returns True if need to cancel closing. However
+                        will return None if self.__saved is false
+        """
+        # If the current project is saved then return and don't do anything
+        if self.__saved:
+            return
+
+        result = self._offer_save_message_box(parent)
+
+        if result == QMessageBox.Yes:
+            self.save()
+        elif result == QMessageBox.Cancel:
+            return True
+        # if yes or no return false
+        return False
+
+    @staticmethod
+    def _offer_save_message_box(parent):
+        return QMessageBox.question(parent, 'Unsaved Project', "The project is currently unsaved would you like to "
+                                    "save before closing?", QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel,
+                                    QMessageBox.Yes)
+
+    def modified_project(self):
+        self.__saved = False
+
+    def anyChangeHandle(self):
+        """
+        The method that will be triggered if any of the changes in the ADS have occurred, that are checked for using the
+        AnalysisDataServiceObserver class' observeAll method
+        """
+        self.modified_project()
diff --git a/qt/python/mantidqt/project/projectloader.py b/qt/python/mantidqt/project/projectloader.py
new file mode 100644
index 0000000000000000000000000000000000000000..ecb2281d3cf0595863dc965f3bca378e445b27f0
--- /dev/null
+++ b/qt/python/mantidqt/project/projectloader.py
@@ -0,0 +1,69 @@
+# Mantid Repository : https://github.com/mantidproject/mantid
+#
+# Copyright &copy; 2017 ISIS Rutherford Appleton Laboratory UKRI,
+#     NScD Oak Ridge National Laboratory, European Spallation Source
+#     & Institut Laue - Langevin
+# SPDX - License - Identifier: GPL - 3.0 +
+#  This file is part of the mantidqt package
+#
+from __future__ import (absolute_import, division, print_function, unicode_literals)
+
+import json
+import os
+
+from mantidqt.project import workspaceloader
+from mantid import AnalysisDataService as ADS
+from mantid import logger
+
+
+def _confirm_all_workspaces_loaded(workspaces_to_confirm):
+    current_workspaces = ADS.getObjectNames()
+    for ws in workspaces_to_confirm:
+        if ws not in current_workspaces:
+            logger.warning("Project Loader was unable to load back all of project workspaces")
+            return False
+    return True
+
+
+class ProjectLoader(object):
+    def __init__(self, project_file_ext):
+        self.project_reader = ProjectReader(project_file_ext)
+        self.workspace_loader = workspaceloader.WorkspaceLoader()
+        self.project_file_ext = project_file_ext
+
+    def load_project(self, directory):
+        """
+        Will load the project in the given directory
+        :param directory: String or string castable object; the directory of the project
+        :return: Bool; True if all workspace loaded successfully, False if not loaded successfully.
+        """
+        # It can be expected that if at this point it is NoneType that it's an error
+        if directory is None:
+            return
+
+        # Read project
+        self.project_reader.read_project(directory)
+
+        # Load in the workspaces
+        self.workspace_loader.load_workspaces(directory=directory,
+                                              workspaces_to_load=self.project_reader.workspace_names)
+        return _confirm_all_workspaces_loaded(workspaces_to_confirm=self.project_reader.workspace_names)
+
+
+class ProjectReader(object):
+    def __init__(self, project_file_ext):
+        self.workspace_names = None
+        self.plot_dicts = None
+        self.project_file_ext = project_file_ext
+
+    def read_project(self, directory):
+        """
+        Will read the project file in from the directory that is given.
+        :param directory: String or string castable object; the directory of the project
+        """
+        try:
+            with open(os.path.join(directory, (os.path.basename(directory) + self.project_file_ext))) as f:
+                json_data = json.load(f)
+                self.workspace_names = json_data["workspaces"]
+        except Exception:
+            logger.warning("JSON project file unable to be loaded/read")
diff --git a/qt/python/mantidqt/project/projectsaver.py b/qt/python/mantidqt/project/projectsaver.py
new file mode 100644
index 0000000000000000000000000000000000000000..175f9a8af8fe5f5b3339bc828c0d74c0f77dfce2
--- /dev/null
+++ b/qt/python/mantidqt/project/projectsaver.py
@@ -0,0 +1,70 @@
+# Mantid Repository : https://github.com/mantidproject/mantid
+#
+# Copyright &copy; 2017 ISIS Rutherford Appleton Laboratory UKRI,
+#     NScD Oak Ridge National Laboratory, European Spallation Source
+#     & Institut Laue - Langevin
+# SPDX - License - Identifier: GPL - 3.0 +
+#  This file is part of the mantidqt package
+#
+from __future__ import (absolute_import, division, print_function, unicode_literals)
+
+from json import dump
+import os
+
+from mantidqt.project import workspacesaver
+from mantid import logger
+
+
+class ProjectSaver(object):
+    def __init__(self, project_file_ext):
+        self.project_file_ext = project_file_ext
+
+    def save_project(self, directory, workspace_to_save=None):
+        """
+        The method that will actually save the project and call relevant savers for workspaces, plots, interfaces etc.
+        :param directory: String; The directory of the
+        :param workspace_to_save: List; of Strings that will have workspace names in it, if None will save all
+        :return: None; If the method cannot be completed.
+        """
+        # Check if the directory doesn't exist
+        if directory is None:
+            logger.warning("Can not save to empty directory")
+            return
+
+        # Check this isn't saving a blank project file
+        if workspace_to_save is None:
+            logger.warning("Can not save an empty project")
+            return
+
+        # Save workspaces to that location
+        workspace_saver = workspacesaver.WorkspaceSaver(directory=directory)
+        workspace_saver.save_workspaces(workspaces_to_save=workspace_to_save)
+
+        # Pass dicts to Project Writer
+        writer = ProjectWriter(directory, workspace_saver.get_output_list(),
+                               self.project_file_ext)
+        writer.write_out()
+
+
+class ProjectWriter(object):
+    def __init__(self, save_location, workspace_names, project_file_ext):
+        self.workspace_names = workspace_names
+        self.directory = save_location
+        self.project_file_ext = project_file_ext
+
+    def write_out(self):
+        """
+        Write out the project file that contains workspace names, interfaces information, plot preferences etc.
+        """
+        # Get the JSON string versions
+        to_save_dict = {"workspaces": self.workspace_names}
+
+        # Open file and save the string to it alongside the workspace_names
+        if not os.path.isdir(self.directory):
+            os.makedirs(self.directory)
+        file_path = os.path.join(self.directory, (os.path.basename(self.directory) + self.project_file_ext))
+        try:
+            with open(file_path, "w+") as f:
+                dump(obj=to_save_dict, fp=f)
+        except Exception:
+            logger.warning("JSON project file unable to be opened/written to")
diff --git a/qt/python/mantidqt/project/test/__init__.py b/qt/python/mantidqt/project/test/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..20b85731aa0c9565f89493f598ca368a49d707b3
--- /dev/null
+++ b/qt/python/mantidqt/project/test/__init__.py
@@ -0,0 +1,8 @@
+# Mantid Repository : https://github.com/mantidproject/mantid
+#
+# Copyright &copy; 2017 ISIS Rutherford Appleton Laboratory UKRI,
+#     NScD Oak Ridge National Laboratory, European Spallation Source
+#     & Institut Laue - Langevin
+# SPDX - License - Identifier: GPL - 3.0 +
+#  This file is part of the mantidqt package
+#
diff --git a/qt/python/mantidqt/project/test/test_project.py b/qt/python/mantidqt/project/test/test_project.py
new file mode 100644
index 0000000000000000000000000000000000000000..c92e8e1e4fcb183977157e6c0096a4317c164e4c
--- /dev/null
+++ b/qt/python/mantidqt/project/test/test_project.py
@@ -0,0 +1,158 @@
+# Mantid Repository : https://github.com/mantidproject/mantid
+#
+# Copyright &copy; 2017 ISIS Rutherford Appleton Laboratory UKRI,
+#     NScD Oak Ridge National Laboratory, European Spallation Source
+#     & Institut Laue - Langevin
+# SPDX - License - Identifier: GPL - 3.0 +
+#  This file is part of the mantidqt package
+#
+from __future__ import (absolute_import, division, print_function, unicode_literals)
+
+import unittest
+import sys
+import tempfile
+import os
+
+from qtpy.QtWidgets import QMessageBox
+
+from mantid.simpleapi import CreateSampleWorkspace, GroupWorkspaces, RenameWorkspace, UnGroupWorkspace
+from mantid.api import AnalysisDataService as ADS
+from mantidqt.project.project import Project
+
+if sys.version_info.major == 3:
+    from unittest import mock
+else:
+    import mock
+
+
+class ProjectTest(unittest.TestCase):
+    def setUp(self):
+        self.project = Project()
+
+    def tearDown(self):
+        ADS.clear()
+
+    def test_save_calls_save_as_when_last_location_is_not_none(self):
+        self.project.save_as = mock.MagicMock()
+        self.project.save()
+        self.assertEqual(self.project.save_as.call_count, 1)
+
+    def test_save_does_not_call_save_as_when_last_location_is_not_none(self):
+        self.project.save_as = mock.MagicMock()
+        self.project.last_project_location = "1"
+        self.assertEqual(self.project.save_as.call_count, 0)
+
+    def test_save_saves_project_successfully(self):
+        working_directory = tempfile.mkdtemp()
+        self.project.last_project_location = working_directory
+        CreateSampleWorkspace(OutputWorkspace="ws1")
+        self.project._offer_overwriting_gui = mock.MagicMock(return_value=QMessageBox.Yes)
+
+        self.project.save()
+
+        self.assertTrue(os.path.isdir(working_directory))
+        file_list = os.listdir(working_directory)
+        self.assertTrue(os.path.basename(working_directory) + ".mtdproj" in file_list)
+        self.assertTrue("ws1.nxs" in file_list)
+        self.assertEqual(self.project._offer_overwriting_gui.call_count, 1)
+
+    def test_save_as_saves_project_successfully(self):
+        working_directory = tempfile.mkdtemp()
+        self.project._save_file_dialog = mock.MagicMock(return_value=working_directory)
+        CreateSampleWorkspace(OutputWorkspace="ws1")
+
+        self.project.save_as()
+
+        self.assertEqual(self.project._save_file_dialog.call_count, 1)
+        self.assertTrue(os.path.isdir(working_directory))
+        file_list = os.listdir(working_directory)
+        self.assertTrue(os.path.basename(working_directory) + ".mtdproj" in file_list)
+        self.assertTrue("ws1.nxs" in file_list)
+
+    def test_load_calls_loads_successfully(self):
+        working_directory = tempfile.mkdtemp()
+        return_value_for_load = os.path.join(working_directory, os.path.basename(working_directory) + ".mtdproj")
+        self.project._save_file_dialog = mock.MagicMock(return_value=working_directory)
+        CreateSampleWorkspace(OutputWorkspace="ws1")
+        self.project.save_as()
+
+        self.assertEqual(self.project._save_file_dialog.call_count, 1)
+        ADS.clear()
+
+        self.project._load_file_dialog = mock.MagicMock(return_value=return_value_for_load)
+        self.project.load()
+        self.assertEqual(self.project._load_file_dialog.call_count, 1)
+        self.assertEqual(["ws1"], ADS.getObjectNames())
+
+    def test_offer_save_does_nothing_if_saved_is_true(self):
+        self.assertEqual(self.project.offer_save(None), None)
+
+    def test_offer_save_does_something_if_saved_is_false(self):
+        self.project._offer_save_message_box = mock.MagicMock(return_value=QMessageBox.Yes)
+        self.project.save = mock.MagicMock()
+
+        # Add something to the ads so __saved is set to false
+        CreateSampleWorkspace(OutputWorkspace="ws1")
+
+        self.assertEqual(self.project.offer_save(None), False)
+        self.assertEqual(self.project.save.call_count, 1)
+        self.assertEqual(self.project._offer_save_message_box.call_count, 1)
+
+    def test_adding_to_ads_calls_any_change_handle(self):
+        self.project.anyChangeHandle = mock.MagicMock()
+        CreateSampleWorkspace(OutputWorkspace="ws1")
+
+        self.assertEqual(1, self.project.anyChangeHandle.call_count)
+
+    def test_removing_from_ads_calls_any_change_handle(self):
+        CreateSampleWorkspace(OutputWorkspace="ws1")
+
+        self.project.anyChangeHandle = mock.MagicMock()
+        ADS.remove("ws1")
+
+        self.assertEqual(1, self.project.anyChangeHandle.call_count)
+
+    def test_grouping_in_ads_calls_any_change_handle(self):
+        CreateSampleWorkspace(OutputWorkspace="ws1")
+        CreateSampleWorkspace(OutputWorkspace="ws2")
+
+        self.project.anyChangeHandle = mock.MagicMock()
+        GroupWorkspaces(InputWorkspaces="ws1,ws2", OutputWorkspace="NewGroup")
+
+        # Called twice because group is made and then added to the ADS
+        self.assertEqual(2, self.project.anyChangeHandle.call_count)
+
+    def test_renaming_in_ads_calls_any_change_handle(self):
+        CreateSampleWorkspace(OutputWorkspace="ws1")
+
+        self.project.anyChangeHandle = mock.MagicMock()
+        RenameWorkspace(InputWorkspace="ws1", OutputWorkspace="ws2")
+
+        # Called twice because first workspace is removed and second is added
+        self.assertEqual(2, self.project.anyChangeHandle.call_count)
+
+    def test_ungrouping_in_ads_calls_any_change_handle(self):
+        CreateSampleWorkspace(OutputWorkspace="ws1")
+        CreateSampleWorkspace(OutputWorkspace="ws2")
+        GroupWorkspaces(InputWorkspaces="ws1,ws2", OutputWorkspace="NewGroup")
+
+        self.project.anyChangeHandle = mock.MagicMock()
+        UnGroupWorkspace(InputWorkspace="NewGroup")
+
+        # 1 for removing old group and 1 for something else but 2 seems right
+        self.assertEqual(2, self.project.anyChangeHandle.call_count)
+
+    def test_group_updated_in_ads_calls_any_change_handle(self):
+        CreateSampleWorkspace(OutputWorkspace="ws1")
+        CreateSampleWorkspace(OutputWorkspace="ws2")
+        GroupWorkspaces(InputWorkspaces="ws1,ws2", OutputWorkspace="NewGroup")
+        CreateSampleWorkspace(OutputWorkspace="ws3")
+
+        self.project.anyChangeHandle = mock.MagicMock()
+        ADS.addToGroup("NewGroup", "ws3")
+
+        self.assertEqual(1, self.project.anyChangeHandle.call_count)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/qt/python/mantidqt/project/test/test_projectloader.py b/qt/python/mantidqt/project/test/test_projectloader.py
new file mode 100644
index 0000000000000000000000000000000000000000..869d9e61bae6d95714dc7b136560210520335e3d
--- /dev/null
+++ b/qt/python/mantidqt/project/test/test_projectloader.py
@@ -0,0 +1,75 @@
+# Mantid Repository : https://github.com/mantidproject/mantid
+#
+# Copyright &copy; 2017 ISIS Rutherford Appleton Laboratory UKRI,
+#     NScD Oak Ridge National Laboratory, European Spallation Source
+#     & Institut Laue - Langevin
+# SPDX - License - Identifier: GPL - 3.0 +
+#  This file is part of the mantidqt package
+#
+
+import unittest
+
+from os.path import isdir
+from shutil import rmtree
+import tempfile
+
+from mantid.api import AnalysisDataService as ADS
+from mantid.simpleapi import CreateSampleWorkspace
+from mantidqt.project import projectloader, projectsaver
+
+
+project_file_ext = ".mtdproj"
+working_directory = tempfile.mkdtemp()
+
+
+class ProjectLoaderTest(unittest.TestCase):
+    def setUp(self):
+        ws1_name = "ws1"
+        ADS.addOrReplace(ws1_name, CreateSampleWorkspace(OutputWorkspace=ws1_name))
+        project_saver = projectsaver.ProjectSaver(project_file_ext)
+        project_saver.save_project(workspace_to_save=[ws1_name], directory=working_directory)
+
+    def tearDown(self):
+        ADS.clear()
+        if isdir(working_directory):
+            rmtree(working_directory)
+
+    def test_project_loading_when_directory_is_none(self):
+        # Tests that error handling of a value being none receives a None back.
+        project_loader = projectloader.ProjectLoader(project_file_ext)
+
+        self.assertEqual(project_loader.load_project(None), None)
+
+    def test_project_loading(self):
+        project_loader = projectloader.ProjectLoader(project_file_ext)
+
+        self.assertTrue(project_loader.load_project(working_directory))
+
+        self.assertEqual(ADS.getObjectNames(), ["ws1"])
+
+    def test_confirm_all_workspaces_loaded(self):
+        ws1_name = "ws1"
+        ADS.addOrReplace(ws1_name, CreateSampleWorkspace(OutputWorkspace=ws1_name))
+        self.assertTrue(projectloader._confirm_all_workspaces_loaded(workspaces_to_confirm=[ws1_name]))
+
+
+class ProjectReaderTest(unittest.TestCase):
+    def setUp(self):
+        ws1_name = "ws1"
+        ADS.addOrReplace(ws1_name, CreateSampleWorkspace(OutputWorkspace=ws1_name))
+        project_saver = projectsaver.ProjectSaver(project_file_ext)
+        project_saver.save_project(workspace_to_save=[ws1_name], directory=working_directory)
+
+    def tearDown(self):
+        ADS.clear()
+        if isdir(working_directory):
+            rmtree(working_directory)
+
+    def test_project_reading(self):
+        project_reader = projectloader.ProjectReader(project_file_ext)
+        project_reader.read_project(working_directory)
+        self.assertEqual(["ws1"], project_reader.workspace_names)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/qt/python/mantidqt/project/test/test_projectsaver.py b/qt/python/mantidqt/project/test/test_projectsaver.py
new file mode 100644
index 0000000000000000000000000000000000000000..b311eca69456e467bd47639533a9d433acc00f92
--- /dev/null
+++ b/qt/python/mantidqt/project/test/test_projectsaver.py
@@ -0,0 +1,147 @@
+# Mantid Repository : https://github.com/mantidproject/mantid
+#
+# Copyright &copy; 2017 ISIS Rutherford Appleton Laboratory UKRI,
+#     NScD Oak Ridge National Laboratory, European Spallation Source
+#     & Institut Laue - Langevin
+# SPDX - License - Identifier: GPL - 3.0 +
+#  This file is part of the mantidqt package
+#
+import unittest
+import tempfile
+
+import os
+from shutil import rmtree
+
+from mantid.api import AnalysisDataService as ADS
+from mantid.simpleapi import CreateSampleWorkspace
+from mantidqt.project import projectsaver
+
+
+project_file_ext = ".mtdproj"
+working_directory = tempfile.mkdtemp()
+
+
+class ProjectSaverTest(unittest.TestCase):
+    def tearDown(self):
+        ADS.clear()
+
+    def setUp(self):
+        # In case it was hard killed and is still present
+        if os.path.isdir(working_directory):
+            rmtree(working_directory)
+
+    def test_only_one_workspace_saving(self):
+        ws1_name = "ws1"
+        ADS.addOrReplace(ws1_name, CreateSampleWorkspace(OutputWorkspace=ws1_name))
+        project_saver = projectsaver.ProjectSaver(project_file_ext)
+        file_name = working_directory + "/" + os.path.basename(working_directory) + project_file_ext
+
+        workspaces_string = "\"workspaces\": [\"ws1\"]"
+
+        project_saver.save_project(workspace_to_save=[ws1_name], directory=working_directory)
+
+        # Check project file is saved correctly
+        f = open(file_name, "r")
+        file_string = f.read()
+        self.assertTrue(workspaces_string in file_string)
+
+        # Check workspace is saved
+        list_of_files = os.listdir(working_directory)
+        self.assertEqual(len(list_of_files), 2)
+        self.assertTrue(os.path.basename(working_directory) + project_file_ext in list_of_files)
+        self.assertTrue(ws1_name + ".nxs" in list_of_files)
+
+    def test_only_multiple_workspaces_saving(self):
+        ws1_name = "ws1"
+        ws2_name = "ws2"
+        ws3_name = "ws3"
+        ws4_name = "ws4"
+        ws5_name = "ws5"
+        CreateSampleWorkspace(OutputWorkspace=ws1_name)
+        CreateSampleWorkspace(OutputWorkspace=ws2_name)
+        CreateSampleWorkspace(OutputWorkspace=ws3_name)
+        CreateSampleWorkspace(OutputWorkspace=ws4_name)
+        CreateSampleWorkspace(OutputWorkspace=ws5_name)
+        project_saver = projectsaver.ProjectSaver(project_file_ext)
+        file_name = working_directory + "/" + os.path.basename(working_directory) + project_file_ext
+
+        workspaces_string = "\"workspaces\": [\"ws1\", \"ws2\", \"ws3\", \"ws4\", \"ws5\"]"
+        project_saver.save_project(workspace_to_save=[ws1_name, ws2_name, ws3_name, ws4_name, ws5_name],
+                                   directory=working_directory)
+
+        # Check project file is saved correctly
+        f = open(file_name, "r")
+        file_string = f.read()
+        self.assertTrue(workspaces_string in file_string)
+
+        # Check workspace is saved
+        list_of_files = os.listdir(working_directory)
+        self.assertEqual(len(list_of_files), 6)
+        self.assertTrue(os.path.basename(working_directory) + project_file_ext in list_of_files)
+        self.assertTrue(ws1_name + ".nxs" in list_of_files)
+        self.assertTrue(ws2_name + ".nxs" in list_of_files)
+        self.assertTrue(ws3_name + ".nxs" in list_of_files)
+        self.assertTrue(ws4_name + ".nxs" in list_of_files)
+        self.assertTrue(ws5_name + ".nxs" in list_of_files)
+
+    def test_only_saving_one_workspace_when_multiple_are_present_in_the_ADS(self):
+        ws1_name = "ws1"
+        ws2_name = "ws2"
+        ws3_name = "ws3"
+        CreateSampleWorkspace(OutputWorkspace=ws1_name)
+        CreateSampleWorkspace(OutputWorkspace=ws2_name)
+        CreateSampleWorkspace(OutputWorkspace=ws3_name)
+        project_saver = projectsaver.ProjectSaver(project_file_ext)
+        file_name = working_directory + "/" + os.path.basename(working_directory) + project_file_ext
+        workspaces_string = "\"workspaces\": [\"ws1\"]"
+        project_saver.save_project(workspace_to_save=[ws1_name], directory=working_directory)
+
+        # Check project file is saved correctly
+        f = open(file_name, "r")
+        file_string = f.read()
+        self.assertTrue(workspaces_string in file_string)
+
+        # Check workspace is saved
+        list_of_files = os.listdir(working_directory)
+        self.assertEqual(len(list_of_files), 2)
+        self.assertTrue(os.path.basename(working_directory) + project_file_ext in list_of_files)
+        self.assertTrue(ws1_name + ".nxs" in list_of_files)
+
+
+class ProjectWriterTest(unittest.TestCase):
+    def tearDown(self):
+        ADS.clear()
+
+    def setUp(self):
+        # In case it was hard killed and is still present
+        if os.path.isdir(working_directory):
+            rmtree(working_directory)
+
+    def test_write_out_empty_workspaces(self):
+        workspace_list = []
+        project_writer = projectsaver.ProjectWriter(working_directory, workspace_list, project_file_ext)
+        file_name = working_directory + "/" + os.path.basename(working_directory) + project_file_ext
+
+        workspaces_string = "\"workspaces\": []"
+
+        project_writer.write_out()
+
+        f = open(file_name, "r")
+        file_string = f.read()
+        self.assertTrue(workspaces_string in file_string)
+
+    def test_write_out_on_just_workspaces(self):
+        workspace_list = ["ws1", "ws2", "ws3", "ws4"]
+        project_writer = projectsaver.ProjectWriter(working_directory, workspace_list, project_file_ext)
+        file_name = working_directory + "/" + os.path.basename(working_directory) + project_file_ext
+
+        workspaces_string = "\"workspaces\": [\"ws1\", \"ws2\", \"ws3\", \"ws4\"]"
+
+        project_writer.write_out()
+        f = open(file_name, "r")
+        file_string = f.read()
+        self.assertTrue(workspaces_string in file_string)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/qt/python/mantidqt/project/test/test_workspaceloader.py b/qt/python/mantidqt/project/test/test_workspaceloader.py
new file mode 100644
index 0000000000000000000000000000000000000000..dd433316c087ce82d271117348d8266c281b64b3
--- /dev/null
+++ b/qt/python/mantidqt/project/test/test_workspaceloader.py
@@ -0,0 +1,42 @@
+# Mantid Repository : https://github.com/mantidproject/mantid
+#
+# Copyright &copy; 2017 ISIS Rutherford Appleton Laboratory UKRI,
+#     NScD Oak Ridge National Laboratory, European Spallation Source
+#     & Institut Laue - Langevin
+# SPDX - License - Identifier: GPL - 3.0 +
+#  This file is part of the mantidqt package
+#
+
+import unittest
+
+from os.path import isdir
+from shutil import rmtree
+import tempfile
+
+from mantid.api import AnalysisDataService as ADS
+from mantid.simpleapi import CreateSampleWorkspace
+from mantidqt.project import projectsaver, workspaceloader
+
+
+class WorkspaceLoaderTest(unittest.TestCase):
+    def setUp(self):
+        self.working_directory = tempfile.mkdtemp()
+        self.ws1_name = "ws1"
+        self.project_ext = ".mtdproj"
+        ADS.addOrReplace(self.ws1_name, CreateSampleWorkspace(OutputWorkspace=self.ws1_name))
+        project_saver = projectsaver.ProjectSaver(self.project_ext)
+        project_saver.save_project(workspace_to_save=[self.ws1_name], directory=self.working_directory)
+
+    def tearDown(self):
+        ADS.clear()
+        if isdir(self.working_directory):
+            rmtree(self.working_directory)
+
+    def test_workspace_loading(self):
+        workspace_loader = workspaceloader.WorkspaceLoader()
+        workspace_loader.load_workspaces(self.working_directory, workspaces_to_load=[self.ws1_name])
+        self.assertEqual(ADS.getObjectNames(), [self.ws1_name])
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/qt/python/mantidqt/project/test/test_workspacesaver.py b/qt/python/mantidqt/project/test/test_workspacesaver.py
new file mode 100644
index 0000000000000000000000000000000000000000..70fa1cf4bb3d1e1a499b18c62c39ef2644a7a0de
--- /dev/null
+++ b/qt/python/mantidqt/project/test/test_workspacesaver.py
@@ -0,0 +1,84 @@
+# Mantid Repository : https://github.com/mantidproject/mantid
+#
+# Copyright &copy; 2017 ISIS Rutherford Appleton Laboratory UKRI,
+#     NScD Oak Ridge National Laboratory, European Spallation Source
+#     & Institut Laue - Langevin
+# SPDX - License - Identifier: GPL - 3.0 +
+#  This file is part of the mantidqt package
+#
+
+import unittest
+
+from os import listdir
+from os.path import isdir
+from shutil import rmtree
+import tempfile
+
+from mantid.api import AnalysisDataService as ADS, IMDEventWorkspace  # noqa
+from mantid.dataobjects import MDHistoWorkspace, MaskWorkspace  # noqa
+from mantidqt.project import workspacesaver
+from mantid.simpleapi import (CreateSampleWorkspace, CreateMDHistoWorkspace, LoadMD, LoadMask, MaskDetectors,  # noqa
+                              ExtractMask)  # noqa
+
+
+class WorkspaceSaverTest(unittest.TestCase):
+    def setUp(self):
+        self.working_directory = tempfile.mkdtemp()
+
+    def tearDown(self):
+        ADS.clear()
+        if isdir(self.working_directory):
+            rmtree(self.working_directory)
+
+    def test_saving_single_workspace(self):
+        ws_saver = workspacesaver.WorkspaceSaver(self.working_directory)
+        ws1 = CreateSampleWorkspace()
+        ws1_name = "ws1"
+
+        ADS.addOrReplace(ws1_name, ws1)
+        ws_saver.save_workspaces([ws1_name])
+
+        list_of_files = listdir(self.working_directory)
+        self.assertEqual(len(list_of_files), 1)
+        self.assertTrue(ws1_name + ".nxs" in list_of_files)
+
+    def test_saving_multiple_workspaces(self):
+        ws_saver = workspacesaver.WorkspaceSaver(self.working_directory)
+        ws1 = CreateSampleWorkspace()
+        ws1_name = "ws1"
+        ws2 = CreateSampleWorkspace()
+        ws2_name = "ws2"
+
+        ADS.addOrReplace(ws1_name, ws1)
+        ADS.addOrReplace(ws2_name, ws2)
+        ws_saver.save_workspaces([ws1_name, ws2_name])
+
+        list_of_files = listdir(self.working_directory)
+        self.assertEqual(len(list_of_files), 2)
+        self.assertTrue(ws2_name + ".nxs" in list_of_files)
+        self.assertTrue(ws1_name + ".nxs" in list_of_files)
+
+    def test_when_MDWorkspace_is_in_ADS(self):
+        ws_saver = workspacesaver.WorkspaceSaver(self.working_directory)
+        ws1 = CreateMDHistoWorkspace(SignalInput='1,2,3,4,5,6,7,8,9', ErrorInput='1,1,1,1,1,1,1,1,1',
+                                     Dimensionality='2', Extents='-1,1,-1,1', NumberOfBins='3,3', Names='A,B',
+                                     Units='U,T')
+        ws1_name = "ws1"
+
+        ADS.addOrReplace(ws1_name, ws1)
+        ws_saver.save_workspaces([ws1_name])
+
+        list_of_files = listdir(self.working_directory)
+        self.assertEqual(len(list_of_files), 1)
+        self.assertTrue(ws1_name + ".nxs" in list_of_files)
+        self._load_MDWorkspace_and_test_it(ws1_name)
+
+    def _load_MDWorkspace_and_test_it(self, save_name):
+        filename = self.working_directory + '/' + save_name + ".nxs"
+        ws = LoadMD(Filename=filename)
+        ws_is_a_mdworkspace = isinstance(ws, IMDEventWorkspace) or isinstance(ws, MDHistoWorkspace)
+        self.assertEqual(ws_is_a_mdworkspace, True)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/qt/python/mantidqt/project/workspaceloader.py b/qt/python/mantidqt/project/workspaceloader.py
new file mode 100644
index 0000000000000000000000000000000000000000..2bf4ccf05e56b3913c86636823aa54ba395aca6a
--- /dev/null
+++ b/qt/python/mantidqt/project/workspaceloader.py
@@ -0,0 +1,29 @@
+# Mantid Repository : https://github.com/mantidproject/mantid
+#
+# Copyright &copy; 2017 ISIS Rutherford Appleton Laboratory UKRI,
+#     NScD Oak Ridge National Laboratory, European Spallation Source
+#     & Institut Laue - Langevin
+# SPDX - License - Identifier: GPL - 3.0 +
+#  This file is part of the mantidqt package
+#
+from __future__ import (absolute_import, division, print_function, unicode_literals)
+
+from os import path
+
+from mantid import logger
+
+
+class WorkspaceLoader(object):
+    @staticmethod
+    def load_workspaces(directory, workspaces_to_load):
+        """
+        The method that is called to load in workspaces. From the given directory and the workspace names provided.
+        :param directory: String or string castable object; The project directory
+        :param workspaces_to_load: List of Strings; of the workspaces to load
+        """
+        from mantid.simpleapi import Load  # noqa
+        for workspace in workspaces_to_load:
+            try:
+                Load(path.join(directory, (workspace + ".nxs")), OutputWorkspace=workspace)
+            except Exception:
+                logger.warning("Couldn't load file in project: " + workspace + ".nxs")
diff --git a/qt/python/mantidqt/project/workspacesaver.py b/qt/python/mantidqt/project/workspacesaver.py
new file mode 100644
index 0000000000000000000000000000000000000000..b3e0b583edcc69cd9ef2cafad366eb31b98bf852
--- /dev/null
+++ b/qt/python/mantidqt/project/workspacesaver.py
@@ -0,0 +1,63 @@
+# Mantid Repository : https://github.com/mantidproject/mantid
+#
+# Copyright &copy; 2017 ISIS Rutherford Appleton Laboratory UKRI,
+#     NScD Oak Ridge National Laboratory, European Spallation Source
+#     & Institut Laue - Langevin
+# SPDX - License - Identifier: GPL - 3.0 +
+#  This file is part of the mantidqt package
+#
+from __future__ import (absolute_import, division, print_function, unicode_literals)
+
+import os.path
+
+from mantid.api import AnalysisDataService as ADS, IMDEventWorkspace
+from mantid.dataobjects import MDHistoWorkspace
+from mantid import logger
+
+
+class WorkspaceSaver(object):
+    def __init__(self, directory):
+        """
+
+        :param directory:
+        """
+        self.directory = directory
+        self.output_list = []
+
+    def save_workspaces(self, workspaces_to_save=None):
+        """
+        Use the private method _get_workspaces_to_save to get a list of workspaces that are present in the ADS to save
+        to the directory that was passed at object creation time, it will also add each of them to the output_list
+        private instance variable on the WorkspaceSaver class.
+        :param workspaces_to_save: List of Strings; The workspaces that are to be saved to the project.
+        """
+
+        # Handle getting here and nothing has been given passed
+        if workspaces_to_save is None:
+            return
+
+        for workspace_name in workspaces_to_save:
+            # Get the workspace from the ADS
+            workspace = ADS.retrieve(workspace_name)
+            place_to_save_workspace = os.path.join(self.directory, workspace_name)
+
+            from mantid.simpleapi import SaveMD, SaveNexusProcessed
+
+            try:
+                if isinstance(workspace, MDHistoWorkspace) or isinstance(workspace, IMDEventWorkspace):
+                    # Save normally using SaveMD
+                    SaveMD(InputWorkspace=workspace_name, Filename=place_to_save_workspace + ".nxs")
+                else:
+                    # Save normally using SaveNexusProcessed
+                    SaveNexusProcessed(InputWorkspace=workspace_name, Filename=place_to_save_workspace + ".nxs")
+            except Exception:
+                logger.warning("Couldn't save workspace in project: " + workspace)
+
+            self.output_list.append(workspace_name)
+
+    def get_output_list(self):
+        """
+        Get the output_list
+        :return: List; String list of the workspaces that were saved
+        """
+        return self.output_list
diff --git a/qt/scientific_interfaces/Indirect/ConvFit.cpp b/qt/scientific_interfaces/Indirect/ConvFit.cpp
index ec038f7f165c2039f340a79ac2c508310b7b38a0..6050d8c58d90035ea920163aa209bbd0a23efec0 100644
--- a/qt/scientific_interfaces/Indirect/ConvFit.cpp
+++ b/qt/scientific_interfaces/Indirect/ConvFit.cpp
@@ -53,9 +53,9 @@ void ConvFit::setupFitTab() {
   setConvolveMembers(true);
 
   setSampleWSSuffices({"_red", "_sqw"});
-  setSampleFBSuffices({"_red.nxs", "_sqw.nxs"});
+  setSampleFBSuffices({"_red.nxs", "_sqw.nxs", "_sqw.dave"});
   setResolutionWSSuffices({"_res", "_red", "_sqw"});
-  setResolutionFBSuffices({"_res.nxs", "_red.nxs", "_sqw.nxs"});
+  setResolutionFBSuffices({"_res.nxs", "_red.nxs", "_sqw.nxs", "_sqw.dave"});
 
   // Initialise fitTypeStrings
   m_fitStrings["None"] = "";
diff --git a/qt/scientific_interfaces/Indirect/Elwin.cpp b/qt/scientific_interfaces/Indirect/Elwin.cpp
index ee340dfee7316ccae7747e8ac4e5aa3d72a8a18e..e449ea05131c0fb2da64d5cb252045ae5ed2ae2a 100644
--- a/qt/scientific_interfaces/Indirect/Elwin.cpp
+++ b/qt/scientific_interfaces/Indirect/Elwin.cpp
@@ -8,6 +8,7 @@
 #include "../General/UserInputValidator.h"
 
 #include "MantidGeometry/Instrument.h"
+#include "MantidQtWidgets/Common/SignalBlocker.h"
 #include "MantidQtWidgets/LegacyQwt/RangeSelector.h"
 
 #include <QFileInfo>
@@ -25,10 +26,30 @@ MatrixWorkspace_sptr getADSMatrixWorkspace(std::string const &workspaceName) {
       workspaceName);
 }
 
+bool doesExistInADS(std::string const &workspaceName) {
+  return AnalysisDataService::Instance().doesExist(workspaceName);
+}
+
 bool isWorkspacePlottable(MatrixWorkspace_sptr workspace) {
   return workspace->y(0).size() > 1;
 }
 
+bool isWorkspacePlottable(std::string const &workspaceName) {
+  return isWorkspacePlottable(getADSMatrixWorkspace(workspaceName));
+}
+
+bool canPlotWorkspace(std::string const &workspaceName) {
+  return doesExistInADS(workspaceName) && isWorkspacePlottable(workspaceName);
+}
+
+std::vector<std::string> getOutputWorkspaceSuffices() {
+  return {"_eq", "_eq2", "_elf", "_elt"};
+}
+
+int getNumberOfSpectra(std::string const &name) {
+  return static_cast<int>(getADSMatrixWorkspace(name)->getNumberHistograms());
+}
+
 } // namespace
 
 namespace MantidQt {
@@ -124,6 +145,9 @@ void Elwin::setup() {
   connect(m_uiForm.pbPlotPreview, SIGNAL(clicked()), this,
           SLOT(plotCurrentPreview()));
 
+  connect(m_uiForm.cbPlotWorkspace, SIGNAL(currentIndexChanged(int)), this,
+          SLOT(updateAvailablePlotSpectra()));
+
   // Set any default values
   m_dblManager->setValue(m_properties["IntegrationStart"], -0.02);
   m_dblManager->setValue(m_properties["IntegrationEnd"], 0.02);
@@ -194,8 +218,7 @@ void Elwin::run() {
   }
 
   // Group input workspaces
-  IAlgorithm_sptr groupWsAlg =
-      AlgorithmManager::Instance().create("GroupWorkspaces");
+  auto groupWsAlg = AlgorithmManager::Instance().create("GroupWorkspaces");
   groupWsAlg->initialize();
   API::BatchAlgorithmRunner::AlgorithmRuntimeProps runTimeProps;
   runTimeProps["InputWorkspaces"] = inputWorkspacesString;
@@ -204,7 +227,7 @@ void Elwin::run() {
   m_batchAlgoRunner->addAlgorithm(groupWsAlg, runTimeProps);
 
   // Configure ElasticWindowMultiple algorithm
-  IAlgorithm_sptr elwinMultAlg =
+  auto elwinMultAlg =
       AlgorithmManager::Instance().create("ElasticWindowMultiple");
   elwinMultAlg->initialize();
 
@@ -260,18 +283,63 @@ void Elwin::unGroupInput(bool error) {
 
   if (!error) {
     if (!m_uiForm.ckGroupInput->isChecked()) {
-      IAlgorithm_sptr ungroupAlg =
-          AlgorithmManager::Instance().create("UnGroupWorkspace");
+      auto ungroupAlg = AlgorithmManager::Instance().create("UnGroupWorkspace");
       ungroupAlg->initialize();
       ungroupAlg->setProperty("InputWorkspace", "IDA_Elwin_Input");
       ungroupAlg->execute();
     }
+
+    updatePlotSpectrumOptions();
+
   } else {
     setPlotResultEnabled(false);
     setSaveResultEnabled(false);
   }
 }
 
+void Elwin::updatePlotSpectrumOptions() {
+  updateAvailablePlotWorkspaces();
+  if (m_uiForm.cbPlotWorkspace->size().isEmpty())
+    setPlotResultEnabled(false);
+  else
+    updateAvailablePlotSpectra();
+}
+
+void Elwin::updateAvailablePlotWorkspaces() {
+  MantidQt::API::SignalBlocker<QObject> blocker(m_uiForm.cbPlotWorkspace);
+  m_uiForm.cbPlotWorkspace->clear();
+  for (auto const &suffix : getOutputWorkspaceSuffices()) {
+    auto const workspaceName = getOutputBasename().toStdString() + suffix;
+    if (canPlotWorkspace(workspaceName))
+      m_uiForm.cbPlotWorkspace->addItem(QString::fromStdString(workspaceName));
+  }
+}
+
+QString Elwin::getPlotWorkspaceName() const {
+  return m_uiForm.cbPlotWorkspace->currentText();
+}
+
+void Elwin::setPlotSpectrumValue(int value) {
+  MantidQt::API::SignalBlocker<QObject> blocker(m_uiForm.spPlotSpectrum);
+  m_uiForm.spPlotSpectrum->setValue(value);
+}
+
+void Elwin::updateAvailablePlotSpectra() {
+  auto const name = m_uiForm.cbPlotWorkspace->currentText().toStdString();
+  auto const maximumValue = getNumberOfSpectra(name) - 1;
+  setPlotSpectrumMinMax(0, maximumValue);
+  setPlotSpectrumValue(0);
+}
+
+void Elwin::setPlotSpectrumMinMax(int minimum, int maximum) {
+  m_uiForm.spPlotSpectrum->setMinimum(minimum);
+  m_uiForm.spPlotSpectrum->setMaximum(maximum);
+}
+
+int Elwin::getPlotSpectrumIndex() const {
+  return m_uiForm.spPlotSpectrum->text().toInt();
+}
+
 bool Elwin::validate() {
   UserInputValidator uiv;
 
@@ -474,87 +542,63 @@ void Elwin::updateRS(QtProperty *prop, double val) {
     backgroundRangeSelector->setMaximum(val);
 }
 
+void Elwin::runClicked() { runTab(); }
+
 /**
  * Handles mantid plotting
  */
 void Elwin::plotClicked() {
   setPlotResultIsPlotting(true);
-
-  auto const workspaceBaseName =
-      getWorkspaceBasename(QString::fromStdString(m_pythonExportWsName));
-
-  plotResult(workspaceBaseName + "_eq");
-  plotResult(workspaceBaseName + "_eq2");
-  plotResult(workspaceBaseName + "_elf");
-  plotResult(workspaceBaseName + "_elt");
-
+  plotSpectrum(getPlotWorkspaceName(), getPlotSpectrumIndex());
   setPlotResultIsPlotting(false);
 }
 
-void Elwin::plotResult(QString const &workspaceName) {
-  auto const name = workspaceName.toStdString();
-  if (checkADSForPlotSaveWorkspace(name, true)) {
-    if (isWorkspacePlottable(getADSMatrixWorkspace(name)))
-      plotSpectrum(workspaceName);
-    else
-      showMessageBox("Plotting a spectrum of the workspace " + workspaceName +
-                     " failed : Workspace only has one data point");
-  }
-}
-
 /**
  * Handles saving of workspaces
  */
 void Elwin::saveClicked() {
-  auto workspaceBaseName =
-      getWorkspaceBasename(QString::fromStdString(m_pythonExportWsName));
-
-  if (checkADSForPlotSaveWorkspace((workspaceBaseName + "_eq").toStdString(),
-                                   false))
-    addSaveWorkspaceToQueue(workspaceBaseName + "_eq");
-
-  if (checkADSForPlotSaveWorkspace((workspaceBaseName + "_eq2").toStdString(),
-                                   false))
-    addSaveWorkspaceToQueue(workspaceBaseName + "_eq2");
-
-  if (checkADSForPlotSaveWorkspace((workspaceBaseName + "_elf").toStdString(),
-                                   false))
-    addSaveWorkspaceToQueue(workspaceBaseName + "_elf");
+  auto const workspaceBaseName = getOutputBasename().toStdString();
 
-  if (checkADSForPlotSaveWorkspace((workspaceBaseName + "_elt").toStdString(),
-                                   false, false))
-    addSaveWorkspaceToQueue(workspaceBaseName + "_elt");
+  for (auto const &suffix : getOutputWorkspaceSuffices())
+    if (checkADSForPlotSaveWorkspace(workspaceBaseName + suffix, false))
+      addSaveWorkspaceToQueue(workspaceBaseName + suffix);
 
   m_batchAlgoRunner->executeBatchAsync();
 }
 
-void Elwin::setRunEnabled(bool enabled) { m_uiForm.pbRun->setEnabled(enabled); }
+QString Elwin::getOutputBasename() {
+  return getWorkspaceBasename(QString::fromStdString(m_pythonExportWsName));
+}
 
-void Elwin::setPlotResultEnabled(bool enabled) {
-  m_uiForm.pbPlot->setEnabled(enabled);
+void Elwin::setRunIsRunning(const bool &running) {
+  m_uiForm.pbRun->setText(running ? "Running..." : "Run");
+  setButtonsEnabled(!running);
 }
 
-void Elwin::setSaveResultEnabled(bool enabled) {
-  m_uiForm.pbSave->setEnabled(enabled);
+void Elwin::setPlotResultIsPlotting(const bool &plotting) {
+  m_uiForm.pbPlot->setText(plotting ? "Plotting..." : "Plot Spectrum");
+  setButtonsEnabled(!plotting);
 }
 
-void Elwin::setButtonsEnabled(bool enabled) {
+void Elwin::setButtonsEnabled(const bool &enabled) {
   setRunEnabled(enabled);
   setPlotResultEnabled(enabled);
   setSaveResultEnabled(enabled);
 }
 
-void Elwin::setRunIsRunning(bool running) {
-  m_uiForm.pbRun->setText(running ? "Running..." : "Run");
-  setButtonsEnabled(!running);
+void Elwin::setRunEnabled(const bool &enabled) {
+  m_uiForm.pbRun->setEnabled(enabled);
 }
 
-void Elwin::setPlotResultIsPlotting(bool plotting) {
-  m_uiForm.pbPlot->setText(plotting ? "Plotting..." : "Plot Result");
-  setButtonsEnabled(!plotting);
+void Elwin::setPlotResultEnabled(const bool &enabled) {
+  m_uiForm.pbPlot->setEnabled(enabled);
+  m_uiForm.cbPlotWorkspace->setEnabled(enabled);
+  m_uiForm.spPlotSpectrum->setEnabled(enabled);
 }
 
-void Elwin::runClicked() { runTab(); }
+void Elwin::setSaveResultEnabled(const bool &enabled) {
+  m_uiForm.pbSave->setEnabled(enabled);
+}
 
 } // namespace IDA
 } // namespace CustomInterfaces
diff --git a/qt/scientific_interfaces/Indirect/Elwin.h b/qt/scientific_interfaces/Indirect/Elwin.h
index f47943fa8be03426fe78f5599323074abdc383b4..07f26e161d093c10509cc1590f790c0e1247b1ac 100644
--- a/qt/scientific_interfaces/Indirect/Elwin.h
+++ b/qt/scientific_interfaces/Indirect/Elwin.h
@@ -29,6 +29,7 @@ private slots:
   void maxChanged(double val);
   void updateRS(QtProperty *prop, double val);
   void unGroupInput(bool error);
+  void updateAvailablePlotSpectra();
   void runClicked();
   void saveClicked();
   void plotClicked();
@@ -43,14 +44,21 @@ private:
                             const QPair<double, double> &range);
   void setDefaultSampleLog(Mantid::API::MatrixWorkspace_const_sptr ws);
 
-  void plotResult(QString const &workspaceName);
+  QString getOutputBasename();
 
-  void setRunEnabled(bool enabled);
-  void setPlotResultEnabled(bool enabled);
-  void setSaveResultEnabled(bool enabled);
-  void setButtonsEnabled(bool enabled);
-  void setRunIsRunning(bool running);
-  void setPlotResultIsPlotting(bool plotting);
+  void updatePlotSpectrumOptions();
+  void updateAvailablePlotWorkspaces();
+  QString getPlotWorkspaceName() const;
+  void setPlotSpectrumValue(int value);
+  void setPlotSpectrumMinMax(int minimum, int maximum);
+  int getPlotSpectrumIndex() const;
+
+  void setRunIsRunning(const bool &running);
+  void setPlotResultIsPlotting(const bool &plotting);
+  void setButtonsEnabled(const bool &enabled);
+  void setRunEnabled(const bool &enabled);
+  void setPlotResultEnabled(const bool &enabled);
+  void setSaveResultEnabled(const bool &enabled);
 
   Ui::Elwin m_uiForm;
   QtTreePropertyBrowser *m_elwTree;
diff --git a/qt/scientific_interfaces/Indirect/Elwin.ui b/qt/scientific_interfaces/Indirect/Elwin.ui
index 9a006e4f6f6588c8dda39978be6957c52429aca9..7439abe27aee0e78f0934395d48ad8c60cec4076 100644
--- a/qt/scientific_interfaces/Indirect/Elwin.ui
+++ b/qt/scientific_interfaces/Indirect/Elwin.ui
@@ -268,13 +268,40 @@
          <string>Output</string>
         </property>
         <layout class="QHBoxLayout" name="horizontalLayout_5">
+         <item>
+          <widget class="QLabel" name="label">
+           <property name="text">
+            <string>Workspace:</string>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QComboBox" name="cbPlotWorkspace">
+           <property name="enabled">
+            <bool>false</bool>
+           </property>
+           <property name="minimumSize">
+            <size>
+             <width>200</width>
+             <height>0</height>
+            </size>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QSpinBox" name="spPlotSpectrum">
+           <property name="enabled">
+            <bool>false</bool>
+           </property>
+          </widget>
+         </item>
          <item>
           <widget class="QPushButton" name="pbPlot">
            <property name="enabled">
             <bool>false</bool>
            </property>
            <property name="text">
-            <string>Plot Result</string>
+            <string>Plot Spectrum</string>
            </property>
           </widget>
          </item>
diff --git a/qt/scientific_interfaces/Indirect/IndirectTab.cpp b/qt/scientific_interfaces/Indirect/IndirectTab.cpp
index 58563d2ccc0677a789d93aa4cd8db2fb327e8cd4..87724b537b9524eebd0bbe2f2f794ff3ecb5cf32 100644
--- a/qt/scientific_interfaces/Indirect/IndirectTab.cpp
+++ b/qt/scientific_interfaces/Indirect/IndirectTab.cpp
@@ -167,19 +167,23 @@ bool IndirectTab::loadFile(const QString &filename, const QString &outputName,
  */
 void IndirectTab::addSaveWorkspaceToQueue(const QString &wsName,
                                           const QString &filename) {
+  addSaveWorkspaceToQueue(wsName.toStdString(), filename.toStdString());
+}
+
+void IndirectTab::addSaveWorkspaceToQueue(const std::string &wsName,
+                                          const std::string &filename) {
   // Setup the input workspace property
   API::BatchAlgorithmRunner::AlgorithmRuntimeProps saveProps;
-  saveProps["InputWorkspace"] = wsName.toStdString();
+  saveProps["InputWorkspace"] = wsName;
 
   // Setup the algorithm
-  IAlgorithm_sptr saveAlgo =
-      AlgorithmManager::Instance().create("SaveNexusProcessed");
+  auto saveAlgo = AlgorithmManager::Instance().create("SaveNexusProcessed");
   saveAlgo->initialize();
 
-  if (filename.isEmpty())
-    saveAlgo->setProperty("Filename", wsName.toStdString() + ".nxs");
+  if (filename.empty())
+    saveAlgo->setProperty("Filename", wsName + ".nxs");
   else
-    saveAlgo->setProperty("Filename", filename.toStdString());
+    saveAlgo->setProperty("Filename", filename);
 
   // Add the save algorithm to the batch
   m_batchAlgoRunner->addAlgorithm(saveAlgo, saveProps);
diff --git a/qt/scientific_interfaces/Indirect/IndirectTab.h b/qt/scientific_interfaces/Indirect/IndirectTab.h
index b62834cb7625deeac80f37a2815074c8e5869aeb..fc28f96d59428937e0ce96a7551ffb20a8f3eaf1 100644
--- a/qt/scientific_interfaces/Indirect/IndirectTab.h
+++ b/qt/scientific_interfaces/Indirect/IndirectTab.h
@@ -81,6 +81,8 @@ protected:
                 const int specMin = -1, const int specMax = -1);
 
   /// Add a SaveNexusProcessed step to the batch queue
+  void addSaveWorkspaceToQueue(const std::string &wsName,
+                               const std::string &filename = "");
   void addSaveWorkspaceToQueue(const QString &wsName,
                                const QString &filename = "");
 
diff --git a/qt/scientific_interfaces/Indirect/JumpFitDataPresenter.cpp b/qt/scientific_interfaces/Indirect/JumpFitDataPresenter.cpp
index 149bfd3dcff4d1ee59026ee4d0985b4dae73eb56..1c8c56cfe20440ce98a749510b1ef1d2dd3cbe58 100644
--- a/qt/scientific_interfaces/Indirect/JumpFitDataPresenter.cpp
+++ b/qt/scientific_interfaces/Indirect/JumpFitDataPresenter.cpp
@@ -38,7 +38,7 @@ JumpFitDataPresenter::JumpFitDataPresenter(
           SLOT(setParameterLabel(const QString &)));
   connect(cbParameterType, SIGNAL(currentIndexChanged(const QString &)), this,
           SLOT(updateAvailableParameters(QString const &)));
-  connect(cbParameterType, SIGNAL(currentIndexChanged(int)), this,
+  connect(cbParameterType, SIGNAL(currentIndexChanged(const QString &)), this,
           SIGNAL(dataChanged()));
   connect(cbParameter, SIGNAL(currentIndexChanged(int)), this,
           SLOT(setSingleModelSpectrum(int)));
diff --git a/qt/widgets/common/src/DataSelector.cpp b/qt/widgets/common/src/DataSelector.cpp
index ee2b491d544d91bc683e6784d878666acafa1d61..476c0ea8447f57e7ae6af0d43407f70a94147f4a 100644
--- a/qt/widgets/common/src/DataSelector.cpp
+++ b/qt/widgets/common/src/DataSelector.cpp
@@ -17,6 +17,23 @@
 #include <QMimeData>
 #include <QUrl>
 
+namespace {
+
+std::string extractLastOf(const std::string &str,
+                          const std::string &delimiter) {
+  const auto cutIndex = str.rfind(delimiter);
+  if (cutIndex != std::string::npos)
+    return str.substr(cutIndex, str.size() - cutIndex);
+  return str;
+}
+
+std::string loadAlgName(const std::string &filePath) {
+  const auto suffix = extractLastOf(filePath, ".");
+  return suffix == ".dave" ? "LoadDaveGrp" : "Load";
+}
+
+} // namespace
+
 namespace MantidQt {
 namespace MantidWidgets {
 
@@ -125,8 +142,8 @@ bool DataSelector::isValid() {
         // don't use algorithm runner because we need to know instantly.
         const QString filepath =
             m_uiForm.rfFileInput->getUserInput().toString();
-        const Algorithm_sptr loadAlg =
-            AlgorithmManager::Instance().createUnmanaged("Load");
+        const auto loadAlg = AlgorithmManager::Instance().createUnmanaged(
+            loadAlgName(filepath.toStdString()));
         loadAlg->initialize();
         loadAlg->setProperty("Filename", filepath.toStdString());
         loadAlg->setProperty("OutputWorkspace", wsName.toStdString());
@@ -179,14 +196,14 @@ QString DataSelector::getProblem() const {
  */
 void DataSelector::autoLoadFile(const QString &filepath) {
   using namespace Mantid::API;
-  QString baseName = getWsNameFromFiles();
+  const auto baseName = getWsNameFromFiles().toStdString();
 
   // create instance of load algorithm
-  const Algorithm_sptr loadAlg =
-      AlgorithmManager::Instance().createUnmanaged("Load");
+  const auto loadAlg = AlgorithmManager::Instance().createUnmanaged(
+      loadAlgName(filepath.toStdString()));
   loadAlg->initialize();
   loadAlg->setProperty("Filename", filepath.toStdString());
-  loadAlg->setProperty("OutputWorkspace", baseName.toStdString());
+  loadAlg->setProperty("OutputWorkspace", baseName);
 
   m_algRunner.startAlgorithm(loadAlg);
 }
diff --git a/scripts/ErrorReporter/error_report_presenter.py b/scripts/ErrorReporter/error_report_presenter.py
index 02b9dcfb5a170eb5825994bedf064d5f0bb0ef1a..bf7a7d6b6092029c62382a2890cfe067cc47920f 100644
--- a/scripts/ErrorReporter/error_report_presenter.py
+++ b/scripts/ErrorReporter/error_report_presenter.py
@@ -4,9 +4,11 @@
 #     NScD Oak Ridge National Laboratory, European Spallation Source
 #     & Institut Laue - Langevin
 # SPDX - License - Identifier: GPL - 3.0 +
+import os
+
 from mantid.kernel import ErrorReporter, UsageService, ConfigService
 from mantid.kernel import Logger
-from ErrorReporter.retrieve_recovery_files import zip_recovery_directory, remove_recovery_file
+from ErrorReporter.retrieve_recovery_files import zip_recovery_directory
 import requests
 
 
@@ -31,13 +33,21 @@ class ErrorReporterPresenter(object):
 
     def share_all_information(self, continue_working, name, email, text_box):
         uptime = UsageService.getUpTime()
-        zip_recovery_file, file_hash = zip_recovery_directory()
-        status = self._send_report_to_server(share_identifiable=True, uptime=uptime, name=name, email=email, file_hash=file_hash
-                                             , text_box=text_box)
-        self.error_log.notice("Sent complete information")
-        if status == 201:
-            self._upload_recovery_file(zip_recovery_file=zip_recovery_file)
-        remove_recovery_file(zip_recovery_file)
+        try:
+            recovery_archive, file_hash = zip_recovery_directory()
+        except Exception as exc:
+            self.error_log.information("Error creating recovery archive: {}. No recovery information will be sent")
+            recovery_archive, file_hash = None, ""
+        status = self._send_report_to_server(share_identifiable=True, uptime=uptime, name=name, email=email, file_hash=file_hash,
+                                             text_box=text_box)
+        self.error_log.notice("Sent full information")
+        if status == 201 and recovery_archive:
+            self._upload_recovery_file(recovery_archive=recovery_archive)
+            try:
+                os.remove(recovery_archive)
+            except OSError as exc:
+                self.error_log.information("Unable to remove zipped recovery information: {}".format(str(exc)))
+
         self._handle_exit(continue_working)
         return status
 
@@ -62,13 +72,13 @@ class ErrorReporterPresenter(object):
         else:
             self.error_log.error("Continue working.")
 
-    def _upload_recovery_file(self, zip_recovery_file):
+    def _upload_recovery_file(self, recovery_archive):
         url = ConfigService['errorreports.rooturl']
         url = '{}/api/recovery'.format(url)
-        files = {'file': open('{}.zip'.format(zip_recovery_file), 'rb')}
+        files = {'file': open('{}'.format(recovery_archive), 'rb')}
         response = requests.post(url, files=files)
         if response.status_code == 201:
-            self.error_log.notice("Uploaded recovery file to server HTTP response {}".format(response.status_code))
+            self.error_log.notice("Uploaded recovery file to server. HTTP response {}".format(response.status_code))
         else:
             self.error_log.error("Failed to send recovery data HTTP response {}".format(response.status_code))
 
diff --git a/scripts/ErrorReporter/retrieve_recovery_files.py b/scripts/ErrorReporter/retrieve_recovery_files.py
index 519c79ba65b4f910060d272dc4b93d514983aabb..245e1d52bb100df0aedb3d68414e2b4a21e27d1a 100644
--- a/scripts/ErrorReporter/retrieve_recovery_files.py
+++ b/scripts/ErrorReporter/retrieve_recovery_files.py
@@ -10,31 +10,23 @@ def get_properties_directory():
 
 
 def get_recovery_files_path():
-    recovery_files_path = ''
     properties_directory = get_properties_directory()
     if 'recovery' not in os.listdir(properties_directory):
-        return recovery_files_path
+        return None
 
-    recovery_dir_contents = os.listdir(properties_directory + 'recovery')
-    if not recovery_dir_contents:
+    recovery_files_path = os.path.join(properties_directory, 'recovery')
+    if len(os.listdir(recovery_files_path)) > 0:
         return recovery_files_path
-
-    recovery_files_path = properties_directory + 'recovery'
-    return recovery_files_path
+    else:
+        return None
 
 
 def zip_recovery_directory():
     path = get_recovery_files_path()
+    if path is None:
+        return "", ""
     directory = get_properties_directory()
     hash_value = hashlib.md5(str.encode(directory + str(datetime.datetime.now())))
-    zip_file = os.path.join(directory, hash_value.hexdigest())
-    if path:
-        shutil.make_archive(zip_file, 'zip', path)
-        return zip_file, hash_value.hexdigest()
-    return ''
-
-
-def remove_recovery_file(file):
-    directory = get_properties_directory()
-    zip_file = os.path.join(directory, file)
-    os.remove(zip_file + '.zip')
+    base_name = os.path.join(directory, hash_value.hexdigest())
+    zip_file = shutil.make_archive(base_name, 'zip', path)
+    return zip_file, hash_value.hexdigest()
diff --git a/scripts/Muon/GUI/Common/load_file_widget/__init__.py b/scripts/Muon/GUI/Common/load_file_widget/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/scripts/Muon/GUI/Common/load_file_widget/model.py b/scripts/Muon/GUI/Common/load_file_widget/model.py
new file mode 100644
index 0000000000000000000000000000000000000000..c90ed15701e042e7ba67b7c6e1949849871faa43
--- /dev/null
+++ b/scripts/Muon/GUI/Common/load_file_widget/model.py
@@ -0,0 +1,82 @@
+# Mantid Repository : https://github.com/mantidproject/mantid
+#
+# Copyright &copy; 2018 ISIS Rutherford Appleton Laboratory UKRI,
+#     NScD Oak Ridge National Laboratory, European Spallation Source
+#     & Institut Laue - Langevin
+# SPDX - License - Identifier: GPL - 3.0 +
+from __future__ import (absolute_import, division, print_function)
+
+import os
+from mantid.kernel import ConfigService
+from Muon.GUI.Common.muon_load_data import MuonLoadData
+import Muon.GUI.Common.utilities.load_utils as load_utils
+
+
+class BrowseFileWidgetModel(object):
+
+    def __init__(self, loaded_data_store=MuonLoadData()):
+        # Temporary list of filenames used for load thread
+        self._filenames = []
+
+        self._loaded_data_store = loaded_data_store
+
+    @property
+    def loaded_filenames(self):
+        return self._loaded_data_store.get_parameter("filename")
+
+    @property
+    def loaded_workspaces(self):
+        return self._loaded_data_store.get_parameter("workspace")
+
+    @property
+    def loaded_runs(self):
+        return self._loaded_data_store.get_parameter("run")
+
+    # Used with load thread
+    def output(self):
+        pass
+
+    # Used with load thread
+    def cancel(self):
+        pass
+
+    # Used with load thread
+    def loadData(self, filename_list):
+        self._filenames = filename_list
+
+    # Used with load thread
+    def execute(self):
+        failed_files = []
+        for filename in self._filenames:
+            try:
+                ws, run, filename = load_utils.load_workspace_from_filename(filename)
+            except Exception:
+                failed_files += [filename]
+                continue
+            self._loaded_data_store.remove_data(run=run)
+            self._loaded_data_store.add_data(run=run, workspace=ws, filename=filename)
+        if failed_files:
+            message = load_utils.exception_message_for_failed_files(failed_files)
+            raise ValueError(message)
+
+    def clear(self):
+        self._loaded_data_store.clear()
+
+    def remove_previous_data(self):
+        self._loaded_data_store.remove_last_added_data()
+
+    def get_run_list(self):
+        return self.loaded_runs
+
+    def add_directories_to_config_service(self, file_list):
+        """
+        Parses file_list into the unique directories containing the files, and adds these
+        to the global config service. These directories will then be automatically searched in
+        all subsequent Load calls.
+        """
+        dirs = [os.path.dirname(filename) for filename in file_list]
+        dirs = [path if os.path.isdir(path) else "" for path in dirs]
+        dirs = list(set(dirs))
+        if dirs:
+            for directory in dirs:
+                ConfigService.Instance().appendDataSearchDir(directory.encode('ascii', 'ignore'))
diff --git a/scripts/Muon/GUI/Common/load_file_widget/presenter.py b/scripts/Muon/GUI/Common/load_file_widget/presenter.py
new file mode 100644
index 0000000000000000000000000000000000000000..648204065528bfd5c27cb2d2c80ac7ff43021504
--- /dev/null
+++ b/scripts/Muon/GUI/Common/load_file_widget/presenter.py
@@ -0,0 +1,162 @@
+# Mantid Repository : https://github.com/mantidproject/mantid
+#
+# Copyright &copy; 2018 ISIS Rutherford Appleton Laboratory UKRI,
+#     NScD Oak Ridge National Laboratory, European Spallation Source
+#     & Institut Laue - Langevin
+# SPDX - License - Identifier: GPL - 3.0 +
+from __future__ import (absolute_import, division, print_function)
+
+import copy
+
+from Muon.GUI.Common import thread_model
+import Muon.GUI.Common.utilities.muon_file_utils as file_utils
+import Muon.GUI.Common.utilities.load_utils as load_utils
+
+
+class BrowseFileWidgetPresenter(object):
+
+    def __init__(self, view, model):
+        self._view = view
+        self._model = model
+
+        # Whether to allow single or multiple files to be loaded
+        self._multiple_files = False
+        self._multiple_file_mode = "Single"
+
+        self._use_threading = True
+        self._load_thread = None
+
+        self._view.on_browse_clicked(self.on_browse_button_clicked)
+        self._view.on_file_edit_changed(self.handle_file_changed_by_user)
+
+    def show(self):
+        self._view.show()
+
+    def cancel(self):
+        if self._load_thread is not None:
+            self._load_thread.cancel()
+
+    def create_load_thread(self):
+        return thread_model.ThreadModel(self._model)
+
+    def update_multiple_loading_behaviour(self, text):
+        self._multiple_file_mode = text
+
+    def get_filenames_from_user(self):
+        file_filter = file_utils.filter_for_extensions(["nxs"])
+        directory = ""
+        filenames = self._view.show_file_browser_and_return_selection(file_filter, [directory],
+                                                                      multiple_files=self._multiple_files)
+        # validate
+        filenames = file_utils.parse_user_input_to_files(";".join(filenames))
+        filenames = file_utils.remove_duplicated_files_from_list(filenames)
+        return filenames
+
+    def on_browse_button_clicked(self):
+        filenames = self.get_filenames_from_user()
+        filenames = file_utils.remove_duplicated_files_from_list(filenames)
+        if not self._multiple_files and len(filenames) > 1:
+            self._view.warning_popup("Multiple files selected in single file mode")
+            self._view.reset_edit_to_cached_value()
+            return
+        if filenames:
+            self.handle_loading(filenames)
+
+    def handle_file_changed_by_user(self):
+        user_input = self._view.get_file_edit_text()
+        filenames = file_utils.parse_user_input_to_files(user_input)
+        filenames = file_utils.remove_duplicated_files_from_list(filenames)
+        if not filenames:
+            self._view.reset_edit_to_cached_value()
+            return
+        if not self._multiple_files and len(filenames) > 1:
+            self._view.warning_popup("Multiple files selected in single file mode")
+            self._view.reset_edit_to_cached_value()
+            return
+        if self._multiple_file_mode == "Co-Add":
+            # We don't want to allow messy appending when co-adding
+            self.clear_loaded_data()
+        self.handle_loading(filenames)
+
+    def handle_loading(self, filenames):
+        if self._use_threading:
+            self.handle_load_thread_start(filenames)
+        else:
+            self.handle_load_no_threading(filenames)
+
+    def handle_load_no_threading(self, filenames):
+        self._view.notify_loading_started()
+        self.disable_loading()
+        self._model.loadData(filenames)
+        try:
+            self._model.execute()
+        except ValueError as error:
+            self._view.warning_popup(error.args[0])
+        self.on_loading_finished()
+
+    def handle_load_thread_start(self, filenames):
+        if self._load_thread:
+            return
+        self._view.notify_loading_started()
+        self._load_thread = self.create_load_thread()
+        self._load_thread.threadWrapperSetUp(self.disable_loading,
+                                             self.handle_load_thread_finished,
+                                             self._view.warning_popup)
+        self._load_thread.loadData(filenames)
+        self._load_thread.start()
+
+    def handle_load_thread_finished(self):
+        self._load_thread.deleteLater()
+        self._load_thread = None
+
+        # If in single file mode, remove the previous run
+        if not self._multiple_files and len(self._model.get_run_list()) > 1:
+            self._model.remove_previous_data()
+
+        self.on_loading_finished()
+
+    def on_loading_finished(self):
+        file_list = self._model.loaded_filenames
+        self.set_file_edit(file_list)
+
+        if self._multiple_files and self._multiple_file_mode == "Co-Add":
+            load_utils.combine_loaded_runs(self._model, self._model.loaded_runs)
+
+        self._view.notify_loading_finished()
+        self.enable_loading()
+        self._model.add_directories_to_config_service(file_list)
+
+    def clear_loaded_data(self):
+        self._view.clear()
+        self._model.clear()
+
+    def disable_loading(self):
+        self._view.disable_load_buttons()
+
+    def enable_loading(self):
+        self._view.enable_load_buttons()
+
+    def enable_multiple_files(self, enabled):
+        self._multiple_files = enabled
+
+    @property
+    def workspaces(self):
+        return self._model.loaded_workspaces
+
+    @property
+    def runs(self):
+        return self._model.loaded_runs
+
+    def set_file_edit(self, file_list):
+        file_list = sorted(copy.copy(file_list))
+        if file_list == []:
+            self._view.set_file_edit("No data loaded", False)
+        else:
+            self._view.set_file_edit(";".join(file_list), False)
+
+    # used by parent widget
+    def update_view_from_model(self, file_list):
+        self.set_file_edit(file_list)
+
+    def set_current_instrument(self, instrument):
+        pass
diff --git a/scripts/Muon/GUI/Common/load_file_widget/view.py b/scripts/Muon/GUI/Common/load_file_widget/view.py
new file mode 100644
index 0000000000000000000000000000000000000000..52157be58c84bfa8d10142040d0a373f47f69884
--- /dev/null
+++ b/scripts/Muon/GUI/Common/load_file_widget/view.py
@@ -0,0 +1,139 @@
+from __future__ import (absolute_import, division, print_function)
+
+from PyQt4 import QtGui
+from PyQt4.QtCore import pyqtSignal
+import Muon.GUI.Common.message_box as message_box
+
+
+class BrowseFileWidgetView(QtGui.QWidget):
+    # signals for use by parent widgets
+    loadingStarted = pyqtSignal()
+    loadingFinished = pyqtSignal()
+    dataChanged = pyqtSignal()
+
+    def __init__(self, parent=None):
+        super(BrowseFileWidgetView, self).__init__(parent)
+
+        self.horizontal_layout = None
+        self.browse_button = None
+        self.file_path_edit = None
+
+        self.setup_interface_layout()
+
+        self._store_edit_text = False
+        self._stored_edit_text = ""
+        self._cached_text = ""
+
+        self.set_file_edit("No data loaded", False)
+
+        self.file_path_edit.setReadOnly(True)
+
+    def setup_interface_layout(self):
+        self.setObjectName("BrowseFileWidget")
+        self.resize(500, 100)
+
+        self.browse_button = QtGui.QPushButton(self)
+
+        size_policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed)
+        size_policy.setHorizontalStretch(0)
+        size_policy.setVerticalStretch(0)
+        size_policy.setHeightForWidth(self.browse_button.sizePolicy().hasHeightForWidth())
+
+        self.browse_button.setSizePolicy(size_policy)
+        self.browse_button.setObjectName("browseButton")
+        self.browse_button.setText("Browse")
+
+        self.file_path_edit = QtGui.QLineEdit(self)
+
+        size_policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed)
+        size_policy.setHorizontalStretch(0)
+        size_policy.setVerticalStretch(0)
+        size_policy.setHeightForWidth(self.file_path_edit.sizePolicy().hasHeightForWidth())
+
+        self.file_path_edit.setSizePolicy(size_policy)
+        self.file_path_edit.setToolTip("")
+        self.file_path_edit.setObjectName("filePathEdit")
+
+        self.setStyleSheet("QLineEdit {background: #d7d6d5}")
+
+        self.horizontal_layout = QtGui.QHBoxLayout()
+        self.horizontal_layout.setObjectName("horizontalLayout")
+        self.horizontal_layout.addWidget(self.browse_button)
+        self.horizontal_layout.addWidget(self.file_path_edit)
+
+        self.horizontal_layout.setContentsMargins(0, 0, 0, 0)
+        self.horizontal_layout.setMargin(0)
+
+        self.setLayout(self.horizontal_layout)
+
+    def getLayout(self):
+        return self.horizontal_layout
+
+    def on_browse_clicked(self, slot):
+        self.browse_button.clicked.connect(slot)
+
+    def on_file_edit_changed(self, slot):
+        self.file_path_edit.returnPressed.connect(slot)
+
+    def show_file_browser_and_return_selection(self, file_filter, search_directories, multiple_files=False):
+        default_directory = search_directories[0]
+        print("OPENING FILE BROWSER")
+        if multiple_files:
+            chosen_files = QtGui.QFileDialog.getOpenFileNames(self, "Select files", default_directory,
+                                                              file_filter)
+            return [str(chosen_file) for chosen_file in chosen_files]
+        else:
+            chosen_file = QtGui.QFileDialog.getOpenFileName(self, "Select file", default_directory,
+                                                            file_filter)
+            return [str(chosen_file)]
+
+    def disable_loading(self):
+        self.disable_load_buttons()
+        self.loadingStarted.emit()
+
+    def enable_loading(self):
+        self.enable_load_buttons()
+        self.loadingFinished.emit()
+
+    def notify_loading_started(self):
+        self.loadingStarted.emit()
+
+    def notify_loading_finished(self):
+        self.loadingFinished.emit()
+        self.dataChanged.emit()
+
+    def disable_load_buttons(self):
+        self.browse_button.setEnabled(False)
+        self.file_path_edit.setEnabled(False)
+
+    def enable_load_buttons(self):
+        self.browse_button.setEnabled(True)
+        self.file_path_edit.setEnabled(True)
+
+    def get_file_edit_text(self):
+        if self._store_edit_text:
+            return str(self._stored_edit_text)
+        else:
+            return str(self.file_path_edit.text())
+
+    def set_file_edit(self, text, store=False):
+        if store:
+            self._store_edit_text = True
+            self._stored_edit_text = text
+            self.file_path_edit.setText("(... more than 10 files, use right-click -> copy)")
+        else:
+            self.file_path_edit.setText(text)
+        self._cached_text = self.get_file_edit_text()
+
+    def clear(self):
+        self.set_file_edit("No data loaded")
+        self._store_edit_text = False
+        self._cached_text = "No data loaded"
+
+    def reset_edit_to_cached_value(self):
+        tmp = self._cached_text
+        self.set_file_edit(tmp)
+        self._cached_text = tmp
+
+    def warning_popup(self, message):
+        message_box.warning(str(message))
diff --git a/scripts/test/ErrorReportPresenterTest.py b/scripts/test/ErrorReportPresenterTest.py
index 3f1a89e1e8ccc1638b4752f21304c9f13cb0fd4d..66d4413a5681faf37bf463858c03d80d40d70b8f 100644
--- a/scripts/test/ErrorReportPresenterTest.py
+++ b/scripts/test/ErrorReportPresenterTest.py
@@ -26,10 +26,6 @@ class ErrorReportPresenterTest(unittest.TestCase):
         self.zip_recovery_mock = zip_recovery_patcher.start()
         self.zip_recovery_mock.return_value = ('zipped_file', 'file_hash')
 
-        file_removal_patcher = mock.patch('ErrorReporter.error_report_presenter.remove_recovery_file')
-        self.addCleanup(file_removal_patcher.stop)
-        self.file_removal_mock = file_removal_patcher.start()
-
         self.view = mock.MagicMock()
         self.exit_code = 255
         self.error_report_presenter = ErrorReporterPresenter(self.view, self.exit_code)
@@ -97,7 +93,7 @@ class ErrorReportPresenterTest(unittest.TestCase):
         self.error_report_presenter._send_report_to_server = mock.MagicMock(return_value=201)
         self.error_report_presenter._upload_recovery_file = mock.MagicMock()
         self.error_report_presenter._handle_exit = mock.MagicMock()
-        
+
         self.error_report_presenter.error_handler(continue_working, share, name, email, text_box)
 
         self.error_report_presenter._send_report_to_server.called_once_with(share_identifiable=True, name=name, email=email,
diff --git a/scripts/test/Muon/CMakeLists.txt b/scripts/test/Muon/CMakeLists.txt
index fec446733c0646dc792ee4554bc8aa7c7bc3e6ce..f29fc35f9acbf87c07344ddd84c2ff3c6b383404 100644
--- a/scripts/test/Muon/CMakeLists.txt
+++ b/scripts/test/Muon/CMakeLists.txt
@@ -7,6 +7,10 @@ set ( TEST_PY_FILES
    AxisChangerView_test.py
    FFTModel_test.py
    FFTPresenter_test.py
+   load_file_widget/loadfile_model_test.py
+   load_file_widget/loadfile_presenter_single_file_test.py
+   load_file_widget/loadfile_presenter_multiple_file_test.py
+   load_file_widget/loadfile_view_test.py
    load_run_widget/loadrun_model_test.py
    load_run_widget/loadrun_presenter_current_run_test.py
    load_run_widget/loadrun_presenter_single_file_test.py
diff --git a/scripts/test/Muon/PlottingView_test.py b/scripts/test/Muon/PlottingView_test.py
index 16c54482ebee6b48dbd2c307852e1545c0472af4..7fbe9ad99ee98bcd8b123915c3c1931674bbd68b 100644
--- a/scripts/test/Muon/PlottingView_test.py
+++ b/scripts/test/Muon/PlottingView_test.py
@@ -6,12 +6,12 @@
 # SPDX - License - Identifier: GPL - 3.0 +
 import unittest
 
-import os
+import os, sys
 os.environ["QT_API"] = "pyqt"  # noqa E402
 
 from matplotlib.figure import Figure
 
-from mantid.simpleapi import *
+from mantid import WorkspaceFactory
 from mantid import plots
 from Muon.GUI.ElementalAnalysis.Plotting.subPlot_object import subPlot
 from Muon.GUI.ElementalAnalysis.Plotting.plotting_view import PlotView
@@ -26,7 +26,12 @@ except ImportError:
 
 
 def get_subPlot(name):
-    ws1 = CreateWorkspace(DataX=[1, 2, 3, 4], DataY=[4, 5, 6, 7], NSpec=2)
+    data_x, data_y = [1, 2, 3, 4], [4, 5, 6, 7]
+    nspec = 2
+    ws1 = WorkspaceFactory.create("Workspace2D", nspec, len(data_x), len(data_y))
+    for i in range(ws1.getNumberHistograms()):
+        ws1.setX(i, data_x)
+        ws1.setY(i, data_y)
     label1 = "test"
     # create real lines
     fig = Figure()
@@ -37,7 +42,7 @@ def get_subPlot(name):
     subplot.addLine(label1, line1, ws1, 2)
     return subplot, ws1
 
-
+@unittest.skipIf(lambda: sys.platform=='win32'(), "Test segfaults on Windows and code will be removed soon")
 class PlottingViewHelperFunctionTests(unittest.TestCase):
 
     def setUp(self):
@@ -294,6 +299,7 @@ class PlottingViewHelperFunctionTests(unittest.TestCase):
             list(self.view.plots.keys()))
 
 
+@unittest.skipIf(lambda: sys.platform=='win32'(), "Test segfaults on Windows and code will be removed soon")
 class PlottingViewPlotFunctionsTests(unittest.TestCase):
 
     def setUp(self):
diff --git a/scripts/test/Muon/load_file_widget/__init__.py b/scripts/test/Muon/load_file_widget/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/scripts/test/Muon/load_file_widget/loadfile_model_test.py b/scripts/test/Muon/load_file_widget/loadfile_model_test.py
new file mode 100644
index 0000000000000000000000000000000000000000..aea20f20e38de326433dae87c7637df582d5888d
--- /dev/null
+++ b/scripts/test/Muon/load_file_widget/loadfile_model_test.py
@@ -0,0 +1,83 @@
+import sys
+import six
+
+import unittest
+
+if sys.version_info.major < 2:
+    from unittest import mock
+else:
+    import mock
+
+from Muon.GUI.Common.load_file_widget.model import BrowseFileWidgetModel
+from Muon.GUI.Common.muon_load_data import MuonLoadData
+from Muon.GUI.Common.utilities.muon_test_helpers import IteratorWithException
+
+class LoadFileWidgetModelTest(unittest.TestCase):
+
+    def setUp(self):
+        self.data = MuonLoadData()
+        self.model = BrowseFileWidgetModel(self.data)
+
+        patcher = mock.patch('Muon.GUI.Common.load_file_widget.model.load_utils')
+        self.addCleanup(patcher.stop)
+        self.load_utils_patcher = patcher.start()
+
+    def mock_load_function(self, files_to_load, load_return_values):
+        self.load_utils_patcher.load_workspace_from_filename = mock.Mock(side_effect=load_return_values)
+        self.model.loadData(files_to_load)
+
+    def assert_model_empty(self):
+        self.assertEqual(self.model.loaded_workspaces, [])
+        self.assertEqual(self.model.loaded_filenames, [])
+        self.assertEqual(self.model.loaded_runs, [])
+
+    # ------------------------------------------------------------------------------------------------------------------
+    # TESTS
+    # ------------------------------------------------------------------------------------------------------------------
+
+    def test_model_initialized_with_empty_lists_of_loaded_data(self):
+        self.assert_model_empty()
+
+    def test_executing_load_without_filenames_does_nothing(self):
+        self.model.execute()
+        self.assert_model_empty()
+
+    def test_execute_successfully_loads_given_files(self):
+        files = ['EMU00019489.nxs', 'EMU00019490.nxs', 'EMU00019491.nxs']
+        load_return_vals = [([1 + i], 19489 + i, filename) for i, filename in enumerate(files)]
+        self.mock_load_function(files, load_return_vals)
+
+        self.model.execute()
+
+        six.assertCountEqual(self, self.model.loaded_workspaces, [[1], [2], [3]])
+        six.assertCountEqual(self, self.model.loaded_filenames, files)
+        six.assertCountEqual(self, self.model.loaded_runs, [19489, 19490, 19491])
+
+    def test_model_is_cleared_correctly(self):
+        files = [r'EMU00019489.nxs', r'EMU00019490.nxs', r'EMU00019491.nxs']
+        load_return_vals = [([1 + i], 19489 + i, filename) for i, filename in enumerate(files)]
+        self.mock_load_function(files, load_return_vals)
+
+        self.model.execute()
+        self.assertEqual(len(self.model.loaded_filenames), 3)
+        self.model.clear()
+
+        self.assert_model_empty()
+
+    def test_execute_throws_if_one_file_does_not_load_correctly_but_still_loads_other_files(self):
+        files = [r'EMU00019489.nxs', r'EMU00019490.nxs', r'EMU00019491.nxs']
+        load_return_vals = [([1 + i], 19489 + i, filename) for i, filename in enumerate(files)]
+
+        # Mock load to throw on a particular index
+        self.load_utils_patcher.load_workspace_from_filename = mock.Mock()
+        self.load_utils_patcher.load_workspace_from_filename.side_effect = iter(IteratorWithException(load_return_vals, [1]))
+        self.model.loadData(files)
+        with self.assertRaises(ValueError):
+            self.model.execute()
+
+        six.assertCountEqual(self, self.model.loaded_filenames, [files[i] for i in [0, 2]])
+        six.assertCountEqual(self, self.model.loaded_runs, [19489, 19491])
+
+
+if __name__ == '__main__':
+    unittest.main(buffer=False, verbosity=2)
diff --git a/scripts/test/Muon/load_file_widget/loadfile_presenter_multiple_file_test.py b/scripts/test/Muon/load_file_widget/loadfile_presenter_multiple_file_test.py
new file mode 100644
index 0000000000000000000000000000000000000000..f9f36e8e9dacdf1050c404b49071132825f9dbd1
--- /dev/null
+++ b/scripts/test/Muon/load_file_widget/loadfile_presenter_multiple_file_test.py
@@ -0,0 +1,247 @@
+import six
+import sys
+import unittest
+
+if sys.version_info.major == 3:
+    from unittest import mock
+else:
+    import mock
+
+from Muon.GUI.Common import mock_widget
+from Muon.GUI.Common.load_file_widget.view import BrowseFileWidgetView
+from Muon.GUI.Common.load_file_widget.presenter import BrowseFileWidgetPresenter
+from Muon.GUI.Common.load_file_widget.model import BrowseFileWidgetModel
+from Muon.GUI.Common.muon_load_data import MuonLoadData
+
+
+class IteratorWithException:
+    """Wraps a simple iterable (i.e. list) so that it throws a ValueError on a particular index."""
+
+    def __init__(self, iterable, throw_on_index):
+        self.max = len(iterable)
+        self.iterable = iter(iterable)
+
+        self.throw_indices = [index for index in throw_on_index if index < self.max]
+
+    def __iter__(self):
+        self.n = 0
+        return self
+
+    def __next__(self):
+
+        if self.n in self.throw_indices:
+            next(self.iterable)
+            self.n += 1
+            raise ValueError()
+        elif self.n == self.max:
+            raise StopIteration()
+        else:
+            self.n += 1
+            return next(self.iterable)
+
+    next = __next__
+
+
+class LoadFileWidgetPresenterMultipleFileModeTest(unittest.TestCase):
+    def run_test_with_and_without_threading(test_function):
+
+        def run_twice(self):
+            test_function(self)
+            self.setUp()
+            self.presenter._use_threading = False
+            test_function(self)
+
+        return run_twice
+
+    def wait_for_thread(self, thread_model):
+        if thread_model:
+            thread_model._thread.wait()
+            self._qapp.processEvents()
+
+    def setUp(self):
+        self._qapp = mock_widget.mockQapp()
+        self.data = MuonLoadData()
+        self.view = BrowseFileWidgetView()
+        self.model = BrowseFileWidgetModel(self.data)
+
+        self.view.disable_load_buttons = mock.Mock()
+        self.view.enable_load_buttons = mock.Mock()
+        self.view.warning_popup = mock.Mock()
+
+        self.presenter = BrowseFileWidgetPresenter(self.view, self.model)
+        self.presenter.enable_multiple_files(True)
+
+        patcher = mock.patch('Muon.GUI.Common.load_file_widget.model.load_utils')
+        self.addCleanup(patcher.stop)
+        self.load_utils_patcher = patcher.start()
+
+    def mock_loading_multiple_files_from_browse(self, runs, workspaces, filenames):
+        self.view.show_file_browser_and_return_selection = mock.Mock(return_value=filenames)
+        self.load_utils_patcher.load_workspace_from_filename = mock.Mock(side_effect=zip(workspaces, runs, filenames))
+
+    # ------------------------------------------------------------------------------------------------------------------
+    # TESTS : Multiple runs can be selected via browse and entered explicitly using the ";" separator
+    # ------------------------------------------------------------------------------------------------------------------
+
+    @run_test_with_and_without_threading
+    def test_that_cannot_load_same_file_twice_from_same_browse_even_if_filepaths_are_different(self):
+        self.view.show_file_browser_and_return_selection = mock.Mock(
+            return_value=["C:/dir1/file1.nxs", "C:/dir2/file1.nxs", "C:/dir2/file2.nxs"])
+
+        self.presenter.on_browse_button_clicked()
+        self.wait_for_thread(self.presenter._load_thread)
+
+        self.assertEqual(self.load_utils_patcher.load_workspace_from_filename.call_count, 2)
+        self.load_utils_patcher.load_workspace_from_filename.assert_any_call("C:/dir1/file1.nxs")
+        self.load_utils_patcher.load_workspace_from_filename.assert_any_call("C:/dir2/file2.nxs")
+
+    @run_test_with_and_without_threading
+    def test_that_cannot_load_same_file_twice_from_user_input_even_if_filepaths_are_different(self):
+        self.view.set_file_edit("C:/dir1/file1.nxs;C:/dir2/file1.nxs;C:/dir2/file2.nxs")
+
+        self.presenter.handle_file_changed_by_user()
+        self.wait_for_thread(self.presenter._load_thread)
+
+        self.assertEqual(self.load_utils_patcher.load_workspace_from_filename.call_count, 2)
+        self.load_utils_patcher.load_workspace_from_filename.assert_any_call("C:/dir1/file1.nxs")
+        self.load_utils_patcher.load_workspace_from_filename.assert_any_call("C:/dir2/file2.nxs")
+
+    @run_test_with_and_without_threading
+    def test_that_cannot_browse_and_load_same_run_twice_even_if_filenames_are_different(self):
+        self.load_utils_patcher.load_workspace_from_filename = mock.Mock(side_effect=zip([[1], [2]], [1234, 1234], ["C:/dir1/file1.nxs", "C:/dir1/file2.nxs"]))
+        self.view.show_file_browser_and_return_selection = mock.Mock(
+            return_value=["C:/dir1/file1.nxs", "C:/dir1/file2.nxs"])
+
+        self.presenter.on_browse_button_clicked()
+        self.wait_for_thread(self.presenter._load_thread)
+
+        # Load will take the last occurrence of the run from the list
+        six.assertCountEqual(self, self.model.loaded_filenames, ["C:/dir1/file2.nxs"])
+        six.assertCountEqual(self, self.model.loaded_workspaces, [[2]])
+        six.assertCountEqual(self, self.model.loaded_runs, [1234])
+
+    @run_test_with_and_without_threading
+    def test_that_cannot_input_and_load_same_run_twice_even_if_filenames_are_different(self):
+        self.load_utils_patcher.load_workspace_from_filename = mock.Mock(side_effect=zip([[1], [2]], [1234, 1234], ["C:/dir1/file1.nxs", "C:/dir1/file2.nxs"]))
+        self.view.set_file_edit("C:/dir1/file1.nxs;C:/dir1/file2.nxs")
+
+        self.presenter.handle_file_changed_by_user()
+        self.wait_for_thread(self.presenter._load_thread)
+
+        # Load will take the last occurrence of the run from the user input
+        six.assertCountEqual(self, self.model.loaded_filenames, ["C:/dir1/file2.nxs"])
+        six.assertCountEqual(self, self.model.loaded_workspaces, [[2]])
+        six.assertCountEqual(self, self.model.loaded_runs, [1234])
+
+    @run_test_with_and_without_threading
+    def test_that_loading_two_files_from_browse_sets_model_and_interface_correctly(self):
+        self.presenter.enable_multiple_files(True)
+        self.mock_loading_multiple_files_from_browse([1234, 1235], [[1], [2]],
+                                                     ["C:/dir1/file1.nxs", "C:/dir2/file2.nxs"])
+
+        self.presenter.on_browse_button_clicked()
+        self.wait_for_thread(self.presenter._load_thread)
+
+        self.assertEqual(self.model.loaded_filenames, ["C:/dir1/file1.nxs", "C:/dir2/file2.nxs"])
+        self.assertEqual(self.model.loaded_workspaces, [[1], [2]])
+        self.assertEqual(self.model.loaded_runs, [1234, 1235])
+
+        self.assertEqual(self.view.get_file_edit_text(), "C:/dir1/file1.nxs;C:/dir2/file2.nxs")
+
+    @run_test_with_and_without_threading
+    def test_that_loading_two_files_from_user_input_sets_model_and_interface_correctly(self):
+        self.presenter.enable_multiple_files(True)
+        self.load_utils_patcher.load_workspace_from_filename = mock.Mock(side_effect=zip([[1], [2]], [1234, 1235], ["C:/dir1/file1.nxs", "C:/dir2/file2.nxs"]))
+        self.view.set_file_edit("C:/dir1/file1.nxs;C:/dir2/file2.nxs")
+
+        self.presenter.handle_file_changed_by_user()
+        self.wait_for_thread(self.presenter._load_thread)
+
+        self.assertEqual(self.model.loaded_filenames, ["C:/dir1/file1.nxs", "C:/dir2/file2.nxs"])
+        self.assertEqual(self.model.loaded_workspaces, [[1], [2]])
+        self.assertEqual(self.model.loaded_runs, [1234, 1235])
+
+        self.assertEqual(self.view.get_file_edit_text(), "C:/dir1/file1.nxs;C:/dir2/file2.nxs")
+
+    @run_test_with_and_without_threading
+    def test_that_loading_two_files_from_browse_sets_interface_alphabetically(self):
+        self.mock_loading_multiple_files_from_browse([1234, 1235], [[1], [2]],
+                                                     ["C:/dir1/file2.nxs", "C:/dir1/file1.nxs"])
+
+        self.presenter.on_browse_button_clicked()
+        self.wait_for_thread(self.presenter._load_thread)
+
+        self.assertEqual(self.view.get_file_edit_text(), "C:/dir1/file1.nxs;C:/dir1/file2.nxs")
+
+    @run_test_with_and_without_threading
+    def test_that_loading_two_files_from_user_input_sets_interface_alphabetically(self):
+        self.load_utils_patcher.load_workspace_from_filename = mock.Mock(
+            side_effect=zip([[2], [1]], [1235, 1234], ["C:/dir1/file2.nxs", "C:/dir1/file1.nxs"]))
+        self.view.set_file_edit("C:/dir1/file2.nxs;C:/dir1/file1.nxs")
+
+        self.presenter.handle_file_changed_by_user()
+        self.wait_for_thread(self.presenter._load_thread)
+
+        self.assertEqual(self.view.get_file_edit_text(), "C:/dir1/file1.nxs;C:/dir1/file2.nxs")
+
+    @run_test_with_and_without_threading
+    def test_that_loading_multiple_files_from_browse_ignores_loads_which_throw(self):
+        self.presenter.enable_multiple_files(True)
+
+        files = ["C:/dir1/file1.nxs", "C:/dir2/file2.nxs", "C:/dir2/file3.nxs"]
+        self.view.show_file_browser_and_return_selection = mock.Mock(return_value=files)
+        load_return_values = [([1], 1234 + i, filename) for i, filename in enumerate(files)]
+        self.load_utils_patcher.load_workspace_from_filename = mock.Mock(
+            side_effect=iter(IteratorWithException(load_return_values, [1])))
+
+        self.presenter.on_browse_button_clicked()
+        self.wait_for_thread(self.presenter._load_thread)
+
+        self.assertEqual(self.model.loaded_filenames, ["C:/dir1/file1.nxs", "C:/dir2/file3.nxs"])
+        self.assertEqual(self.model.loaded_runs, [1234, 1236])
+        self.assertEqual(self.view.get_file_edit_text(), "C:/dir1/file1.nxs;C:/dir2/file3.nxs")
+
+    @run_test_with_and_without_threading
+    def test_that_browse_allows_loading_of_additional_files(self):
+        self.mock_loading_multiple_files_from_browse([1234, 1235], [[1], [2]],
+                                                     ["C:/dir1/file1.nxs", "C:/dir2/file2.nxs"])
+
+        self.presenter.on_browse_button_clicked()
+        self.wait_for_thread(self.presenter._load_thread)
+
+        self.mock_loading_multiple_files_from_browse([1236], [[3]], ["C:/dir1/file3.nxs"])
+
+        self.presenter.on_browse_button_clicked()
+        self.wait_for_thread(self.presenter._load_thread)
+
+        six.assertCountEqual(self, self.model.loaded_filenames,
+                             ["C:/dir1/file1.nxs", "C:/dir2/file2.nxs", "C:/dir1/file3.nxs"])
+        six.assertCountEqual(self, self.model.loaded_workspaces, [[1], [2], [3]])
+        six.assertCountEqual(self, self.model.loaded_runs, [1234, 1235, 1236])
+
+        self.assertEqual(self.view.get_file_edit_text(), "C:/dir1/file1.nxs;C:/dir1/file3.nxs;C:/dir2/file2.nxs")
+
+    @run_test_with_and_without_threading
+    def test_that_loading_an_already_loaded_file_from_browse_overwrites_it(self):
+        self.mock_loading_multiple_files_from_browse([1234, 1235], [[1], [2]],
+                                                     ["C:/dir1/file1.nxs", "C:/dir2/file2.nxs"])
+
+        self.presenter.on_browse_button_clicked()
+        self.wait_for_thread(self.presenter._load_thread)
+
+        # only checks runs, so can have a different file/workspace (this is why overwriting is
+        # the most useful behaviour in this situation).
+        self.mock_loading_multiple_files_from_browse([1234], [[3]], ["C:/dir2/file1.nxs"])
+
+        self.presenter.on_browse_button_clicked()
+        self.wait_for_thread(self.presenter._load_thread)
+
+        six.assertCountEqual(self, self.model.loaded_filenames, ["C:/dir2/file1.nxs", "C:/dir2/file2.nxs"])
+        six.assertCountEqual(self, self.model.loaded_workspaces, [[3], [2]])
+        six.assertCountEqual(self, self.model.loaded_runs, [1234, 1235])
+
+        self.assertEqual(self.view.get_file_edit_text(), "C:/dir2/file1.nxs;C:/dir2/file2.nxs")
+
+
+if __name__ == '__main__':
+    unittest.main(buffer=False, verbosity=2)
diff --git a/scripts/test/Muon/load_file_widget/loadfile_presenter_single_file_test.py b/scripts/test/Muon/load_file_widget/loadfile_presenter_single_file_test.py
new file mode 100644
index 0000000000000000000000000000000000000000..0506feab4afce72fd710e7629ff3798db931f93a
--- /dev/null
+++ b/scripts/test/Muon/load_file_widget/loadfile_presenter_single_file_test.py
@@ -0,0 +1,302 @@
+# Mantid Repository : https://github.com/mantidproject/mantid
+#
+# Copyright &copy; 2018 ISIS Rutherford Appleton Laboratory UKRI,
+#     NScD Oak Ridge National Laboratory, European Spallation Source
+#     & Institut Laue - Langevin
+# SPDX - License - Identifier: GPL - 3.0 +
+import sys
+import unittest
+
+if sys.version_info.major == 3:
+    from unittest import mock
+else:
+    import mock
+
+from Muon.GUI.Common.load_file_widget.view import BrowseFileWidgetView
+from Muon.GUI.Common.load_file_widget.presenter import BrowseFileWidgetPresenter
+from Muon.GUI.Common.load_file_widget.model import BrowseFileWidgetModel
+from Muon.GUI.Common.muon_load_data import MuonLoadData
+from Muon.GUI.Common import mock_widget
+
+
+class IteratorWithException:
+    """Wraps a simple iterable (i.e. list) so that it throws a ValueError on a particular index."""
+
+    def __init__(self, iterable, throw_on_index):
+        self.max = len(iterable)
+        self.iterable = iter(iterable)
+
+        self.throw_indices = [index for index in throw_on_index if index < self.max]
+
+    def __iter__(self):
+        self.n = 0
+        return self
+
+    def __next__(self):
+
+        if self.n in self.throw_indices:
+            next(self.iterable)
+            self.n += 1
+            raise ValueError()
+        elif self.n == self.max:
+            raise StopIteration()
+        else:
+            self.n += 1
+            return next(self.iterable)
+
+    next = __next__
+
+
+class LoadFileWidgetPresenterTest(unittest.TestCase):
+    def run_test_with_and_without_threading(test_function):
+
+        def run_twice(self):
+            test_function(self)
+            self.setUp()
+            self.presenter._use_threading = False
+            test_function(self)
+
+        return run_twice
+
+    def wait_for_thread(self, thread_model):
+        if thread_model:
+            thread_model._thread.wait()
+            self._qapp.processEvents()
+
+    def setUp(self):
+        self._qapp = mock_widget.mockQapp()
+        self.view = BrowseFileWidgetView()
+
+        self.view.on_browse_clicked = mock.Mock()
+        self.view.set_file_edit = mock.Mock()
+        self.view.reset_edit_to_cached_value = mock.Mock()
+        self.view.show_file_browser_and_return_selection = mock.Mock(
+            return_value=["C:/dir1/file1.nxs", "C:/dir2/file2.nxs"])
+
+        self.data = MuonLoadData()
+        self.model = BrowseFileWidgetModel(self.data)
+        self.model.exception_message_for_failed_files = mock.Mock()
+
+        self.view.disable_load_buttons = mock.Mock()
+        self.view.enable_load_buttons = mock.Mock()
+        self.view.warning_popup = mock.Mock()
+
+        self.presenter = BrowseFileWidgetPresenter(self.view, self.model)
+        self.presenter.enable_multiple_files(False)
+
+        patcher = mock.patch('Muon.GUI.Common.load_file_widget.model.load_utils')
+        self.addCleanup(patcher.stop)
+        self.load_utils_patcher = patcher.start()
+
+    def mock_browse_button_to_return_files(self, files):
+        self.view.show_file_browser_and_return_selection = mock.Mock(return_value=files)
+
+    def mock_user_input_text(self, text):
+        self.view.get_file_edit_text = mock.Mock(return_value=text)
+
+    def mock_model_to_load_workspaces(self, workspaces, runs, filenames):
+        self.load_utils_patcher.load_workspace_from_filename = mock.Mock(side_effect=zip(workspaces, runs, filenames))
+
+    def load_workspaces_into_model_and_view_from_browse(self, workspaces, runs, files):
+        self.mock_model_to_load_workspaces(workspaces, runs, files)
+        self.mock_browse_button_to_return_files(files)
+
+        self.presenter.on_browse_button_clicked()
+        self.wait_for_thread(self.presenter._load_thread)
+
+    def load_failure(self):
+        raise ValueError("Error text")
+
+    # ------------------------------------------------------------------------------------------------------------------
+    # TESTS
+    # ------------------------------------------------------------------------------------------------------------------
+
+    @run_test_with_and_without_threading
+    def test_browser_dialog_opens_when_browse_button_clicked(self):
+        self.mock_browse_button_to_return_files(["file.nxs"])
+
+        self.presenter.on_browse_button_clicked()
+        self.wait_for_thread(self.presenter._load_thread)
+
+        self.assertEqual(self.view.show_file_browser_and_return_selection.call_count, 1)
+
+    @run_test_with_and_without_threading
+    def test_loading_not_initiated_if_no_file_selected_from_browser(self):
+        self.mock_model_to_load_workspaces([], [], [])
+        self.mock_browse_button_to_return_files([])
+
+        self.presenter.on_browse_button_clicked()
+        self.wait_for_thread(self.presenter._load_thread)
+
+        self.assertEqual(self.load_utils_patcher.load_workspace_from_filename.call_count, 0)
+
+    @run_test_with_and_without_threading
+    def test_buttons_disabled_while_load_thread_running(self):
+        self.mock_browse_button_to_return_files(["file.nxs"])
+
+        self.presenter.on_browse_button_clicked()
+        self.wait_for_thread(self.presenter._load_thread)
+
+        self.load_utils_patcher.load_workspace_from_filename.assert_called_once_with("file.nxs")
+        self.assertEqual(self.view.disable_load_buttons.call_count, 1)
+        self.assertEqual(self.view.enable_load_buttons.call_count, 1)
+
+    @run_test_with_and_without_threading
+    def test_buttons_enabled_after_load_even_if_load_thread_throws(self):
+        self.mock_browse_button_to_return_files(["file.nxs"])
+        self.load_utils_patcher.load_workspace_from_filename.side_effect = self.load_failure
+
+        self.presenter.on_browse_button_clicked()
+        self.wait_for_thread(self.presenter._load_thread)
+
+        self.load_utils_patcher.load_workspace_from_filename.assert_called_once_with("file.nxs")
+        self.assertEqual(self.view.disable_load_buttons.call_count, 1)
+        self.assertEqual(self.view.enable_load_buttons.call_count, 1)
+
+    @run_test_with_and_without_threading
+    def test_files_not_loaded_into_model_if_multiple_files_selected_from_browse_in_single_file_mode(self):
+        self.mock_model_to_load_workspaces([[1], [2]], [1234, 1235], ["C:/dir1/file1.nxs", "C:/dir2/file2.nxs"])
+        self.mock_browse_button_to_return_files(["C:/dir1/file1.nxs", "C:/dir2/file2.nxs"])
+        self.model.execute = mock.Mock()
+
+        self.presenter.on_browse_button_clicked()
+        self.wait_for_thread(self.presenter._load_thread)
+
+        self.assertEqual(self.model.execute.call_count, 0)
+        self.assertEqual(self.view.disable_load_buttons.call_count, 0)
+        self.assertEqual(self.view.enable_load_buttons.call_count, 0)
+
+    @run_test_with_and_without_threading
+    def test_files_not_loaded_into_model_if_multiple_files_entered_by_user_in_single_file_mode(self):
+        self.mock_user_input_text("C:/dir1/file1.nxs;C:/dir2/file2.nxs")
+        self.mock_model_to_load_workspaces([[1], [2]], [1234, 1235], ["C:/dir1/file1.nxs", "C:/dir2/file2.nxs"])
+        self.model.execute = mock.Mock()
+
+        self.presenter.handle_file_changed_by_user()
+        self.wait_for_thread(self.presenter._load_thread)
+
+        self.assertEqual(self.model.execute.call_count, 0)
+        self.assertEqual(self.view.disable_load_buttons.call_count, 0)
+        self.assertEqual(self.view.enable_load_buttons.call_count, 0)
+
+    @run_test_with_and_without_threading
+    def test_warning_shown_if_multiple_files_selected_from_browse_in_single_file_mode(self):
+        self.mock_browse_button_to_return_files(["C:/dir1/file1.nxs", "C:/dir2/file2.nxs"])
+        self.mock_model_to_load_workspaces([[1], [2]], [1234, 1235], ["C:/dir1/file1.nxs", "C:/dir2/file2.nxs"])
+
+        self.presenter.on_browse_button_clicked()
+        self.wait_for_thread(self.presenter._load_thread)
+
+        self.assertEqual(self.view.warning_popup.call_count, 1)
+
+    @run_test_with_and_without_threading
+    def test_warning_shown_if_multiple_files_entered_by_user_in_single_file_mode(self):
+        self.mock_user_input_text("C:/dir1/file1.nxs;C:/dir2/file2.nxs")
+        self.mock_model_to_load_workspaces([[1], [2]], [1234, 1235], ["C:/dir1/file1.nxs;C:/dir2/file2.nxs"])
+
+        self.presenter.handle_file_changed_by_user()
+        self.wait_for_thread(self.presenter._load_thread)
+
+        self.assertEqual(self.view.warning_popup.call_count, 1)
+
+    @run_test_with_and_without_threading
+    def test_single_file_from_browse_loaded_into_model_and_view_in_single_file_mode(self):
+        self.mock_browse_button_to_return_files(["C:/dir1/file1.nxs"])
+        self.mock_model_to_load_workspaces([[1]], [1234], ["C:/dir1/file1.nxs"])
+        self.view.set_file_edit = mock.Mock()
+
+        self.presenter.on_browse_button_clicked()
+        self.wait_for_thread(self.presenter._load_thread)
+
+        self.assertEqual(self.model.loaded_filenames, ["C:/dir1/file1.nxs"])
+        self.assertEqual(self.model.loaded_workspaces, [[1]])
+        self.assertEqual(self.model.loaded_runs, [1234])
+
+        self.view.set_file_edit.assert_called_once_with("C:/dir1/file1.nxs", mock.ANY)
+
+    @run_test_with_and_without_threading
+    def test_single_file_from_user_input_loaded_into_model_and_view_in_single_file_mode(self):
+        self.view.set_file_edit = mock.Mock()
+        self.mock_model_to_load_workspaces([[1]], [1234], ["C:/dir1/file1.nxs"])
+        self.mock_user_input_text("C:/dir1/file1.nxs")
+
+        self.presenter.handle_file_changed_by_user()
+        self.wait_for_thread(self.presenter._load_thread)
+
+        self.assertEqual(self.model.loaded_filenames, ["C:/dir1/file1.nxs"])
+        self.assertEqual(self.model.loaded_workspaces, [[1]])
+        self.assertEqual(self.model.loaded_runs, [1234])
+
+        self.view.set_file_edit.assert_called_once_with("C:/dir1/file1.nxs", mock.ANY)
+
+    @run_test_with_and_without_threading
+    def test_that_if_invalid_file_selected_in_browser_view_does_not_change(self):
+        self.mock_browse_button_to_return_files(["not_a_file"])
+        self.mock_model_to_load_workspaces([[1]], [1234], ["not_a_file"])
+
+        self.view.set_file_edit = mock.Mock()
+        self.view.reset_edit_to_cached_value = mock.Mock()
+
+        self.presenter.on_browse_button_clicked()
+        self.wait_for_thread(self.presenter._load_thread)
+
+        self.load_utils_patcher.load_workspace_from_filename = mock.Mock(side_effect=self.load_failure)
+
+        set_file_edit_count = self.view.set_file_edit.call_count
+        self.presenter.on_browse_button_clicked()
+        self.wait_for_thread(self.presenter._load_thread)
+
+        self.assertEqual(self.view.set_file_edit.call_count, set_file_edit_count)
+        self.assertEqual(self.view.reset_edit_to_cached_value.call_count, 0)
+
+    @run_test_with_and_without_threading
+    def test_that_view_reverts_to_previous_text_if_users_supplies_invalid_text(self):
+        self.load_workspaces_into_model_and_view_from_browse([[1]], [1234], ["C:/dir1/EMU0001234.nxs"])
+
+        invalid_user_input = ["some random text", "1+1=2", "..."]
+
+        call_count = self.view.reset_edit_to_cached_value.call_count
+        for invalid_text in invalid_user_input:
+            call_count += 1
+            self.view.get_file_edit_text = mock.Mock(return_value=invalid_text)
+
+            self.presenter.handle_file_changed_by_user()
+            self.wait_for_thread(self.presenter._load_thread)
+
+            self.assertEqual(self.view.reset_edit_to_cached_value.call_count, call_count)
+
+    @run_test_with_and_without_threading
+    def test_that_model_and_interface_revert_to_previous_values_if_load_fails_from_browse(self):
+        self.load_workspaces_into_model_and_view_from_browse([[1]], [1234], ["C:/dir1/EMU0001234.nxs"])
+
+        self.load_utils_patcher.load_workspace_from_filename = mock.Mock(side_effect=self.load_failure)
+
+        self.presenter.on_browse_button_clicked()
+        self.wait_for_thread(self.presenter._load_thread)
+
+        self.assertEqual(self.model.loaded_filenames, ["C:/dir1/EMU0001234.nxs"])
+        self.assertEqual(self.model.loaded_workspaces, [[1]])
+        self.assertEqual(self.model.loaded_runs, [1234])
+
+        self.assertEqual(self.view.reset_edit_to_cached_value.call_count, 0)
+        self.assertEqual(self.view.set_file_edit.call_args[0][0], "C:/dir1/EMU0001234.nxs")
+
+    @run_test_with_and_without_threading
+    def test_that_model_and_interface_revert_to_previous_values_if_load_fails_from_user_input(self):
+        self.load_workspaces_into_model_and_view_from_browse([[1]], [1234], ["C:/dir1/EMU0001234.nxs"])
+
+        self.load_utils_patcher.load_workspace_from_filename = mock.Mock(side_effect=self.load_failure)
+        self.view.set_file_edit("C:\dir2\EMU000123.nxs")
+
+        self.presenter.handle_file_changed_by_user()
+        self.wait_for_thread(self.presenter._load_thread)
+
+        self.assertEqual(self.model.loaded_filenames, ["C:/dir1/EMU0001234.nxs"])
+        self.assertEqual(self.model.loaded_workspaces, [[1]])
+        self.assertEqual(self.model.loaded_runs, [1234])
+
+        self.assertEqual(self.view.reset_edit_to_cached_value.call_count, 1)
+
+
+if __name__ == '__main__':
+    unittest.main(buffer=False, verbosity=2)
diff --git a/scripts/test/Muon/load_file_widget/loadfile_view_test.py b/scripts/test/Muon/load_file_widget/loadfile_view_test.py
new file mode 100644
index 0000000000000000000000000000000000000000..fa8ea3a4783b9660e0fd2515252ca1f39418ca96
--- /dev/null
+++ b/scripts/test/Muon/load_file_widget/loadfile_view_test.py
@@ -0,0 +1,47 @@
+import unittest
+
+from Muon.GUI.Common import mock_widget
+
+from Muon.GUI.Common.load_file_widget.view import BrowseFileWidgetView
+
+
+class LoadFileWidgetViewTest(unittest.TestCase):
+
+    def setUp(self):
+        self._qapp = mock_widget.mockQapp()
+        self.view = BrowseFileWidgetView()
+
+    # ------------------------------------------------------------------------------------------------------------------
+    # TESTS
+    # ------------------------------------------------------------------------------------------------------------------
+
+    def test_view_initialized_with_empty_line_edit(self):
+        self.assertEqual(self.view.get_file_edit_text(), "No data loaded")
+
+    def test_reset_text_to_cache_resets_correctly(self):
+        text = "C:\dir1\dir2\EMU00012345.nxs;C:\dir1\dir2\EMU00012345.nxs"
+        self.view.set_file_edit(text)
+        # User may then overwrite the text in the LineEdit, causing a signal to be sent
+        # and the corresponding slot should implement reset_edit_to_cached_value()
+        self.view.reset_edit_to_cached_value()
+        self.assertEqual(self.view.get_file_edit_text(), text)
+
+    def test_text_clears_from_line_edit_correctly(self):
+        text = "C:\dir1\dir2\EMU00012345.nxs;C:\dir1\dir2\EMU00012345.nxs"
+        self.view.set_file_edit(text)
+        self.view.clear()
+        self.assertEqual(self.view.get_file_edit_text(), "No data loaded")
+        self.view.reset_edit_to_cached_value()
+        self.assertEqual(self.view.get_file_edit_text(), "No data loaded")
+
+    def test_text_stored_correctly_when_not_visible_in_line_edit(self):
+        # This feature is currently unused
+        text = "C:\dir1\dir2\EMU00012345.nxs;C:\dir1\dir2\EMU00012345.nxs"
+        self.view.set_file_edit(text, store=True)
+        self.assertEqual(self.view.get_file_edit_text(), text)
+        self.view.reset_edit_to_cached_value()
+        self.assertEqual(self.view.get_file_edit_text(), text)
+
+
+if __name__ == '__main__':
+    unittest.main(buffer=False, verbosity=2)