Mantid
Loading...
Searching...
No Matches
HeliumAnalyserEfficiency.cpp
Go to the documentation of this file.
1// Mantid Repository : https://github.com/mantidproject/mantid
2//
3// Copyright © 2025 ISIS Rutherford Appleton Laboratory UKRI,
4// NScD Oak Ridge National Laboratory, European Spallation Source,
5// Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS
6// SPDX - License - Identifier: GPL - 3.0 +
7
10
13#include "MantidAPI/Axis.h"
18
24
25#include <algorithm>
26#include <boost/math/distributions/students_t.hpp>
27#include <vector>
28
29namespace Mantid::Algorithms {
30// Register the algorithm into the algorithm factory
31DECLARE_ALGORITHM(HeliumAnalyserEfficiency)
32
33using namespace Kernel;
34using namespace API;
35
36constexpr double LAMBDA_CONVERSION_FACTOR = 0.0733;
37
38namespace PropertyNames {
39constexpr auto INPUT_WORKSPACES{"InputWorkspaces"};
40constexpr auto OUTPUT_WORKSPACE{"OutputWorkspace"};
41constexpr auto SPIN_STATES{"SpinStates"};
42
43constexpr auto PXD{"PXD"};
44constexpr auto PXD_ERROR{"PXDError"};
45constexpr auto DECAY_TIME_INITIAL{"DecayTimeInitial"};
46constexpr auto H3_POLARIZATION_INITIAL{"H3PolarizationInitial"};
47
48constexpr auto IGNORE_FIT_QUALITY_ERROR{"IgnoreFitQualityError"};
49constexpr auto OUTPUT_FIT_CURVES{"OutputFitCurves"};
50constexpr auto OUTPUT_FIT_PARAMS{"OutputFitParameters"};
51
52constexpr auto GROUP_INPUTS{"Inputs"};
53constexpr auto GROUP_FIT_OPTIONS{"Fit Options"};
54constexpr auto GROUP_OUTPUTS{"Outputs"};
55} // namespace PropertyNames
56
57namespace Fitting {
58constexpr auto OUTPUT_HE3_FIT{"_He3_polarization"};
59constexpr auto OUTPUT_DECAY_FIT{"_decay"};
60constexpr auto INPUT_WORKSPACE{"InputWorkspace"};
61constexpr auto START_X{"StartX"};
62constexpr auto END_X{"EndX"};
63
65constexpr double PXD_INITIAL = 12.0;
66constexpr double PXD_ERROR_INITIAL = 0.0;
67constexpr double START_WAVELENGTH_INITIAL = 1.75;
68constexpr double END_WAVELENGTH_INITIAL = 8;
69constexpr double H3_POLARIZATION_INITIAL = 0.6;
70constexpr double DECAY_TIME_INITIAL = 54; // Hours
71
72std::shared_ptr<IFunction> prepareExpDecayFunction(const double initialDecay, const double initialPolarization) {
73 std::string funcStr = "name=ExpDecay,";
74 const std::string params =
75 "Lifetime=" + std::to_string(initialDecay) + "," + "Height=" + std::to_string(initialPolarization);
76 funcStr += params;
77 return FunctionFactory::Instance().createInitialized(funcStr);
78}
79
80std::shared_ptr<MultiDomainFunction> prepareEfficiencyFunc(const double mu, const size_t numberOfDomains) {
81 const auto efficiencyFunc = "name=UserFunction,Formula=(1 + tanh(" + std::to_string(mu) + "*phe*x))/2";
82 return FunctionFactory::Instance().createInitializedMultiDomainFunction(efficiencyFunc, numberOfDomains);
83}
84
85MatrixWorkspace_sptr createFitDecayWorkspace(const std::vector<double> &time, const std::vector<double> &timeErrors,
86 const std::vector<double> &HePolarization,
87 const std::vector<double> &HePolarizationErrors) {
88 const HistogramData::Points xVals(time);
89 const HistogramData::Frequencies yVals(HePolarization);
90 const HistogramData::FrequencyStandardDeviations eVals(
91 HePolarizationErrors.empty() ? std::vector<double>(HePolarization.size()) : HePolarizationErrors);
92 auto retVal = std::make_shared<DataObjects::Workspace2D>();
93 retVal->initialize(1, HistogramData::Histogram(xVals, yVals, eVals));
94 retVal->setPointStandardDeviations(0, timeErrors.empty() ? std::vector<double>(time.size()) : timeErrors);
95 return retVal;
96}
97} // namespace Fitting
98
99namespace {
100Mantid::Kernel::Logger logger("HeliumAnalyserEfficiency");
101constexpr size_t NUM_FIT_PARAMS = 1;
102
103MatrixWorkspace_sptr calculateAnalyserEfficiency(const WorkspaceGroup_sptr &groupWorkspace,
104 const std::string &spinStates) {
105 using namespace PolarizationCorrectionsHelpers;
106 using namespace FlipperConfigurations;
107
108 const auto t11Ws = workspaceForSpinState(groupWorkspace, spinStates, ON_ON);
109 const auto t10Ws = workspaceForSpinState(groupWorkspace, spinStates, ON_OFF);
110 const auto t01Ws = workspaceForSpinState(groupWorkspace, spinStates, OFF_ON);
111 const auto t00Ws = workspaceForSpinState(groupWorkspace, spinStates, OFF_OFF);
112
113 // T_NSF = T11 + T00 (NSF = not spin flipped)
114 const MatrixWorkspace_sptr tnsfWs = t11Ws + t00Ws;
115 // T_SF = T01 + T10 (SF = spin flipped)
116 const MatrixWorkspace_sptr tsfWs = t01Ws + t10Ws;
117
118 // Calculate the analyser efficiency from the data, eff = T_NSF / (T_NSF + T_SF)
119 return tnsfWs / (tnsfWs + tsfWs);
120}
121
122// We template this as we expect either a vector of MatrixWorkspace_sptr or TableWorkspace_sptr
123WorkspaceGroup_sptr prepareOutputGroup(const std::vector<MatrixWorkspace_sptr> &workspaces, const std::string &baseName,
124 const std::string &suffix = "") {
125 // If we are extracting the names for the curves, the last workspace of the input vector will
126 // always correspond to the decay fit.
127 // The first n-1 workspaces refers each to one of the He3 Polarization fit curves.
128 auto outputName = [&](const size_t index) -> std::string {
129 return suffix.empty() ? baseName + "_" + std::to_string(index)
130 : (workspaces.size() > 1 && index == workspaces.size() - 1
131 ? baseName + Fitting::OUTPUT_DECAY_FIT + suffix + "_0"
132 : baseName + Fitting::OUTPUT_HE3_FIT + suffix + "_" + std::to_string(index));
133 };
134
135 const auto group = std::make_shared<WorkspaceGroup>();
136 for (size_t index = 0; index < workspaces.size(); index++) {
137 AnalysisDataService::Instance().addOrReplace(outputName(index), workspaces.at(index));
138 group->addWorkspace(workspaces.at(index));
139 }
140 return group;
141}
142} // unnamed namespace
143
145 : m_outputCurves{std::vector<MatrixWorkspace_sptr>()}, m_outputParameters{std::vector<ITableWorkspace_sptr>()} {};
146
148 declareProperty(std::make_unique<ArrayProperty<std::string>>("InputWorkspaces", std::make_shared<ADSValidator>()),
149 "List of Polarized Transmission Group Workspaces. Each item on the list must be a workspace group "
150 "with 4 members, "
151 "each one representing a spin state.");
152 const auto spinValidator = std::make_shared<SpinStateValidator>(std::unordered_set<int>{4});
154 PropertyNames::SPIN_STATES, "", spinValidator,
155 "Order of individual spin configurations in the input group workspaces, e.g. \"01,11,00,10\", it is assumed"
156 " that all input workspaces have the same spin order.");
157}
158
160 const auto mustBePositive = std::make_shared<BoundedValidator<double>>();
161 mustBePositive->setLower(0);
162
164 "Gas pressure in bar multiplied by cell length in metres");
166 "Error in gas pressure (p x d)");
168 "Initial decay time for He3 Polarization decay fit");
170 "Initial polarization for He3 Polarization decay fit");
172 "Lower boundary of wavelength range to use for fitting helium polarization");
174 "Upper boundary of wavelength range to use for fitting helium polarization");
176 "Whether the algorithm should ignore a poor chi-squared (fit cost value) of greater than 1 and "
177 "therefore not throw an error");
178}
180
183 "Helium analyzer efficiency as a function of wavelength");
184
187 "A group workspace containing the fit curves.");
188
191 "A table workspace containing the fit parameters.");
192}
193
214
221std::map<std::string, std::string> HeliumAnalyserEfficiency::validateInputs() {
222 std::map<std::string, std::string> errorList;
223 const std::vector<std::string> &inputWorkspaces = getProperty(PropertyNames::INPUT_WORKSPACES);
224 const auto polSANSValidator = std::make_shared<PolSANSWorkspaceValidator>();
225 for (const auto &wsName : inputWorkspaces) {
226 const auto ws = AnalysisDataService::Instance().retrieveWS<WorkspaceGroup>(wsName);
227 if (!ws) {
228 errorList[PropertyNames::INPUT_WORKSPACES] += "Workspace " + wsName + " is not a group workspace.";
229 } else if (const auto polSANSError = polSANSValidator->isValid(ws); !polSANSError.empty()) {
230 errorList[PropertyNames::INPUT_WORKSPACES] += "Error in workspace " + wsName + " : " + polSANSError;
231 }
232 }
233 return errorList;
234}
235
236ITableWorkspace_sptr HeliumAnalyserEfficiency::prepareOutputTable(const std::vector<ITableWorkspace_sptr> &tables) {
237 if (tables.size() == 1) {
238 return tables.at(0);
239 }
240
241 const auto combineTableAlg = createChildAlgorithm("CombineTableWorkspaces");
242 // Tables vector only has two tables.
243 combineTableAlg->initialize();
244 combineTableAlg->setProperty("LHSWorkspace", tables.at(0));
245 combineTableAlg->setProperty("RHSWorkspace", tables.at(1));
246 combineTableAlg->execute();
247
248 const DataObjects::TableWorkspace_sptr table = combineTableAlg->getProperty("OutputWorkspace");
249 return std::dynamic_pointer_cast<ITableWorkspace>(table);
250}
251
252VectorPair HeliumAnalyserEfficiency::getTimeDifferences(const std::vector<std::string> &wsNames) {
253 const auto timeDiff = createChildAlgorithm("TimeDifference");
254 timeDiff->initialize();
255 timeDiff->setProperty("InputWorkspaces", wsNames);
256
257 timeDiff->execute();
258
259 const ITableWorkspace_sptr table = timeDiff->getProperty("OutputWorkspace");
260 const auto colThours = table->getColumn("hours");
261 const auto colThoursErr = table->getColumn("hours_error");
262 auto tHours = colThours->numeric_fill();
263 auto tHoursErr = colThoursErr->numeric_fill();
264 return std::make_pair(tHours, tHoursErr);
265}
266
268 const std::vector<std::string> workspaceNames = getProperty(PropertyNames::INPUT_WORKSPACES);
269 const std::string spinConfigurationInput = getProperty(PropertyNames::SPIN_STATES);
270 const double mu = LAMBDA_CONVERSION_FACTOR * static_cast<double>(getProperty(PropertyNames::PXD));
271
272 const auto efficiencies = calculateEfficiencies(workspaceNames, spinConfigurationInput);
273 const auto [pHe, pHeError] = fitHe3Polarization(mu, efficiencies);
274 convertToTheoreticalEfficiencies(efficiencies, pHe, pHeError, mu);
275 if (efficiencies.size() > 1) {
276 const auto [t, tError] = getTimeDifferences(workspaceNames);
277 const auto fitDecayWorkspace = Fitting::createFitDecayWorkspace(t, tError, pHe, pHeError);
278 fitDecayTime(fitDecayWorkspace);
279 } else {
280 logger.notice("Only one input workspace provided, polarization decay can't be fit as it is a 2 parameter fit.");
281 }
282
283 prepareOutputs(efficiencies);
284}
285
286void HeliumAnalyserEfficiency::prepareOutputs(const std::vector<MatrixWorkspace_sptr> &efficiencies) {
287 if (const auto outputCurvesBaseName = getPropertyValue(PropertyNames::OUTPUT_FIT_CURVES);
288 !outputCurvesBaseName.empty()) {
289 setProperty(PropertyNames::OUTPUT_FIT_CURVES, prepareOutputGroup(m_outputCurves, outputCurvesBaseName, "_curves"));
290 }
291 if (const auto outputParamsBaseName = getPropertyValue(PropertyNames::OUTPUT_FIT_PARAMS);
292 !outputParamsBaseName.empty()) {
294 }
296 prepareOutputGroup(efficiencies, getPropertyValue(PropertyNames::OUTPUT_WORKSPACE)));
297}
298
300 const std::vector<MatrixWorkspace_sptr> &efficiencies) {
301 const auto numberOfDomains = efficiencies.size();
302 const auto multiDomainFunc = Fitting::prepareEfficiencyFunc(mu, numberOfDomains);
303 const auto fit = createChildAlgorithm("Fit");
304 fit->initialize();
305 fit->setProperty("Function", multiDomainFunc->asString());
306 fit->setProperty(Fitting::INPUT_WORKSPACE, efficiencies.at(0));
307 for (size_t index = 1; index < numberOfDomains; index++) {
308 fit->setProperty("InputWorkspace_" + std::to_string(index), efficiencies.at(index));
309 }
310
311 fit->setProperty(Fitting::START_X, static_cast<double>(getProperty(Fitting::START_X)));
312 fit->setProperty(Fitting::END_X, static_cast<double>(getProperty(Fitting::END_X)));
314
315 const auto &fitParameters = std::dynamic_pointer_cast<ITableWorkspace>(m_outputParameters.at(0));
316 const auto colPhe = fitParameters->getColumn("Value");
317 const auto colPheError = fitParameters->getColumn("Error");
318 const auto pHe = colPhe->numeric_fill(numberOfDomains);
319 const auto pHeError = colPheError->numeric_fill(numberOfDomains);
320 return std::make_pair(pHe, pHeError);
321}
322
324 const auto fit = createChildAlgorithm("Fit");
325 fit->initialize();
326 const double initialTau = getProperty(PropertyNames::DECAY_TIME_INITIAL);
327 const double initialPol = getProperty(PropertyNames::H3_POLARIZATION_INITIAL);
328 fit->setProperty("Function", Fitting::prepareExpDecayFunction(initialTau, initialPol));
329 fit->setProperty(Fitting::INPUT_WORKSPACE, workspace);
330
332}
333
334void HeliumAnalyserEfficiency::makeFit(const Algorithm_sptr &fitAlgorithm, const std::string &fitOutputName) {
335 const auto extractParameters =
337 const auto extractCurves = !isDefault(PropertyNames::OUTPUT_FIT_CURVES);
338 const bool ignoreFitQualityError = getProperty(PropertyNames::IGNORE_FIT_QUALITY_ERROR);
339
340 fitAlgorithm->setProperty("CreateOutput", extractParameters || extractCurves);
341 fitAlgorithm->setProperty("OutputParametersOnly", !extractCurves);
342
343 fitAlgorithm->execute();
344
345 if (const std::string status = fitAlgorithm->getProperty("OutputStatus");
346 !ignoreFitQualityError && (!fitAlgorithm->isExecuted() || status != "success")) {
347 throw std::runtime_error("Failed to fit to data in the fitting of" + fitOutputName + " : " + status);
348 }
349
350 if (extractParameters) {
351 const ITableWorkspace_sptr fitParameters = fitAlgorithm->getProperty("OutputParameters");
352 m_outputParameters.push_back(fitParameters);
353 }
354
355 if (extractCurves) {
356 // If output is a group, the name of the group will end with `Workspaces`.
357 if (fitAlgorithm->getPropertyValue(PropertyNames::OUTPUT_WORKSPACE).ends_with('s')) {
358 const WorkspaceGroup_sptr &fitCurves = fitAlgorithm->getProperty(PropertyNames::OUTPUT_WORKSPACE);
359 for (int i = 0; i < fitCurves->getNumberOfEntries(); i++) {
360 m_outputCurves.push_back(std::dynamic_pointer_cast<MatrixWorkspace>(fitCurves->getItem(i)));
361 }
362 } else {
363 const MatrixWorkspace_sptr &fitCurve = fitAlgorithm->getProperty(PropertyNames::OUTPUT_WORKSPACE);
364 m_outputCurves.push_back(fitCurve);
365 }
366 }
367}
368
369double HeliumAnalyserEfficiency::calculateTCrit(const size_t numberOfBins) const {
370 // Create a t distribution with degrees of freedom given by the number of data points minus
371 // the number of parameters that were fitted for
372 double tPpf = 1;
373 if (numberOfBins > NUM_FIT_PARAMS) {
374 const boost::math::students_t dist(static_cast<double>(numberOfBins) - static_cast<double>(NUM_FIT_PARAMS));
375 // Critical value corresponding to 1-sigma
376 const double alpha = (1 + std::erf(1.0 / sqrt(2))) / 2;
377 // Scale factor for the error calculations
378 tPpf = boost::math::quantile(dist, alpha);
379 } else {
380 logger.warning("The number of histogram bins must be greater than " + std::to_string(NUM_FIT_PARAMS) +
381 " in order to provide an accurate error calculation");
382 }
383 return tPpf;
384}
385
386std::vector<MatrixWorkspace_sptr>
387HeliumAnalyserEfficiency::calculateEfficiencies(const std::vector<std::string> &workspaceNames,
388 const std::string &spinConfiguration) {
389 std::vector<MatrixWorkspace_sptr> efficiencies(workspaceNames.size());
390 for (size_t index = 0; index < workspaceNames.size(); index++) {
391 const auto inputGroupWs = AnalysisDataService::Instance().retrieveWS<WorkspaceGroup>(workspaceNames.at(index));
392 efficiencies.at(index) = calculateAnalyserEfficiency(inputGroupWs, spinConfiguration);
393 }
394
395 return efficiencies;
396}
397
398void HeliumAnalyserEfficiency::convertToTheoreticalEfficiencies(const std::vector<MatrixWorkspace_sptr> &efficiencies,
399 const std::vector<double> &pHeVec,
400 const std::vector<double> &pHeErrorVec,
401 const double mu) {
402 const double muError = LAMBDA_CONVERSION_FACTOR * static_cast<double>(getProperty(PropertyNames::PXD_ERROR));
403
404 for (size_t index = 0; index < efficiencies.size(); index++) {
405 const auto pHe = pHeVec.at(index);
406 const auto pHeError = pHeErrorVec.at(index);
407 const auto eff = efficiencies.at(index);
408
409 const auto &pointData = eff->histogram(0).points();
410 const auto &binPoints = pointData.rawData();
411 const auto &binBoundaries = eff->x(0);
412
413 auto &theoreticalEff = eff->mutableY(0);
414 auto &efficiencyErrors = eff->mutableE(0);
415
416 // The value tCrit is used to give us the correct error bounds
417 const double tCrit = calculateTCrit(eff->blocksize());
418 for (size_t i = 0; i < binPoints.size(); ++i) {
419 const double lambda = binPoints[i];
420 const double lambdaError = binBoundaries[i + 1] - binBoundaries[i];
421
422 theoreticalEff[i] = (1 + std::tanh(mu * pHe * lambda)) / 2.0;
423
424 // Calculate the errors for the efficiency
425 const auto commonTerm = 0.5 / std::pow(std::cosh(mu * lambda * pHe), 2);
426 const double de_dpHe = mu * commonTerm * lambda;
427 const double de_dmu = pHe * commonTerm * lambda;
428 const double de_dlambda = mu * pHe * commonTerm;
429 // Covariance between p_He and mu is zero
430 efficiencyErrors[i] =
431 tCrit * std::sqrt(de_dpHe * de_dpHe * pHeError * pHeError + de_dmu * de_dmu * muError * muError +
432 de_dlambda * de_dlambda * lambdaError * lambdaError);
433 }
434 }
435}
436} // namespace Mantid::Algorithms
#define DECLARE_ALGORITHM(classname)
Definition Algorithm.h:538
const std::vector< double > * lambda
IPeaksWorkspace_sptr workspace
std::map< DeltaEMode::Type, std::string > index
void declareProperty(std::unique_ptr< Kernel::Property > p, const std::string &doc="") override
Add a property to the list of managed properties.
std::string getPropertyValue(const std::string &name) const override
Get the value of a property as a string.
TypedValue getProperty(const std::string &name) const override
Get the value of a property.
virtual std::shared_ptr< Algorithm > createChildAlgorithm(const std::string &name, const double startProgress=-1., const double endProgress=-1., const bool enableLogging=true, const int &version=-1)
Create a Child Algorithm.
bool isDefault(const std::string &name) const
Class to hold a set of workspaces.
A property class for workspaces.
void fitDecayTime(const MatrixWorkspace_sptr &workspace)
VectorPair getTimeDifferences(const std::vector< std::string > &wsNames)
std::map< std::string, std::string > validateInputs() override
Tests that the inputs are all valid.
void init() override
Virtual method - must be overridden by concrete algorithm.
VectorPair fitHe3Polarization(const double mu, const std::vector< MatrixWorkspace_sptr > &efficiencies)
std::vector< MatrixWorkspace_sptr > calculateEfficiencies(const std::vector< std::string > &workspaceNames, const std::string &spinConfiguration)
ITableWorkspace_sptr prepareOutputTable(const std::vector< ITableWorkspace_sptr > &tables)
void convertToTheoreticalEfficiencies(const std::vector< MatrixWorkspace_sptr > &efficiencies, const std::vector< double > &pHeVec, const std::vector< double > &pHeErrorVec, const double mu)
void exec() override
Virtual method - must be overridden by concrete algorithm.
void prepareOutputs(const std::vector< MatrixWorkspace_sptr > &efficiencies)
std::vector< ITableWorkspace_sptr > m_outputParameters
std::vector< MatrixWorkspace_sptr > m_outputCurves
double calculateTCrit(const size_t numberOfBins) const
void makeFit(const Algorithm_sptr &fitAlgorithm, const std::string &fitOutputName)
Support for a property that holds an array of values.
IPropertyManager * setProperty(const std::string &name, const T &value)
Templated method to set the value of a PropertyWithValue.
void setPropertyGroup(const std::string &name, const std::string &group)
Set the group for a given property.
The Logger class is in charge of the publishing messages from the framework through various channels.
Definition Logger.h:51
std::shared_ptr< WorkspaceGroup > WorkspaceGroup_sptr
shared pointer to Mantid::API::WorkspaceGroup
std::shared_ptr< ITableWorkspace > ITableWorkspace_sptr
shared pointer to Mantid::API::ITableWorkspace
std::shared_ptr< Algorithm > Algorithm_sptr
Typedef for a shared pointer to an Algorithm.
Definition Algorithm.h:52
std::shared_ptr< MatrixWorkspace > MatrixWorkspace_sptr
shared pointer to the matrix workspace base class
std::shared_ptr< IFunction > prepareExpDecayFunction(const double initialDecay, const double initialPolarization)
MatrixWorkspace_sptr createFitDecayWorkspace(const std::vector< double > &time, const std::vector< double > &timeErrors, const std::vector< double > &HePolarization, const std::vector< double > &HePolarizationErrors)
constexpr double PXD_INITIAL
Initial fitting function values.
std::shared_ptr< MultiDomainFunction > prepareEfficiencyFunc(const double mu, const size_t numberOfDomains)
static const std::string OUTPUT_WORKSPACE
constexpr double LAMBDA_CONVERSION_FACTOR
std::pair< std::vector< double >, std::vector< double > > VectorPair
std::shared_ptr< TableWorkspace > TableWorkspace_sptr
shared pointer to Mantid::DataObjects::TableWorkspace
STL namespace.
std::string to_string(const wide_integer< Bits, Signed > &n)
@ Output
An output workspace.
Definition Property.h:54