Newer
Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#include "MantidAlgorithms/ReflectometrySumInQ.h"
#include "MantidAPI/Algorithm.tcc"
#include "MantidAPI/InstrumentValidator.h"
#include "MantidAPI/SpectrumInfo.h"
#include "MantidAPI/WorkspaceUnitValidator.h"
#include "MantidDataObjects/Workspace2D.h"
#include "MantidDataObjects/WorkspaceCreation.h"
#include "MantidGeometry/IDetector.h"
#include "MantidHistogramData/LinearGenerator.h"
#include "MantidIndexing/IndexInfo.h"
#include "MantidIndexing/SpectrumNumber.h"
#include "MantidKernel/BoundedValidator.h"
#include "MantidKernel/CompositeValidator.h"
#include "MantidKernel/MandatoryValidator.h"
#include "MantidKernel/Strings.h"
namespace {
namespace Prop {
const static std::string BEAM_CENTRE{"BeamCentre"};
const static std::string INPUT_WS{"InputWorkspace"};
const static std::string IS_FLAT_SAMPLE{"FlatSample"};
const static std::string OUTPUT_WS{"OutputWorkspace"};
const static std::string WAVELENGTH_MAX{"WavelengthMax"};
const static std::string WAVELENGTH_MIN{"WavelengthMin"};
}
/**
* Share the given input counts into the output array bins proportionally
* according to how much the bins overlap the given lambda range.
* outputX.size() must equal outputY.size() + 1
*
* @param inputCounts [in] :: the input counts to share out
* @param inputErr [in] :: the input errors to share out
* @param bLambda [in] :: the bin width in lambda
* @param lambdaMin [in] :: the start of the range to share counts to
* @param lambdaMax [in] :: the end of the range to share counts to
* @param outSpecIdx [in] :: the spectrum index to be updated in the output
* workspace
* @param IvsLam [in,out] :: the output workspace
* @param outputE [in,out] :: the projected E values
*/
void shareCounts(
const double inputCounts, const double inputErr,
const Mantid::Algorithms::ReflectometrySumInQ::MinMax &lambdaRange,
Mantid::API::MatrixWorkspace &IvsLam, std::vector<double> &outputE) {
// Check that we have histogram data
const auto &outputX = IvsLam.dataX(0);
auto &outputY = IvsLam.dataY(0);
if (outputX.size() != outputY.size() + 1) {
throw std::runtime_error(
"Expected output array to be histogram data (got X len=" +
std::to_string(outputX.size()) + ", Y len=" +
std::to_string(outputY.size()) + ")");
}
const double totalWidth = lambdaRange.max - lambdaRange.min;
// Get the first bin edge in the output X array that is within range.
// There will probably be some overlap, so start from the bin edge before
// this (unless we're already at the first bin edge).
auto startIter = std::lower_bound(outputX.begin(), outputX.end(), lambdaRange.min);
if (startIter != outputX.begin()) {
--startIter;
}
// Loop through all overlapping output bins. Convert the iterator to an
// index because we need to index both the X and Y arrays.
const int xSize = static_cast<int>(outputX.size());
for (auto outIdx = startIter - outputX.begin(); outIdx < xSize - 1;
++outIdx) {
const double binStart = outputX[outIdx];
const double binEnd = outputX[outIdx + 1];
if (binStart > lambdaRange.max) {
// No longer in the overlap region so we're finished
break;
}
// Add a share of the input counts to this bin based on the proportion of
// overlap.
if (totalWidth > Mantid::Kernel::Tolerance) {
// Share counts out proportionally based on the overlap of this range
const double overlapWidth =
std::min({binEnd - binStart, totalWidth, lambdaRange.max - binStart, binEnd - lambdaRange.min});
const double fraction = overlapWidth / totalWidth;
outputY[outIdx] += inputCounts * fraction;
outputE[outIdx] += inputErr * fraction;
} else {
// Projection to a single value. Put all counts in the overlapping output
// bin.
outputY[outIdx] += inputCounts;
outputE[outIdx] += inputCounts;
}
}
}
Mantid::Algorithms::ReflectometrySumInQ::MinMax twoThetaWidth(const size_t wsIndex, const Mantid::API::SpectrumInfo &spectrumInfo) {
const double twoTheta = spectrumInfo.twoTheta(wsIndex);
Mantid::Algorithms::ReflectometrySumInQ::MinMax range;
if (wsIndex == 0) {
if (spectrumInfo.size() == 1) {
throw std::runtime_error("Cannot calculate the two theta range from a single detector only.");
}
const auto nextTwoTheta = spectrumInfo.twoTheta(1);
const auto d = std::abs(nextTwoTheta - twoTheta) / 2.;
range.min = twoTheta - d;
range.max = twoTheta + d;
} else if (wsIndex == spectrumInfo.size() - 1) {
const auto previousTwoTheta = spectrumInfo.twoTheta(wsIndex - 1);
const auto d = std::abs(twoTheta - previousTwoTheta) / 2.;
range.min = twoTheta - d;
range.max = twoTheta + d;
} else {
const auto t1 = spectrumInfo.twoTheta(wsIndex - 1);
const auto t2 = spectrumInfo.twoTheta(wsIndex + 1);
Mantid::Algorithms::ReflectometrySumInQ::MinMax neighbours(t1, t2);
const auto d1 = std::abs(twoTheta - neighbours.min) / 2.;
const auto d2 = std::abs(neighbours.max - twoTheta) / 2.;
range.min = twoTheta - d1;
range.max = twoTheta + d2;
return range;
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
}
}
namespace Mantid {
namespace Algorithms {
ReflectometrySumInQ::MinMax::MinMax(const double a, const double b) noexcept
: min(std::min(a, b)), max(std::max(a, b))
{
}
void ReflectometrySumInQ::MinMax::testAndSet(const double a) noexcept {
if (a < min) {
min = a;
}
if (a > max) {
max = a;
}
}
// Register the algorithm into the AlgorithmFactory
DECLARE_ALGORITHM(ReflectometrySumInQ)
//----------------------------------------------------------------------------------------------
/// Algorithms name for identification. @see Algorithm::name
const std::string ReflectometrySumInQ::name() const { return "ReflectometrySumInQ"; }
/// Algorithm's version for identification. @see Algorithm::version
int ReflectometrySumInQ::version() const { return 1; }
/// Algorithm's category for identification. @see Algorithm::category
const std::string ReflectometrySumInQ::category() const {
return "Reflectometry;ILL\\Reflectometry";
}
/// Algorithm's summary for use in the GUI and help. @see Algorithm::summary
const std::string ReflectometrySumInQ::summary() const {
return "Sum counts in lambda along lines of constant Q by projecting to virtual lambda at a reference angle.";
}
/** Initialize the algorithm's properties.
*/
void ReflectometrySumInQ::init() {
auto inputWSValidator = boost::make_shared<Kernel::CompositeValidator>();
inputWSValidator->add<API::WorkspaceUnitValidator>("Wavelength");
inputWSValidator->add<API::InstrumentValidator>();
auto mandatoryNonnegativeDouble = boost::make_shared<Kernel::CompositeValidator>();
mandatoryNonnegativeDouble->add<Kernel::MandatoryValidator<double>>();
auto nonnegativeDouble = boost::make_shared<Kernel::BoundedValidator<double>>();
nonnegativeDouble->setLower(0.);
mandatoryNonnegativeDouble->add(nonnegativeDouble);
auto mandatoryNonnegativeInt = boost::make_shared<Kernel::CompositeValidator>();
mandatoryNonnegativeInt->add<Kernel::MandatoryValidator<int>>();
auto nonnegativeInt = boost::make_shared<Kernel::BoundedValidator<int>>();
nonnegativeInt->setLower(0);
mandatoryNonnegativeInt->add(nonnegativeInt);
declareWorkspaceInputProperties<API::MatrixWorkspace, API::IndexType::SpectrumNum | API::IndexType::WorkspaceIndex>(Prop::INPUT_WS, "A workspace in X units of wavelength to be summed.", inputWSValidator);
declareProperty(
Kernel::make_unique<API::WorkspaceProperty<API::MatrixWorkspace>>(Prop::OUTPUT_WS, "",
Kernel::Direction::Output),
"A single histogram workspace containing the result of summation in Q.");
declareProperty(Prop::BEAM_CENTRE, EMPTY_INT(), mandatoryNonnegativeInt, "Fractional workspace index of the specular reflection centre.");
declareProperty(Prop::WAVELENGTH_MIN, EMPTY_DBL(), mandatoryNonnegativeDouble, "Minimum wavelength in Angstroms.");
declareProperty(Prop::WAVELENGTH_MAX, EMPTY_DBL(), mandatoryNonnegativeDouble, "Maximum wavelength in Angstroms.");
declareProperty(Prop::IS_FLAT_SAMPLE, true, "If true, the summation is handled as the standard divergent beam case, otherwise as the non-flat sample case.");
}
/** Execute the algorithm.
*/
void ReflectometrySumInQ::exec() {
API::MatrixWorkspace_sptr inWS;
Indexing::SpectrumIndexSet indices;
std::tie(inWS, indices) = getWorkspaceAndIndices<API::MatrixWorkspace>(Prop::INPUT_WS);
checkBeamCentreIn(indices);
auto outWS = sumInQ(*inWS, indices);
setProperty(Prop::OUTPUT_WS, outWS);
}
std::map<std::string, std::string> ReflectometrySumInQ::validateInputs() {
std::map<std::string, std::string> issues;
const double wavelengthMin = getProperty(Prop::WAVELENGTH_MIN);
const double wavelengthMax = getProperty(Prop::WAVELENGTH_MAX);
if (wavelengthMin >= wavelengthMax) {
issues[Prop::WAVELENGTH_MIN] = "Mininum wavelength cannot be greater or equal to maximum wavelength";
}
return issues;
}
void ReflectometrySumInQ::checkBeamCentreIn(const Indexing::SpectrumIndexSet &indices) {
const int beamCentre = getProperty(Prop::BEAM_CENTRE);
const auto iter = std::find(indices.begin(), indices.end(), static_cast<size_t>(beamCentre));
if (iter == indices.end()) {
throw std::runtime_error(Prop::BEAM_CENTRE + " is not included in InputWorkspaceIndexSet.");
}
}
/**
* Construct an "empty" output workspace in virtual-lambda for summation in Q.
* The workspace will have the same x values as the input workspace but the y
* values will all be zero.
*
* @return : a 1D workspace where y values are all zero
*/
API::MatrixWorkspace_sptr
ReflectometrySumInQ::constructIvsLamWS(const API::MatrixWorkspace &detectorWS, const Indexing::SpectrumIndexSet &indices, const Angles &refAngles) {
// Calculate the number of bins based on the min/max wavelength, using
// the same bin width as the input workspace
const int twoThetaRIdx = getProperty(Prop::BEAM_CENTRE);
const auto &x = detectorWS.x(static_cast<size_t>(twoThetaRIdx));
const double binWidth = (x.back() - x.front()) / static_cast<double>(x.size());
const auto wavelengthRange = findWavelengthMinMax(detectorWS, indices, refAngles);
if (std::abs(wavelengthRange.max - wavelengthRange.min) < binWidth) {
throw std::runtime_error("");
}
const int numBins = static_cast<int>(
std::ceil((wavelengthRange.max - wavelengthRange.min) / binWidth));
// Construct the histogram with these X values. Y and E values are zero.
const HistogramData::BinEdges bins(numBins, HistogramData::LinearGenerator(wavelengthRange.min, binWidth));
const HistogramData::Histogram modelHistogram(bins);
// Create the output workspace
API::MatrixWorkspace_sptr outputWS = DataObjects::create<DataObjects::Workspace2D>(detectorWS, 1, std::move(modelHistogram));
// Set the detector ID from the twoThetaR detector.
auto &outSpec = outputWS->getSpectrum(0);
// TODO: Handle grouped detectors correctly.
const detid_t twoThetaRDetID =
detectorWS.spectrumInfo().detector(static_cast<size_t>(twoThetaRIdx)).getID();
outSpec.clearDetectorIDs();
outSpec.addDetectorID(twoThetaRDetID);
return outputWS;
}
ReflectometrySumInQ::MinMax ReflectometrySumInQ::findWavelengthMinMax(const API::MatrixWorkspace &detectorWS, const Indexing::SpectrumIndexSet &indices, const Angles &refAngles) {
const double lambdaMin = getProperty(Prop::WAVELENGTH_MIN);
const double lambdaMax = getProperty(Prop::WAVELENGTH_MAX);
const API::SpectrumInfo &spectrumInfo = detectorWS.spectrumInfo();
// Get the new max and min X values of the projected (virtual) lambda range
// Find minimum and maximum 2thetas and the corresponding indices.
// It cannot be assumed that 2theta increases with indices, check for example
// D17 at ILL
std::pair<size_t, double> twoThetaMin{0, std::numeric_limits<double>::max()};
std::pair<size_t, double> twoThetaMax{0, std::numeric_limits<double>::lowest()};
for (const auto i : indices) {
const auto twoTheta = spectrumInfo.signedTwoTheta(i);
if (twoTheta < twoThetaMin.second) {
twoThetaMin.first = i;
twoThetaMin.second = twoTheta;
}
if (twoTheta > twoThetaMax.second) {
twoThetaMax.first = i;
twoThetaMax.second = twoTheta;
}
}
MinMax wavelengthRange;
const auto twoThetaMinRange = twoThetaWidth(twoThetaMin.first, spectrumInfo);
// For bLambda, use the average bin size for this spectrum
auto xValues = detectorWS.x(twoThetaMin.first);
double bLambda = (xValues[xValues.size() - 1] - xValues[0]) /
static_cast<int>(xValues.size());
MinMax lambdaRange(lambdaMax - bLambda / 2., lambdaMax + bLambda / 2.);
auto r = projectedLambdaRange(lambdaRange, twoThetaMinRange, refAngles);
wavelengthRange.max = r.max;
const auto twoThetaMaxRange = twoThetaWidth(twoThetaMax.first, spectrumInfo);
xValues = detectorWS.x(twoThetaMax.first);
bLambda = (xValues[xValues.size() - 1] - xValues[0]) /
static_cast<int>(xValues.size());
lambdaRange.min = lambdaMin - bLambda / 2.;
lambdaRange.max = lambdaMin + bLambda / 2.;
r = projectedLambdaRange(lambdaRange, twoThetaMaxRange, refAngles);
wavelengthRange.min = r.min;
if (wavelengthRange.min > wavelengthRange.max) {
throw std::runtime_error(
"Error projecting lambda range to reference line; projected range (" +
std::to_string(wavelengthRange.min) + "," + std::to_string(wavelengthRange.max) +
") is negative.");
}
return wavelengthRange;
}
/**
* Share counts from an input value onto the projected output in virtual-lambda
*
* @param inputIdx [in] :: the index into the input arrays
* @param twoTheta [in] :: the value of twotTheta for this spectrum
* @param bTwoTheta [in] :: the size of the pixel in twoTheta
* @param inputX [in] :: the input spectrum X values
* @param inputY [in] :: the input spectrum Y values
* @param inputE [in] :: the input spectrum E values
* @param detectors [in] :: spectrum indices of the detectors of interest
* @param outSpecIdx [in] :: the output spectrum index
* @param IvsLam [in,out] :: the output workspace
* @param outputE [in,out] :: the projected E values
*/
void ReflectometrySumInQ::processValue(const int inputIdx, const MinMax &twoThetaRange,
const Angles &refAngles, const HistogramData::HistogramX &inputX, const HistogramData::HistogramY &inputY,
const HistogramData::HistogramE &inputE, API::MatrixWorkspace &IvsLam,
std::vector<double> &outputE) {
// Check whether there are any counts (if not, nothing to share)
const double inputCounts = inputY[inputIdx];
if (inputCounts <= 0.0 || std::isnan(inputCounts) ||
std::isinf(inputCounts)) {
return;
}
// Get the bin width and the bin centre
const MinMax wavelengthRange(inputX[inputIdx], inputX[inputIdx + 1]);
// Project these coordinates onto the virtual-lambda output (at twoThetaR)
const auto lambdaRange = projectedLambdaRange(wavelengthRange, twoThetaRange, refAngles);
// Share the input counts into the output array
shareCounts(inputCounts, inputE[inputIdx], lambdaRange, IvsLam, outputE);
}
/**
* Project an input pixel onto an arbitrary reference line at twoThetaR. The
* projection is done along lines of constant Q, which emanate from theta0. The
* top-left and bottom-right corners of the pixel are projected, resulting in an
* output range in "virtual" lambda (lambdaV).
*
* For a description of this projection, see:
* R. Cubitt, T. Saerbeck, R.A. Campbell, R. Barker, P. Gutfreund
* J. Appl. Crystallogr., 48 (6) (2015)
*
* @param lambda [in] :: the lambda coord of the centre of the pixel to project
* @param twoTheta [in] :: the twoTheta coord of the centre of the pixel to
*project
* @param bLambda [in] :: the pixel size in lambda
* @param bTwoTheta [in] :: the pixel size in twoTheta
* @param detectors [in] :: spectrum indices of the detectors of interest
* @param lambdaVMin [out] :: the projected range start
* @param lambdaVMax [out] :: the projected range end
*/
ReflectometrySumInQ::MinMax ReflectometrySumInQ::projectedLambdaRange(const MinMax &wavelengthRange, const MinMax &twoThetaRange, const Angles &refAngles) {
// We cannot project pixels below the horizon angle
if (twoThetaRange.min <= refAngles.horizon) {
const auto twoTheta = (twoThetaRange.min + twoThetaRange.max) / 2.;
throw std::runtime_error("Cannot process twoTheta=" +
std::to_string(twoTheta * 180.0 / M_PI) +
" as it is below the horizon angle=" +
std::to_string(refAngles.horizon * 180.0 / M_PI));
}
// Calculate the projected wavelength range
MinMax range;
try {
const double lambdaTop =
wavelengthRange.max *
std::sin(refAngles.delta) /
std::sin(twoThetaRange.min - refAngles.horizon);
const double lambdaBot =
wavelengthRange.min *
std::sin(refAngles.delta) /
std::sin(twoThetaRange.max - refAngles.horizon);
range.testAndSet(lambdaBot);
range.testAndSet(lambdaTop);
} catch (std::exception &ex) {
const auto twoTheta = (twoThetaRange.min + twoThetaRange.max) / 2.;
const auto lambda = (wavelengthRange.min + wavelengthRange.max) / 2.;
throw std::runtime_error(
"Failed to project (lambda, twoTheta) = (" + std::to_string(lambda) +
"," + std::to_string(twoTheta * 180.0 / M_PI) + ") onto twoThetaR = " +
std::to_string(refAngles.twoTheta) + ": " + ex.what());
}
return range;
}
ReflectometrySumInQ::Angles ReflectometrySumInQ::referenceAngles(const API::SpectrumInfo &spectrumInfo) {
Angles a;
const int beamCentre = getProperty(Prop::BEAM_CENTRE);
const double centreTwoTheta = spectrumInfo.signedTwoTheta(static_cast<size_t>(beamCentre));
const bool isFlat = getProperty(Prop::IS_FLAT_SAMPLE);
if (isFlat) {
a.horizon = centreTwoTheta / 2.;
} else {
a.horizon = 0.;
}
a.twoTheta = centreTwoTheta;
a.delta = a.twoTheta - a.horizon;
return a;
}
/**
* Sum counts from the input workspace in lambda along lines of constant Q by
* projecting to "virtual lambda" at a reference angle twoThetaR.
*
* @param detectorWS [in] :: the input workspace in wavelength
* @return :: the output workspace in wavelength
*/
API::MatrixWorkspace_sptr
ReflectometrySumInQ::sumInQ(const API::MatrixWorkspace &detectorWS, const Indexing::SpectrumIndexSet &indices) {
const auto spectrumInfo = detectorWS.spectrumInfo();
const auto refAngles = referenceAngles(spectrumInfo);
// Construct the output array in virtual lambda
API::MatrixWorkspace_sptr IvsLam = constructIvsLamWS(detectorWS, indices, refAngles);
auto &outputE = IvsLam->dataE(0);
// Loop through each spectrum in the detector group
for (auto spIdx : indices) {
// Get the size of this detector in twoTheta
const auto twoThetaRange = twoThetaWidth(spIdx, spectrumInfo);
// Check X length is Y length + 1
const auto &inputX = detectorWS.x(spIdx);
const auto &inputY = detectorWS.y(spIdx);
const auto &inputE = detectorWS.e(spIdx);
if (inputX.size() != inputY.size() + 1) {
throw std::runtime_error(
"Expected input workspace to be histogram data (got X len=" +
std::to_string(inputX.size()) + ", Y len=" +
std::to_string(inputY.size()) + ")");
}
// Create a vector for the projected errors for this spectrum.
// (Output Y values can simply be accumulated directly into the output
// workspace, but for error values we need to create a separate error
// vector for the projected errors from each input spectrum and then
// do an overall sum in quadrature.)
std::vector<double> projectedE(outputE.size(), 0.0);
// Process each value in the spectrum
const int ySize = static_cast<int>(inputY.size());
for (int inputIdx = 0; inputIdx < ySize; ++inputIdx) {
// Do the summation in Q
processValue(inputIdx, twoThetaRange, refAngles, inputX, inputY,
inputE, *IvsLam, projectedE);
}
// Sum errors in quadrature
const int eSize = static_cast<int>(outputE.size());
for (int outIdx = 0; outIdx < eSize; ++outIdx) {
outputE[outIdx] += projectedE[outIdx] * projectedE[outIdx];
}
}
// Take the square root of all the accumulated squared errors for this
// detector group. Assumes Gaussian errors
double (*rs)(double) = std::sqrt;
std::transform(outputE.begin(), outputE.end(), outputE.begin(), rs);
return IvsLam;
}
} // namespace Algorithms
} // namespace Mantid