-
Hahn, Steven authoredHahn, Steven authored
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