Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
CalculateFlatBackground.cpp 18.67 KiB
#include "MantidAlgorithms/CalculateFlatBackground.h"
#include "MantidAPI/FunctionFactory.h"
#include "MantidAPI/MatrixWorkspace.h"
#include "MantidAPI/IFunction.h"
#include "MantidAPI/SpectrumInfo.h"
#include "MantidAPI/WorkspaceFactory.h"
#include "MantidAPI/WorkspaceOpOverloads.h"
#include "MantidDataObjects/TableWorkspace.h"
#include "MantidGeometry/IDetector.h"
#include "MantidKernel/ArrayProperty.h"
#include "MantidKernel/EnabledWhenProperty.h"
#include "MantidKernel/ListValidator.h"
#include "MantidKernel/VectorHelper.h"
#include "MantidHistogramData/Histogram.h"
#include <algorithm>
#include <boost/lexical_cast.hpp>
#include <climits>
#include <numeric>

namespace Mantid {
namespace Algorithms {

// Register the algorithm into the AlgorithmFactory
DECLARE_ALGORITHM(CalculateFlatBackground)

using namespace Kernel;
using namespace API;

/// Enumeration for the different operating modes.
enum class Modes { LINEAR_FIT, MEAN, MOVING_AVERAGE };

void CalculateFlatBackground::init() {
  declareProperty(
      make_unique<WorkspaceProperty<>>("InputWorkspace", "", Direction::Input),
      "The input workspace must either have constant width bins or is a "
      "distribution\n"
      "workspace. It is also assumed that all spectra have the same X bin "
      "boundaries");
  declareProperty(make_unique<WorkspaceProperty<>>("OutputWorkspace", "",
                                                   Direction::Output),
                  "Name to use for the output workspace.");
  declareProperty("StartX", Mantid::EMPTY_DBL(),
                  "The X value at which to start the background fit. Mandatory "
                  "for the Linear Fit and Mean modes, ignored by Moving "
                  "Average.");
  setPropertySettings("StartX", make_unique<EnabledWhenProperty>(
                                    "Mode", IS_NOT_EQUAL_TO, "Moving Average"));
  declareProperty("EndX", Mantid::EMPTY_DBL(),
                  "The X value at which to end the background fit. Mandatory "
                  "for the Linear Fit and Mean modes, ignored by Moving "
                  "Average.");
  setPropertySettings("EndX", make_unique<EnabledWhenProperty>(
                                  "Mode", IS_NOT_EQUAL_TO, "Moving Average"));
  declareProperty(
      make_unique<ArrayProperty<int>>("WorkspaceIndexList"),
      "Indices of the spectra that will have their background removed\n"
      "default: modify all spectra");
  std::vector<std::string> modeOptions{"Linear Fit", "Mean", "Moving Average"};
  declareProperty("Mode", "Linear Fit",
                  boost::make_shared<StringListValidator>(modeOptions),
                  "The background count rate is estimated either by taking a "
                  "mean, doing a linear fit, or taking the\n"
                  "minimum of a moving average (default: Linear Fit)");
  // Property to determine whether we subtract the background or just return the
  // background.
  std::vector<std::string> outputOptions{"Subtract Background",
                                         "Return Background"};
  declareProperty("OutputMode", "Subtract Background",
                  boost::make_shared<StringListValidator>(outputOptions),
                  "Once the background has been determined it can either be "
                  "subtracted from \n"
                  "the InputWorkspace and returned or just returned (default: "
                  "Subtract Background)");
  declareProperty("SkipMonitors", false,
                  "By default, the algorithm calculates and removes background "
                  "from monitors in the same way as from normal detectors\n"
                  "If this property is set to true, background is not "
                  "calculated/removed from monitors.",
                  Direction::Input);
  declareProperty("NullifyNegativeValues", true,
                  "When background is subtracted, signals in some time "
                  "channels may become negative.\n"
                  "If this option is true, signal in such bins is nullified "
                  "and the module of the removed signal"
                  "is added to the error. If false, the signal and errors are "
                  "left unchanged",
                  Direction::Input);
  declareProperty("AveragingWindowWidth", Mantid::EMPTY_INT(),
                  "The width of the moving average window in bins. Mandatory "
                  "for the Moving Average mode.");
  setPropertySettings(
      "AveragingWindowWidth",
      make_unique<EnabledWhenProperty>("Mode", IS_EQUAL_TO, "Moving Average"));
}

void CalculateFlatBackground::exec() {
  // Retrieve the input workspace
  API::MatrixWorkspace_const_sptr inputWS = getProperty("InputWorkspace");

  // Copy over all the data
  const size_t numHists = inputWS->getNumberHistograms();
  const size_t blocksize = inputWS->blocksize();

  if (blocksize == 1)
    throw std::runtime_error("CalculateFlatBackground with only one bin is "
                             "not possible.");

  const bool skipMonitors = getProperty("SkipMonitors");
  const bool nullifyNegative = getProperty("NullifyNegativeValues");
  const std::string modeString = getProperty("Mode");
  Modes mode = Modes::LINEAR_FIT;
  if (modeString == "Mean") {
    mode = Modes::MEAN;
  } else if (modeString == "Moving Average") {
    mode = Modes::MOVING_AVERAGE;
  }
  double startX, endX;
  int windowWidth = 0;
  switch (mode) {
  case Modes::LINEAR_FIT:
  case Modes::MEAN:
    if (getPointerToProperty("StartX")->isDefault()) {
      throw std::runtime_error("StartX property not set to any value");
    }
    if (getPointerToProperty("EndX")->isDefault()) {
      throw std::runtime_error("EndX property not set to any value");
    }
    // Get the required X range
    checkRange(startX, endX);
    break;
  case Modes::MOVING_AVERAGE:
    if (getPointerToProperty("AveragingWindowWidth")->isDefault()) {
      throw std::runtime_error(
          "AveragingWindowWidth property not set to any value");
    }
    windowWidth = getProperty("AveragingWindowWidth");
    if (windowWidth <= 0) {
      throw std::runtime_error("AveragingWindowWidth zero or negative");
    }
    if (blocksize < static_cast<size_t>(windowWidth)) {
      throw std::runtime_error("AveragingWindowWidth is larger than the number "
                               "of bins in InputWorkspace");
    }
    break;
  }

  std::vector<int> wsInds = getProperty("WorkspaceIndexList");
  // check if the user passed an empty list, if so all of spec will be processed
  if (wsInds.empty()) {
    wsInds.resize(numHists);
    std::iota(wsInds.begin(), wsInds.end(), 0);
  }

  // Are we removing the background?
  const bool removeBackground =
      std::string(getProperty("outputMode")) == "Subtract Background";

  // Initialize the progress reporting object
  m_progress = Kernel::make_unique<Progress>(this, 0.0, 0.2, numHists);

  MatrixWorkspace_sptr outputWS = getProperty("OutputWorkspace");
  // If input and output workspaces are not the same, create a new workspace for
  // the output
  if (outputWS != inputWS) {
    outputWS = WorkspaceFactory::Instance().create(inputWS);
  }

  // For logging purposes.
  double backgroundTotal = 0;
  size_t calculationCount = 0;

  PARALLEL_FOR_IF(Kernel::threadSafe(*inputWS, *outputWS))
  for (int64_t i = 0; i < static_cast<int64_t>(numHists); ++i) {
    PARALLEL_START_INTERUPT_REGION
    // Extract a histogram from inputWS, work on it and, finally, put it to
    // outputWS.
    auto histogram = inputWS->histogram(i);
    bool wasCounts = false;
    if (histogram.yMode() == HistogramData::Histogram::YMode::Counts) {
      wasCounts = true;
      histogram.convertToFrequencies();
    }
    bool skipCalculation =
        std::find(wsInds.cbegin(), wsInds.cend(), i) == wsInds.cend();
    if (!skipCalculation && skipMonitors) {
      const auto &spectrumInfo = inputWS->spectrumInfo();
      if (!spectrumInfo.hasDetectors(i)) {
        // Do nothing.
        // not every spectra is the monitor or detector, some spectra have no
        // instrument components attached.
        g_log.information(" Can not find detector for spectra N: " +
                          std::to_string(i) +
                          " Processing background anyway\n");
      } else if (spectrumInfo.isMonitor(i)) {
        skipCalculation = true;
      }
    }
    double background = 0;
    double variance = 0;
    if (!skipCalculation) {
      ++calculationCount;
      // Now call the function the user selected to calculate the background
      switch (mode) {
      case Modes::LINEAR_FIT:
        LinearFit(histogram, background, variance, startX, endX);
        break;
      case Modes::MEAN:
        Mean(histogram, background, variance, startX, endX);
        break;
      case Modes::MOVING_AVERAGE:
        MovingAverage(histogram, background, variance, windowWidth);
        break;
      }
    }
    if (background < 0) {
      g_log.debug() << "The background for spectra index " << i
                    << "was calculated to be " << background << '\n';
      g_log.warning() << "Problem with calculating the background number of "
                         "counts spectrum with index " << i << ".";
      if (removeBackground) {
        g_log.warning() << " The spectrum has been left unchanged.\n";
      } else {
        g_log.warning() << " The output background has been set to zero.\n";
      }
    } else {
      backgroundTotal += background;
    }
    HistogramData::Histogram outHistogram(histogram);
    auto &ys = outHistogram.mutableY();
    auto &es = outHistogram.mutableE();
    if (removeBackground) {
      // When subtracting backgrounds, act only if background is positive.
      if (background >= 0) {
        for (size_t j = 0; j < ys.size(); ++j) {
          double val = ys[j] - background;
          double err = std::sqrt(es[j] * es[j] + variance);
          if (nullifyNegative && (val < 0)) {
            val = 0;
            // The error estimate must go up in this nonideal situation and the
            // value of background is a good estimate for it. However, don't
            // reduce the error if it was already more than that
            err = es[j] > background ? es[j] : background;
          }
          ys[j] = val;
          es[j] = err;
        }
      }
    } else {
      for (size_t j = 0; j < ys.size(); ++j) {
        const double originalVal = histogram.y()[j];
        if (background < 0) {
          ys[j] = 0;
          es[j] = 0;
        } else if (nullifyNegative && (background > originalVal)) {
          ys[j] = originalVal;
          es[j] = es[j] > background ? es[j] : background;
        } else {
          ys[j] = background;
          es[j] = std::sqrt(variance);
        }
      }
    }
    if (wasCounts) {
      outHistogram.convertToCounts();
    }
    outputWS->setHistogram(i, outHistogram);
    m_progress->report();
    PARALLEL_END_INTERUPT_REGION
  }
  PARALLEL_CHECK_INTERUPT_REGION

  g_log.debug() << calculationCount << " spectra corrected\n";
  g_log.information() << "The mean background was "
                      << backgroundTotal / static_cast<double>(calculationCount)
                      << ".\n";
  // Assign the output workspace to its property
  setProperty("OutputWorkspace", outputWS);
}

/**
* Checks that the range parameters have been set correctly.
* @param startX the starting point
* @param endX the ending point
* @throw std::invalid_argument if XMin or XMax are not set, or XMax is less
* than XMin
*/
void CalculateFlatBackground::checkRange(double &startX, double &endX) {
  // use the overloaded operator =() to get the X value stored in each property
  startX = getProperty("StartX");
  endX = getProperty("EndX");

  if (startX > endX) {
    const std::string failure("XMax must be greater than XMin.");
    g_log.error(failure);
    throw std::invalid_argument(failure);
  }
}

/**
* Gets the mean number of counts in each bin the background region and the
* variance (error^2) of that number.
* @param histogram the histogram to operate on
* @param background an output variable for the calculated background
* @param variance an output variable for background's variance.
* @param startX an X-value in the first bin that will be considered, must not
* be greater endX
* @param endX an X-value in the last bin that will be considered, must not
* less than startX
* @throw out_of_range if either startX or endX are out of the range of X-values
* in the specified spectrum
* @throw invalid_argument if endX has the value of first X-value one of the
* spectra
*/
void CalculateFlatBackground::Mean(const HistogramData::Histogram &histogram,
                                   double &background, double &variance,
                                   const double startX,
                                   const double endX) const {
  const auto &XS = histogram.x();
  const auto &YS = histogram.y();
  const auto &ES = histogram.e();
  // the function checkRange should already have checked that startX <= endX,
  // but we still need to check values weren't out side the ranges
  if ((endX > XS.back()) || (startX < XS.front())) {
    throw std::out_of_range("Either the property startX or endX is outside the "
                            "range of X-values present in one of the specified "
                            "spectra");
  }
  // Get the index of the first bin contains the X-value, which means this is an
  // inclusive sum. The minus one is because lower_bound() returns index past
  // the last index pointing to a lower value. For example if startX has a
  // higher X value than the first bin boundary but lower than the second
  // lower_bound returns 1, which is the index of the second bin boundary
  ptrdiff_t startInd =
      std::lower_bound(XS.begin(), XS.end(), startX) - XS.begin() - 1;
  if (startInd == -1) { // happens if startX is the first X-value, e.g. the
                        // first X-value is zero and the user selects zero
    startInd = 0;
  }

  // the -1 matches definition of startIn, see the comment above that statement
  const ptrdiff_t endInd =
      std::lower_bound(XS.begin() + startInd, XS.end(), endX) - XS.begin() - 1;
  if (endInd == -1) { //
    throw std::invalid_argument("EndX was set to the start of one of the "
                                "spectra, it must greater than the first "
                                "X-value in any of the specified spectra");
  }

  // the +1 is because this is an inclusive sum (includes each bin that contains
  // each X-value). Hence if startInd == endInd we are still analyzing one bin
  const double numBins = static_cast<double>(1 + endInd - startInd);
  // the +1 here is because the accumulate() stops one before the location of
  // the last iterator
  background =
      std::accumulate(YS.begin() + startInd, YS.begin() + endInd + 1, 0.0) /
      numBins;
  // The error on the total number of background counts in the background region
  // is taken as the sqrt the total number counts. To get the the error on the
  // counts in each bin just divide this by the number of bins. The variance =
  // error^2 that is the total variance divide by the number of bins _squared_.
  variance = std::accumulate(ES.begin() + startInd, ES.begin() + endInd + 1,
                             0.0, VectorHelper::SumSquares<double>()) /
             (numBins * numBins);
}

/**
* Uses linear algorithm to do the fitting.
* @param histogram the histogram to fit
* @param background an output variable for the calculated background
* @param variance an output variable for background's variance, currently always
* zero.
* @param startX an X value in the first bin to be included in the fit
* @param endX an X value in the last bin to be included in the fit
*/
void CalculateFlatBackground::LinearFit(
    const HistogramData::Histogram &histogram, double &background,
    double &variance, const double startX, const double endX) {
  MatrixWorkspace_sptr WS = WorkspaceFactory::Instance().create(
      "Workspace2D", 1, histogram.x().size(), histogram.y().size());
  WS->setHistogram(0, histogram);
  IAlgorithm_sptr childAlg = createChildAlgorithm("Fit");

  IFunction_sptr func =
      API::FunctionFactory::Instance().createFunction("LinearBackground");
  childAlg->setProperty<IFunction_sptr>("Function", func);

  childAlg->setProperty<MatrixWorkspace_sptr>("InputWorkspace", WS);
  childAlg->setProperty<bool>("CreateOutput", true);
  childAlg->setProperty<int>("WorkspaceIndex", 0);
  childAlg->setProperty<double>("StartX", startX);
  childAlg->setProperty<double>("EndX", endX);
  // Default minimizer doesn't work properly even on the easiest cases,
  // so Levenberg-MarquardtMD is used instead
  childAlg->setProperty<std::string>("Minimizer", "Levenberg-MarquardtMD");

  childAlg->executeAsChildAlg();

  std::string outputStatus = childAlg->getProperty("OutputStatus");
  if (outputStatus != "success") {
    g_log.warning("Unable to successfully fit the data: " + outputStatus);
    background = -1;
    return;
  }

  Mantid::API::ITableWorkspace_sptr output =
      childAlg->getProperty("OutputParameters");

  // Find rows with parameters we are after
  size_t rowA0, rowA1;
  output->find(static_cast<std::string>("A0"), rowA0, 0);
  output->find(static_cast<std::string>("A1"), rowA1, 0);

  // Linear function is defined as A0 + A1*x
  const double intercept = output->cell<double>(rowA0, 1);
  const double slope = output->cell<double>(rowA1, 1);

  const double centre = (startX + endX) / 2.0;

  // Calculate the value of the flat background by taking the value at the
  // centre point of the fit
  background = slope * centre + intercept;
  // ATM we don't calculate the error here.
  variance = 0;
}

/**
* Utilizes cyclic boundary conditions when calculating the
* average in the window.
* @param histogram the histogram to operate on
* @param background an output variable for the calculated background
* @param variance an output variable for background's variance.
* @param windowWidth the width of the averaging window in bins
*/
void CalculateFlatBackground::MovingAverage(
    const HistogramData::Histogram &histogram, double &background,
    double &variance, const size_t windowWidth) const {
  const auto &ys = histogram.y();
  const auto &es = histogram.e();
  double currentMin = std::numeric_limits<double>::max();
  double currentVariance = 0;

  for (size_t i = 0; i < ys.size(); ++i) {
    double sum = 0;
    double varSqSum = 0;
    for (size_t j = 0; j < windowWidth; ++j) {
      size_t index = i + j;
      if (index >= ys.size()) {
        // Cyclic boundary conditions.
        index -= ys.size();
      }
      sum += ys[index];
      varSqSum += es[index] * es[index];
    }
    const double average = sum / static_cast<double>(windowWidth);
    if (average < currentMin) {
      currentMin = average;
      currentVariance =
          varSqSum / static_cast<double>(windowWidth * windowWidth);
    }
  }
  background = currentMin;
  variance = currentVariance;
}

} // namespace Algorithms
} // namespace Mantid