diff --git a/Framework/Algorithms/CMakeLists.txt b/Framework/Algorithms/CMakeLists.txt
index d513840a134f95c5ba095507bba5e1b684539c14..8bff60ceadf780e307ab42c681c8d8abd75b1439 100644
--- a/Framework/Algorithms/CMakeLists.txt
+++ b/Framework/Algorithms/CMakeLists.txt
@@ -288,6 +288,7 @@ set ( SRC_FILES
 	src/SofQWCentre.cpp
 	src/SofQWNormalisedPolygon.cpp
 	src/SofQWPolygon.cpp
+	src/SofTwoThetaTOF.cpp
 	src/SolidAngle.cpp
 	src/SortEvents.cpp
 	src/SortXAxis.cpp
@@ -628,6 +629,7 @@ set ( INC_FILES
 	inc/MantidAlgorithms/SofQWCentre.h
 	inc/MantidAlgorithms/SofQWNormalisedPolygon.h
 	inc/MantidAlgorithms/SofQWPolygon.h
+	inc/MantidAlgorithms/SofTwoThetaTOF.h
 	inc/MantidAlgorithms/SolidAngle.h
 	inc/MantidAlgorithms/SortEvents.h
 	inc/MantidAlgorithms/SortXAxis.h
@@ -957,6 +959,7 @@ set ( TEST_FILES
 	SofQWNormalisedPolygonTest.h
 	SofQWPolygonTest.h
 	SofQWTest.h
+	SofTwoThetaTOFTest.h
 	SolidAngleTest.h
 	SortEventsTest.h
 	SortXAxisTest.h
diff --git a/Framework/Algorithms/inc/MantidAlgorithms/SofTwoThetaTOF.h b/Framework/Algorithms/inc/MantidAlgorithms/SofTwoThetaTOF.h
new file mode 100644
index 0000000000000000000000000000000000000000..5fc799e910a5e1fac23a87dd514fe198f5a76b3b
--- /dev/null
+++ b/Framework/Algorithms/inc/MantidAlgorithms/SofTwoThetaTOF.h
@@ -0,0 +1,43 @@
+// Mantid Repository : https://github.com/mantidproject/mantid
+//
+// Copyright © 2018 ISIS Rutherford Appleton Laboratory UKRI,
+//     NScD Oak Ridge National Laboratory, European Spallation Source
+//     & Institut Laue - Langevin
+// SPDX - License - Identifier: GPL - 3.0 +
+#ifndef MANTID_ALGORITHMS_SOFTWOTHETATOF_H_
+#define MANTID_ALGORITHMS_SOFTWOTHETATOF_H_
+
+#include "MantidAPI/Algorithm.h"
+#include "MantidAlgorithms/DllConfig.h"
+
+namespace Mantid {
+namespace Algorithms {
+
+/** SofTwoThetaTOF : Convert a S(spectrum number, TOF) workspace to
+ * S(twoTheta, TOF) workspace.
+ */
+class MANTID_ALGORITHMS_DLL SofTwoThetaTOF : public API::Algorithm {
+public:
+  const std::string name() const override;
+  int version() const override;
+  const std::string category() const override;
+  const std::string summary() const override;
+
+private:
+  void init() override;
+  void exec() override;
+  double clarifyAngleStep(API::MatrixWorkspace const &ws);
+  API::MatrixWorkspace_sptr convertToConstantL2(API::MatrixWorkspace_sptr &ws);
+  API::MatrixWorkspace_sptr convertToTwoTheta(API::MatrixWorkspace_sptr &ws);
+  API::MatrixWorkspace_sptr groupByTwoTheta(API::MatrixWorkspace_sptr &ws,
+                                            double const twoThetaStep);
+  API::MatrixWorkspace_sptr
+  maskEmptyBins(API::MatrixWorkspace_sptr &maskable,
+                API::MatrixWorkspace_sptr &comparison);
+  API::MatrixWorkspace_sptr rebinToNonRagged(API::MatrixWorkspace_sptr &ws);
+};
+
+} // namespace Algorithms
+} // namespace Mantid
+
+#endif /* MANTID_ALGORITHMS_SOFTWOTHETATOF_H_ */
diff --git a/Framework/Algorithms/src/ConvertToConstantL2.cpp b/Framework/Algorithms/src/ConvertToConstantL2.cpp
index 47f37b4d0c749e6f327438e9e352a6b19a5ab138..6817490d4a212da87742a5d9491b928758aa1942 100644
--- a/Framework/Algorithms/src/ConvertToConstantL2.cpp
+++ b/Framework/Algorithms/src/ConvertToConstantL2.cpp
@@ -148,7 +148,8 @@ double ConvertToConstantL2::getRunProperty(std::string s) {
   Mantid::Kernel::Property *prop = run.getProperty(s);
   double val;
   if (!Strings::convert(prop->value(), val)) {
-    const std::string mesg = "Cannot convert sample log '" + s + "' to a number.";
+    const std::string mesg =
+        "Cannot convert sample log '" + s + "' to a number.";
     throw std::runtime_error(mesg);
   }
   return val;
diff --git a/Framework/Algorithms/src/SofTwoThetaTOF.cpp b/Framework/Algorithms/src/SofTwoThetaTOF.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..70fd77250bb6ef3ae29fca57b144cb2937c57ee1
--- /dev/null
+++ b/Framework/Algorithms/src/SofTwoThetaTOF.cpp
@@ -0,0 +1,249 @@
+// Mantid Repository : https://github.com/mantidproject/mantid
+//
+// Copyright © 2018 ISIS Rutherford Appleton Laboratory UKRI,
+//     NScD Oak Ridge National Laboratory, European Spallation Source
+//     & Institut Laue - Langevin
+// SPDX - License - Identifier: GPL - 3.0 +
+#include "MantidAlgorithms/SofTwoThetaTOF.h"
+
+#include "MantidAPI/FileProperty.h"
+#include "MantidAPI/HistogramValidator.h"
+#include "MantidAPI/InstrumentValidator.h"
+#include "MantidAPI/WorkspaceUnitValidator.h"
+#include "MantidGeometry/Instrument.h"
+#include "MantidGeometry/Instrument/ParameterMap.h"
+#include "MantidKernel/BoundedValidator.h"
+#include "MantidKernel/CompositeValidator.h"
+
+#include "boost/filesystem.hpp"
+
+namespace {
+/// Constants for the algorithm's property names.
+namespace Prop {
+std::string const ANGLE_STEP{"AngleStep"};
+std::string const FILENAME{"GroupingFilename"};
+std::string const INPUT_WS{"InputWorkspace"};
+std::string const OUTPUT_WS{"OutputWorkspace"};
+} // namespace Prop
+
+/// A temporary file resource lifetime manager
+struct RemoveFileAtScopeExit {
+  std::string name;
+  ~RemoveFileAtScopeExit() {
+    if (!name.empty()) {
+      auto const path = boost::filesystem::path(name);
+      if (boost::filesystem::exists(path)) {
+        boost::filesystem::remove(path);
+      }
+    }
+  }
+};
+
+/// A holder of minimum and maximum values.
+struct MinMax {
+  double min{std::numeric_limits<double>::max()};
+  double max{std::numeric_limits<double>::lowest()};
+};
+
+/**
+ * Return the minimum and maximum X over all histograms
+ * @param ws a workspace
+ * @return the minimum and maximum X
+ */
+MinMax minMaxX(Mantid::API::MatrixWorkspace const &ws) {
+  auto const nHisto = ws.getNumberHistograms();
+  MinMax mm;
+  for (size_t i = 0; i < nHisto; ++i) {
+    auto const &x = ws.x(i);
+    mm.min = std::min(mm.min, x.front());
+    mm.max = std::max(mm.max, x.back());
+  }
+  return mm;
+}
+
+/**
+ * Return the width of the first bin in the first histogram.
+ * @param ws a workspace
+ * @return the bin width
+ */
+double binWidth(Mantid::API::MatrixWorkspace const &ws) {
+  auto const &x = ws.x(0);
+  return x[1] - x[0];
+}
+
+/**
+ * Append .xml to filename if needed.
+ * @param filename a filename
+ * @return a suitable name for an XML file
+ */
+std::string ensureXMLExtension(std::string const &filename) {
+  std::string name = filename;
+  auto const path = boost::filesystem::path(filename);
+  if (path.extension() != ".xml") {
+    name += ".xml";
+  }
+  return name;
+}
+} // namespace
+
+namespace Mantid {
+namespace Algorithms {
+
+// Register the algorithm into the AlgorithmFactory
+DECLARE_ALGORITHM(SofTwoThetaTOF)
+
+/// Algorithms name for identification. @see Algorithm::name
+const std::string SofTwoThetaTOF::name() const { return "SofTwoThetaTOF"; }
+
+/// Algorithm's version for identification. @see Algorithm::version
+int SofTwoThetaTOF::version() const { return 1; }
+
+/// Algorithm's category for identification. @see Algorithm::category
+const std::string SofTwoThetaTOF::category() const {
+  return "Inelastic;ILL\\Direct";
+}
+
+/// Algorithm's summary for use in the GUI and help. @see Algorithm::summary
+const std::string SofTwoThetaTOF::summary() const {
+  return "Calculates the intensity as a function of scattering angle and time "
+         "of flight.";
+}
+
+/** Initialize the algorithm's properties.
+ */
+void SofTwoThetaTOF::init() {
+  auto histogrammedTOF = boost::make_shared<Kernel::CompositeValidator>();
+  histogrammedTOF->add(boost::make_shared<API::WorkspaceUnitValidator>("TOF"));
+  histogrammedTOF->add(boost::make_shared<API::HistogramValidator>());
+  histogrammedTOF->add(boost::make_shared<API::InstrumentValidator>());
+  declareProperty(
+      Kernel::make_unique<API::WorkspaceProperty<>>(
+          Prop::INPUT_WS, "", Kernel::Direction::Input, histogrammedTOF),
+      "A workspace to be converted.");
+  declareProperty(Kernel::make_unique<API::WorkspaceProperty<>>(
+                      Prop::OUTPUT_WS, "", Kernel::Direction::Output),
+                  "A workspace with (2theta, TOF) units.");
+  auto positiveDouble = boost::make_shared<Kernel::BoundedValidator<double>>();
+  positiveDouble->setLowerExclusive(0.);
+  declareProperty(Prop::ANGLE_STEP, EMPTY_DBL(), positiveDouble,
+                  "The angle step for detector grouping, in degrees.");
+  declareProperty(Kernel::make_unique<API::FileProperty>(
+                      Prop::FILENAME, "", API::FileProperty::OptionalSave,
+                      std::vector<std::string>{".xml"}),
+                  "A grouping file that will be created; a corresponding .par "
+                  "file wille be created as well.");
+}
+
+/** Execute the algorithm.
+ */
+void SofTwoThetaTOF::exec() {
+  API::MatrixWorkspace_sptr in = getProperty(Prop::INPUT_WS);
+  auto ragged = convertToConstantL2(in);
+  auto equalBinning = rebinToNonRagged(ragged);
+  auto ws = maskEmptyBins(equalBinning, ragged);
+  ws = groupByTwoTheta(ws, clarifyAngleStep(*in));
+  ws = convertToTwoTheta(ws);
+  setProperty(Prop::OUTPUT_WS, ws);
+}
+
+double SofTwoThetaTOF::clarifyAngleStep(API::MatrixWorkspace const &ws) {
+  if (!isDefault(Prop::ANGLE_STEP)) {
+    return getProperty(Prop::ANGLE_STEP);
+  } else {
+    auto instrument = ws.getInstrument();
+    if (!instrument) {
+      throw std::invalid_argument("Missing " + Prop::ANGLE_STEP);
+    }
+    auto const &paramMap = ws.constInstrumentParameters();
+    auto const paramValues =
+        paramMap.getDouble(instrument->getName(), "natural-angle-step");
+    if (paramValues.empty()) {
+      throw std::invalid_argument("Missing " + Prop::ANGLE_STEP +
+                                  " or 'natural-angle-step' not defined in "
+                                  "instrument parameters file.");
+    }
+    return paramValues.front();
+  }
+}
+
+API::MatrixWorkspace_sptr
+SofTwoThetaTOF::convertToConstantL2(API::MatrixWorkspace_sptr &ws) {
+  auto toConstantL2 = createChildAlgorithm("ConvertToConstantL2", 0., 0.2);
+  toConstantL2->setProperty("InputWorkspace", ws);
+  toConstantL2->setPropertyValue("OutputWorkspace", "out");
+  toConstantL2->execute();
+  return toConstantL2->getProperty("OutputWorkspace");
+}
+
+API::MatrixWorkspace_sptr
+SofTwoThetaTOF::convertToTwoTheta(API::MatrixWorkspace_sptr &ws) {
+  auto convertAxis = createChildAlgorithm("ConvertSpectrumAxis", 0.9, 1.0);
+  convertAxis->setProperty("InputWorkspace", ws);
+  convertAxis->setProperty("OutputWorkspace", "out");
+  convertAxis->setProperty("Target", "Theta");
+  convertAxis->execute();
+  return convertAxis->getProperty("OutputWorkspace");
+}
+
+API::MatrixWorkspace_sptr
+SofTwoThetaTOF::groupByTwoTheta(API::MatrixWorkspace_sptr &ws,
+                                double const twoThetaStep) {
+  auto generateGrouping =
+      createChildAlgorithm("GenerateGroupingPowder", 0.2, 0.5);
+  generateGrouping->setProperty("InputWorkspace", ws);
+  generateGrouping->setProperty("AngleStep", twoThetaStep);
+  std::string filename;
+  RemoveFileAtScopeExit deleteThisLater;
+  if (isDefault(Prop::FILENAME)) {
+    auto tempPath = boost::filesystem::temp_directory_path();
+    tempPath /= boost::filesystem::unique_path(
+        "detector-grouping-%%%%-%%%%-%%%%-%%%%.xml");
+    filename = tempPath.c_str();
+    generateGrouping->setProperty("GenerateParFile", false);
+    // Make sure the file gets deleted at scope exit.
+    deleteThisLater.name = filename;
+  } else {
+    filename = static_cast<std::string>(getProperty(Prop::FILENAME));
+    filename = ensureXMLExtension(filename);
+  }
+  generateGrouping->setProperty("GroupingFilename", filename);
+  generateGrouping->execute();
+  auto groupDetectors = createChildAlgorithm("GroupDetectors", 0.7, 0.9);
+  groupDetectors->setProperty("InputWorkspace", ws);
+  groupDetectors->setProperty("OutputWorkspace", "out");
+  groupDetectors->setProperty("MapFile", filename);
+  groupDetectors->setProperty("Behaviour", "Average");
+  groupDetectors->execute();
+  return groupDetectors->getProperty("OutputWorkspace");
+}
+
+API::MatrixWorkspace_sptr
+SofTwoThetaTOF::maskEmptyBins(API::MatrixWorkspace_sptr &maskable,
+                              API::MatrixWorkspace_sptr &comparison) {
+  auto maskNonOverlapping =
+      createChildAlgorithm("MaskNonOverlappingBins", 0.6, 0.7);
+  maskNonOverlapping->setProperty("InputWorkspace", maskable);
+  maskNonOverlapping->setProperty("OutputWorkspace", "out");
+  maskNonOverlapping->setProperty("ComparisonWorkspace", comparison);
+  maskNonOverlapping->setProperty("MaskPartiallyOverlapping", true);
+  maskNonOverlapping->setProperty("RaggedInputs", "Ragged");
+  maskNonOverlapping->setProperty("CheckSortedX", false);
+  maskNonOverlapping->execute();
+  return maskNonOverlapping->getProperty("OutputWorkspace");
+}
+
+API::MatrixWorkspace_sptr
+SofTwoThetaTOF::rebinToNonRagged(API::MatrixWorkspace_sptr &ws) {
+  auto const xRange = minMaxX(*ws);
+  std::vector<double> const rebinParams{xRange.min, binWidth(*ws), xRange.max};
+  auto rebin = createChildAlgorithm("Rebin", 0.5, 0.6);
+  rebin->setProperty("InputWorkspace", ws);
+  rebin->setProperty("OutputWorkspace", "out");
+  rebin->setProperty("Params", rebinParams);
+  rebin->setProperty("FullBinsOnly", true);
+  rebin->execute();
+  return rebin->getProperty("OutputWorkspace");
+}
+
+} // namespace Algorithms
+} // namespace Mantid
diff --git a/Framework/Algorithms/test/SofTwoThetaTOFTest.h b/Framework/Algorithms/test/SofTwoThetaTOFTest.h
new file mode 100644
index 0000000000000000000000000000000000000000..8ae43575d0e1561493e7b148acadfabad5071869
--- /dev/null
+++ b/Framework/Algorithms/test/SofTwoThetaTOFTest.h
@@ -0,0 +1,164 @@
+// 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 MANTID_ALGORITHMS_SOFTWOTHETATOFTEST_H_
+#define MANTID_ALGORITHMS_SOFTWOTHETATOFTEST_H_
+
+#include <cxxtest/TestSuite.h>
+
+#include "MantidAPI/Axis.h"
+#include "MantidAPI/SpectrumInfo.h"
+#include "MantidAlgorithms/SofTwoThetaTOF.h"
+#include "MantidGeometry/Crystal/AngleUnits.h"
+#include "MantidGeometry/Instrument.h"
+#include "MantidTestHelpers/WorkspaceCreationHelper.h"
+
+#include <boost/filesystem.hpp>
+
+using namespace Mantid;
+using Mantid::Geometry::deg2rad;
+using Mantid::Geometry::rad2deg;
+
+class SofTwoThetaTOFTest : 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 SofTwoThetaTOFTest *createSuite() { return new SofTwoThetaTOFTest(); }
+  static void destroySuite(SofTwoThetaTOFTest *suite) { delete suite; }
+
+  void test_init() {
+    Algorithms::SofTwoThetaTOF alg;
+    TS_ASSERT_THROWS_NOTHING(alg.initialize())
+    TS_ASSERT(alg.isInitialized())
+  }
+
+  void test_twoThetaGrouping() {
+    constexpr int numBanks{1};
+    constexpr int bankSize{6};
+    constexpr int numBins{13};
+    constexpr double angleStep{0.1};
+    API::MatrixWorkspace_sptr inputWS =
+        WorkspaceCreationHelper::create2DWorkspaceWithRectangularInstrument(
+            numBanks, bankSize, numBins);
+    inputWS->getAxis(0)->setUnit("TOF");
+    inputWS->mutableRun().addProperty("wavelength", 1.0);
+    auto &paramMap = inputWS->instrumentParameters();
+    paramMap.addString(inputWS->getInstrument().get(), "l2",
+                       std::to_string(5.));
+    Algorithms::SofTwoThetaTOF alg;
+    alg.setChild(true);
+    alg.setRethrows(true);
+    TS_ASSERT_THROWS_NOTHING(alg.initialize())
+    TS_ASSERT_THROWS_NOTHING(alg.setProperty("InputWorkspace", inputWS));
+    TS_ASSERT_THROWS_NOTHING(
+        alg.setProperty("OutputWorkspace", "_unused_for_child"));
+    TS_ASSERT_THROWS_NOTHING(alg.setProperty("AngleStep", angleStep))
+    TS_ASSERT_THROWS_NOTHING(alg.execute())
+    TS_ASSERT(alg.isExecuted())
+    API::MatrixWorkspace_const_sptr outputWS =
+        alg.getProperty("OutputWorkspace");
+    TS_ASSERT(outputWS)
+    auto const &spectrumInfo = outputWS->spectrumInfo();
+    auto const nHist = spectrumInfo.size();
+    TS_ASSERT_LESS_THAN(1, nHist)
+    auto angleBinEdge =
+        std::floor(spectrumInfo.twoTheta(0) / (angleStep * deg2rad));
+    for (size_t i = 0; i < nHist; ++i) {
+      auto const twoTheta = spectrumInfo.twoTheta(i);
+      TS_ASSERT_LESS_THAN(angleBinEdge, twoTheta)
+      angleBinEdge += angleStep * deg2rad;
+      TS_ASSERT_LESS_THAN(twoTheta, angleBinEdge)
+    }
+  }
+
+  void test_grouping_file_and_par_file_creation() {
+    constexpr int numBanks{1};
+    constexpr int bankSize{6};
+    constexpr int numBins{13};
+    constexpr double angleStep{0.1};
+    API::MatrixWorkspace_sptr inputWS =
+        WorkspaceCreationHelper::create2DWorkspaceWithRectangularInstrument(
+            numBanks, bankSize, numBins);
+    inputWS->getAxis(0)->setUnit("TOF");
+    inputWS->mutableRun().addProperty("wavelength", 1.0);
+    auto &paramMap = inputWS->instrumentParameters();
+    paramMap.addString(inputWS->getInstrument().get(), "l2",
+                       std::to_string(5.));
+    Algorithms::SofTwoThetaTOF alg;
+    alg.setChild(true);
+    alg.setRethrows(true);
+    TS_ASSERT_THROWS_NOTHING(alg.initialize())
+    TS_ASSERT_THROWS_NOTHING(alg.setProperty("InputWorkspace", inputWS));
+    TS_ASSERT_THROWS_NOTHING(
+        alg.setProperty("OutputWorkspace", "_unused_for_child"));
+    TS_ASSERT_THROWS_NOTHING(alg.setProperty("AngleStep", angleStep))
+    auto tempXml = boost::filesystem::temp_directory_path();
+    tempXml /= boost::filesystem::unique_path("SofTwoThetaTest-%%%%%%%%.xml");
+    TS_ASSERT_THROWS_NOTHING(
+        alg.setProperty("GroupingFilename", tempXml.c_str()))
+    TS_ASSERT_THROWS_NOTHING(alg.execute())
+    TS_ASSERT(alg.isExecuted())
+    auto const xmlExists = boost::filesystem::exists(tempXml);
+    TS_ASSERT(xmlExists)
+    if (xmlExists) {
+      boost::filesystem::remove(tempXml);
+    }
+    auto tempPar = tempXml;
+    tempPar.replace_extension(".par");
+    auto const parExists = boost::filesystem::exists(tempPar);
+    TS_ASSERT(parExists)
+    if (parExists) {
+      boost::filesystem::remove(tempPar);
+    }
+  }
+
+  void test_averaging() {
+    constexpr int numBanks{1};
+    constexpr int bankSize{6};
+    constexpr int numBins{13};
+    constexpr double angleStep{0.1};
+    API::MatrixWorkspace_sptr inputWS =
+        WorkspaceCreationHelper::create2DWorkspaceWithRectangularInstrument(
+            numBanks, bankSize, numBins);
+    inputWS->getAxis(0)->setUnit("TOF");
+    inputWS->mutableRun().addProperty("wavelength", 1.0);
+    auto &paramMap = inputWS->instrumentParameters();
+    paramMap.addString(inputWS->getInstrument().get(), "l2",
+                       std::to_string(5.));
+    Algorithms::SofTwoThetaTOF alg;
+    alg.setChild(true);
+    alg.setRethrows(true);
+    TS_ASSERT_THROWS_NOTHING(alg.initialize())
+    TS_ASSERT_THROWS_NOTHING(alg.setProperty("InputWorkspace", inputWS));
+    TS_ASSERT_THROWS_NOTHING(
+        alg.setProperty("OutputWorkspace", "_unused_for_child"));
+    TS_ASSERT_THROWS_NOTHING(alg.setProperty("AngleStep", angleStep))
+    TS_ASSERT_THROWS_NOTHING(alg.execute())
+    TS_ASSERT(alg.isExecuted())
+    API::MatrixWorkspace_const_sptr outputWS =
+        alg.getProperty("OutputWorkspace");
+    TS_ASSERT(outputWS)
+    for (size_t i = 0; i < outputWS->getNumberHistograms(); ++i) {
+      auto const &Ys = outputWS->y(i);
+      auto const &Es = outputWS->e(i);
+      for (size_t j = 0; j < Ys.size(); ++j) {
+        if (Ys[j] == 0.)
+          std::cout << "i: " << i << " j: " << j << '\n';
+        if (j == 0 && i != 6) {
+          // These bins are known to be zero.
+          TS_ASSERT_EQUALS(Ys[j], 0.)
+          TS_ASSERT_EQUALS(Es[j], 0.)
+        } else {
+          TS_ASSERT_DELTA(Ys[j], 2., 1e-12)
+          TS_ASSERT_LESS_THAN(0., Es[j])
+          TS_ASSERT_LESS_THAN_EQUALS(Es[j], std::sqrt(2))
+        }
+      }
+    }
+  }
+};
+
+#endif /* MANTID_ALGORITHMS_SOFTWOTHETATOFTEST_H_ */
diff --git a/Framework/DataHandling/test/GenerateGroupingPowderTest.h b/Framework/DataHandling/test/GenerateGroupingPowderTest.h
index 343cc491cceb69fe68e61c78cbf78a00388eb0f2..7521e0a1c4c0266fe40444f42eb49e505d7037bf 100644
--- a/Framework/DataHandling/test/GenerateGroupingPowderTest.h
+++ b/Framework/DataHandling/test/GenerateGroupingPowderTest.h
@@ -154,6 +154,7 @@ public:
       boost::filesystem::remove(parFile);
     }
   }
+
 private:
   std::string parFilename(const std::string &xmlFilename) {
     std::string parFile = xmlFilename;
diff --git a/Framework/TestHelpers/src/WorkspaceCreationHelper.cpp b/Framework/TestHelpers/src/WorkspaceCreationHelper.cpp
index b0e962e69aed9ed295f4cd10c4a0049e13d8bb4d..7e297083364ed7cc5fad74643efd540073053fab 100644
--- a/Framework/TestHelpers/src/WorkspaceCreationHelper.cpp
+++ b/Framework/TestHelpers/src/WorkspaceCreationHelper.cpp
@@ -443,7 +443,7 @@ MatrixWorkspace_sptr create2DDetectorScanWorkspaceWithFullInstrument(
  * @param numBanks :: number of rectangular banks
  * @param numPixels :: each bank will be numPixels*numPixels
  * @param numBins :: each spectrum will have this # of bins
- * @return The EventWorkspace
+ * @return The Workspace2D
  */
 Mantid::DataObjects::Workspace2D_sptr
 create2DWorkspaceWithRectangularInstrument(int numBanks, int numPixels,
diff --git a/docs/source/algorithms/SofTwoThetaTOF-v1.rst b/docs/source/algorithms/SofTwoThetaTOF-v1.rst
new file mode 100644
index 0000000000000000000000000000000000000000..1736d0940b66ce7200c934eaa96d399cdbf1952c
--- /dev/null
+++ b/docs/source/algorithms/SofTwoThetaTOF-v1.rst
@@ -0,0 +1,53 @@
+
+.. algorithm::
+
+.. summary::
+
+.. relatedalgorithms::
+
+.. properties::
+
+Description
+-----------
+
+This algorithm converts a workspace from (spectrum number, TOF) coordinates to (:math:`2\theta`, TOF). The latter view is useful for visualization and diagnostics purposes of direct geometry spectrometers with large PSD arrays. The algorithm performs the following steps:
+
+#. Equalize the sample-to-detector distances using :ref:`ConvertToConstantL2 <algm-ConvertToConstantL2>`
+#. :ref:`Rebin <algm-Rebin>` the ragged workspace to same binning over all histograms. The bin width is taken from the first bin of the first histogram. This will introduce zero padding to the histograms.
+#. Mask the zero padding using :ref:`MaskNonOverlappingBins <algm-MaskNonOverlappingBins>`.
+#. Group the detectors to given :math:`2\theta` bins using :ref:`GenerateGroupingPowder <algm-GenerateGroupingPowder>` and :ref:`GroupDetectors <algm-GroupDetectors>`.
+#. Convert the spectrum axis from spectrum numbers to :math:`2\theta` using :ref:`ConvertSpectrumAxis <algm-ConvertSpectrumAxis>`
+
+A default ``AngleStep`` can be defined in the :ref:`instrument parameters <InstrumentParameterFile>`. The parameter is called ``natural-angle-step``; its type should be ``float`` and value the desired ``AngleStep`` in degrees.
+
+The instrument should have an ``l2`` parameter defined which is the nominal sample to detector distance. Also, sample logs should include a ``wavelength`` entry (in Angstroms). See :ref:`ConvertToConstantL2 <algm-ConvertToConstantL2>` for more details.
+
+By default, the ``.xml`` and ``.par`` files generated by :ref:`GenerateGroupingPowder <algm-GenerateGroupingPowder>` are stored in a temporary location and deleted when the algorithm finishes. If these files are needed afterwards, a name for the ``.xml`` file can be given by ``GroupingFilename``. The ``.par`` file will have the same name except for the file extension which is changed from ``.xml`` to ``.par``. Note that this algorithm forces the ``.xml`` extension to ``GroupingFilename``.
+
+
+Usage
+-----
+
+**Example - Applying SofTwoThetaTOF and plotting the result**
+
+.. plot::
+   :include-source:
+
+   from mantid.simpleapi import *
+   import directtools as dt
+   ws = CreateSampleWorkspace(Function='Quasielastic Tunnelling', NumBanks=1, BankPixelWidth=20)
+   AddSampleLog(ws, 'wavelength', '6.26', 'Number', 'Angstrom')
+   SetInstrumentParameter(ws, ParameterName='l2', ParameterType='String', Value='5')
+   SofTT = SofTwoThetaTOF(ws, AngleStep=1)
+   fig, axes = dt.subplots()
+   axes.pcolor(SofTT)
+   axes.set_xlim(0.)
+   # Uncomment the line below to show the plot.
+   #fig.show()
+   mtd.clear()
+
+
+.. categories::
+
+.. sourcelink::
+
diff --git a/docs/source/release/v3.14.0/direct_inelastic.rst b/docs/source/release/v3.14.0/direct_inelastic.rst
index fb474c020a68c13f6f8f06f1d23de59d7b9d0363..4203e18ee381db05201d1b69f96111f839bd6e90 100644
--- a/docs/source/release/v3.14.0/direct_inelastic.rst
+++ b/docs/source/release/v3.14.0/direct_inelastic.rst
@@ -17,6 +17,7 @@ New Algorithms
 ##############
 
 - Added a new algorithm to ILL's reduction workflow: :ref:`DirectILLTubeBackground <algm-DirectILLTubeBackground>` which can be used to calculate the time-independent backgrounds for instruments with PSD detectors such as IN5.
+- The new algorithm :ref:`SofTwoThetaTOF <algm-SofTwoThetaTOF>` can be used to convert a workspace from (spectrum number, TOF) units to (:math:`2\theta`, TOF) averaging the intensities over constant scattering angles.
 
 Improvements
 ############
diff --git a/instrument/IN5_Parameters.xml b/instrument/IN5_Parameters.xml
index eb1d90293afe47ba3adf4933a011f6e8d8a20252..4eb0ad4c8519fe3b9920d54de22e0289ed4f1ce0 100644
--- a/instrument/IN5_Parameters.xml
+++ b/instrument/IN5_Parameters.xml
@@ -12,7 +12,9 @@
 		<parameter name="l2" type="string">
 			<value val="4.0" />
 		</parameter>
-
+		<parameter name="natural-angle-step" type="float">
+			<value val="0.3724" />
+		</parameter>
 		<!-- formula for Detector efficiency calculation. Algorithm: DetectorEfficiencyCorUser
 			See http://muparser.sourceforge.net/mup_features.html#idDef2 for available
 			operators -->