Mantid
Loading...
Searching...
No Matches
EstimateFitParameters.cpp
Go to the documentation of this file.
1// Mantid Repository : https://github.com/mantidproject/mantid
2//
3// Copyright © 2018 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 +
8
13#include "MantidAPI/TableRow.h"
20
21#include <map>
22#include <numeric>
23#include <random>
24
26
27using namespace Kernel;
28using namespace API;
29
30// Register the algorithm into the AlgorithmFactory
31DECLARE_ALGORITHM(EstimateFitParameters)
32
33namespace {
34
35const size_t MAX_APPROX_SIZE = 129;
36const double MIN_APPROX_TOLERANCE = 1e-4;
37
39double getConstraints(const std::vector<std::unique_ptr<IConstraint>> &constraints) {
40 return std::accumulate(constraints.begin(), constraints.end(), 0.0,
41 [](double s, const std::unique_ptr<IConstraint> &c) { return s + c->check(); });
42}
43
46struct Slice {
47 double operator()(double p) {
48 m_costFunction.setParameter(m_paramIndex, p);
49 m_costFunction.applyTies();
50 return m_costFunction.val();
51 }
53 CostFunctions::CostFuncFitting &m_costFunction;
56};
57
63void fixBadParameters(CostFunctions::CostFuncFitting &costFunction,
64 const std::vector<std::pair<double, double>> &ranges) {
65 std::vector<size_t> indicesOfFixed;
66 std::vector<double> P, A, D;
67 auto &fun = *costFunction.getFittingFunction();
68 for (size_t i = 0, j = 0; i < fun.nParams(); ++i) {
69 if (!fun.isActive(i)) {
70 continue;
71 }
72 auto lBound = ranges[j].first;
73 auto rBound = ranges[j].second;
74 auto storedParam = fun.getParameter(i);
75 Slice slice{costFunction, j};
76 auto base = Functions::ChebfunBase::bestFitAnyTolerance(lBound, rBound, slice, P, A, 1.0, MIN_APPROX_TOLERANCE,
77 MAX_APPROX_SIZE);
78 bool fix = false;
79 if (!base) {
80 fun.setParameter(i, storedParam);
81 base = std::make_shared<Functions::ChebfunBase>(MAX_APPROX_SIZE, lBound, rBound, MIN_APPROX_TOLERANCE);
82 P = base->fit(slice);
83 A = base->calcA(P);
84 }
85 base->derivative(A, D);
86 auto roots = base->roots(D);
87 if (!roots.empty()) {
88 if (roots.size() * 2 >= base->size()) {
89 // If the approximating polynomial oscillates too much
90 // the parameter is likely to be bad.
91 fix = true;
92 }
93 }
94 fun.setParameter(i, storedParam);
95
96 if (fix) {
97 // Parameter is bad - fix it. Delay actual fixing until all bad ones
98 // found.
99 indicesOfFixed.emplace_back(i);
100 }
101 ++j;
102 }
103 for (auto i : indicesOfFixed) {
104 fun.tie(fun.parameterName(i), std::to_string(fun.getParameter(i)));
105 }
106}
107
109class BestParameters {
111 size_t m_size;
113 std::map<double, EigenVector> m_params;
114
115public:
117 explicit BestParameters(size_t size) : m_size(size) {}
119 bool isOneOfBest(double value) const { return m_params.size() < m_size || value < m_params.rbegin()->first; }
121 void insertParams(double value, const EigenVector &params) {
122 if (m_params.size() == m_size) {
123 auto it = m_params.find(m_params.rbegin()->first);
124 m_params.erase(it);
125 }
126 m_params[value] = params;
127 }
129 std::vector<EigenVector> getParams() const {
130 std::vector<EigenVector> res;
131 res.reserve(m_params.size());
132 std::transform(m_params.begin(), m_params.end(), std::back_inserter(res), [](const auto &it) { return it.second; });
133 return res;
134 }
135};
136
146std::vector<EigenVector> runMonteCarlo(CostFunctions::CostFuncFitting &costFunction,
147 const std::vector<std::pair<double, double>> &ranges,
148 const std::vector<std::unique_ptr<IConstraint>> &constraints,
149 const size_t nSamples, const size_t nOutput, const unsigned int seed) {
150 std::mt19937 rng;
151 if (seed != 0) {
152 rng.seed(seed);
153 }
154 double value = costFunction.val() + getConstraints(constraints);
155 auto nParams = costFunction.nParams();
156 EigenVector params;
157 costFunction.getParameters(params);
158 BestParameters bestParams(nOutput);
159 bestParams.insertParams(value, params);
160
161 // Implicit cast from int to size_t
162 for (size_t it = 0; it < nSamples; ++it) {
163 for (size_t i = 0; i < nParams; ++i) {
164 const auto &range = ranges[i];
165 costFunction.setParameter(i, std::uniform_real_distribution<>(range.first, range.second)(rng));
166 }
167 costFunction.applyTies();
168 if (getConstraints(constraints) > 0.0) {
169 continue;
170 }
171 value = costFunction.val();
172 if (costFunction.nParams() != nParams) {
173 throw std::runtime_error("Cost function changed number of parameters " + std::to_string(nParams) + " -> " +
174 std::to_string(costFunction.nParams()));
175 }
176 if (bestParams.isOneOfBest(value)) {
177 costFunction.getParameters(params);
178 bestParams.insertParams(value, params);
179 }
180 }
181 auto outputParams = bestParams.getParams();
182 costFunction.setParameters(outputParams.front());
183 return outputParams;
184}
208void runCrossEntropy(CostFunctions::CostFuncFitting &costFunction, const std::vector<std::pair<double, double>> &ranges,
209 const std::vector<std::unique_ptr<IConstraint>> &constraints, size_t nSamples, size_t nSelection,
210 size_t nIterations, unsigned int seed) {
211 // Initialise the normal distribution parameters (mean and sigma for each
212 // function parameter).
213 std::vector<std::pair<double, double>> distributionParams;
214 for (auto &range : ranges) {
215 auto mean = (range.first + range.second) / 2;
216 auto sigma = std::fabs(range.first - range.second) / 2;
217 distributionParams.emplace_back(mean, sigma);
218 }
219
220 auto nParams = costFunction.nParams();
221 std::mt19937 rng;
222 if (seed != 0) {
223 rng.seed(seed);
224 }
225 // Sets of function parameters (EigenVector) and corresponding values of the
226 // cost function (double)
227 using ParameterSet = std::pair<double, EigenVector>;
228 std::vector<ParameterSet> sampleSets(nSamples);
229 // Function for comparing parameter sets.
230 auto compareSets = [](const ParameterSet &p1, const ParameterSet &p2) { return p1.first < p2.first; };
231
232 // Run nIterations of the algorithm
233 for (size_t it = 0; it < nIterations; ++it) {
234 for (size_t isam = 0; isam < nSamples; ++isam) {
235 // Draw a set of function parameters from their distributions
236 auto &paramSet = sampleSets[isam];
237 if (paramSet.second.size() != nParams) {
238 paramSet.second.resize(nParams);
239 }
240 for (size_t i = 0; i < nParams; ++i) {
241 paramSet.second[i] =
242 Kernel::normal_distribution<>(distributionParams[i].first, distributionParams[i].second)(rng);
243 }
244 // Calculate the cost function with those parameters
245 costFunction.setParameters(paramSet.second);
246 paramSet.first = costFunction.val() + getConstraints(constraints);
247 if (costFunction.nParams() != nParams) {
248 throw std::runtime_error("Cost function changed number of parameters " + std::to_string(nParams) + " -> " +
249 std::to_string(costFunction.nParams()));
250 }
251 }
252 // Partially sort the parameter sets in ascending order of the cost
253 // function.
254 // Find nSelection smallest values.
255 std::partial_sort(sampleSets.begin(), sampleSets.begin() + nSelection, sampleSets.end(), compareSets);
256 // Estimate new distribution parameters from the sample of nSelection sets.
257 EigenVector means(nParams);
258 EigenVector variances(nParams);
259 for (size_t isam = 0; isam < nSelection; ++isam) {
260 auto &paramSet = sampleSets[isam];
261 for (size_t i = 0; i < nParams; ++i) {
262 auto p = paramSet.second[i];
263 means[i] += p;
264 variances[i] += p * p;
265 }
266 }
267 means *= 1.0 / double(nSelection);
268 variances *= 1.0 / double(nSelection);
269 for (size_t i = 0; i < nParams; ++i) {
270 auto mean = means[i];
271 auto sigma = sqrt(variances[i] - mean * mean);
272 distributionParams[i].first = mean;
273 distributionParams[i].second = sigma;
274 }
275 }
276 // Set parameters of the cost function to the best sample set.
277 costFunction.setParameters(sampleSets.front().second);
278}
279
280} // namespace
281
282//----------------------------------------------------------------------------------------------
283
285const std::string EstimateFitParameters::name() const { return "EstimateFitParameters"; }
286
288int EstimateFitParameters::version() const { return 1; }
289
291const std::string EstimateFitParameters::summary() const {
292 return "Estimate parameters of a fitting function using a Monte Carlo "
293 "algorithm.";
294}
295
296//----------------------------------------------------------------------------------------------
298void EstimateFitParameters::initConcrete() {
299 std::vector<std::string> types{"Monte Carlo", "Cross Entropy"};
300 Kernel::StringListValidator TypesValidator(types);
301
303 declareProperty("NSamples", 100, "Number of samples.");
304 declareProperty("Constraints", "", "Additional constraints on tied parameters.");
305 declareProperty("Type", "Monte Carlo", R"(Type of the algorithm: "Monte Carlo" or "Cross Entropy")");
306 declareProperty("NOutputs", 10,
307 "Number of parameter sets to output to "
308 "OutputWorkspace. Unused if OutputWorkspace "
309 "isn't set. (Monte Carlo only)");
310 declareProperty(std::make_unique<WorkspaceProperty<ITableWorkspace>>("OutputWorkspace", "", Direction::Output,
312 "Optional: A table workspace with parameter sets producing "
313 "the smallest values of cost function. (Monte Carlo only)");
314 declareProperty("NIterations", 10, "Number of iterations of the Cross Entropy algorithm.");
315 declareProperty("Selection", 10,
316 "Size of the selection in the Cross Entropy "
317 "algorithm from which to estimate new "
318 "distribution parameters for the next "
319 "iteration.");
320 declareProperty("FixBadParameters", false,
321 "If true try to estimate which "
322 "parameters may cause problems "
323 "for fitting and fix them.");
324 declareProperty("Seed", 0,
325 "A seed value for the random number generator. "
326 "The default value (0) makes the generator use a "
327 "random seed.");
328}
329
330//----------------------------------------------------------------------------------------------
332void EstimateFitParameters::execConcrete() {
333 auto costFunction = getCostFunctionInitialized();
334 auto func = costFunction->getFittingFunction();
335
336 // Use additional constraints on parameters tied in some way
337 // to the varied parameters to exculde unwanted results.
338 std::vector<std::unique_ptr<IConstraint>> constraints;
339 std::string constraintStr = getProperty("Constraints");
340 if (!constraintStr.empty()) {
341 Expression expr;
342 expr.parse(constraintStr);
343 expr.toList();
344 for (auto &term : expr.terms()) {
345 IConstraint *c = ConstraintFactory::Instance().createInitialized(func.get(), term);
346 constraints.emplace_back(std::unique_ptr<IConstraint>(c));
347 }
348 }
349
350 // Ranges to use with random number generators: one for each free parameter.
351 std::vector<std::pair<double, double>> ranges;
352 ranges.reserve(costFunction->nParams());
353 for (size_t i = 0; i < func->nParams(); ++i) {
354 if (!func->isActive(i)) {
355 continue;
356 }
357 auto constraint = func->getConstraint(i);
358 if (!constraint) {
359 func->fix(i);
360 continue;
361 }
362 auto boundary = dynamic_cast<Constraints::BoundaryConstraint *>(constraint);
363 if (!boundary) {
364 throw std::runtime_error("Parameter " + func->parameterName(i) + " must have a boundary constraint. ");
365 }
366 if (!boundary->hasLower()) {
367 throw std::runtime_error("Constraint of " + func->parameterName(i) + " must have a lower bound.");
368 }
369 if (!boundary->hasUpper()) {
370 throw std::runtime_error("Constraint of " + func->parameterName(i) + " must have an upper bound.");
371 }
372 // Use the lower and upper bounds of the constraint to set the range
373 // of a generator with uniform distribution.
374 ranges.emplace_back(boundary->lower(), boundary->upper());
375 }
376 // Number of parameters could have changed
377 costFunction->reset();
378 if (costFunction->nParams() == 0) {
379 throw std::runtime_error("No parameters are given for which to estimate "
380 "initial values. Set boundary constraints to "
381 "parameters that need to be estimated.");
382 }
383
384 size_t nSamples = static_cast<int>(getProperty("NSamples"));
385 unsigned int seed = static_cast<int>(getProperty("Seed"));
386
387 if (getPropertyValue("Type") == "Monte Carlo") {
388 int nOutput = getProperty("NOutputs");
389 auto outputWorkspaceProp = getPointerToProperty("OutputWorkspace");
390 if (outputWorkspaceProp->isDefault() || nOutput <= 0) {
391 nOutput = 1;
392 }
393 auto output = runMonteCarlo(*costFunction, ranges, constraints, nSamples, static_cast<size_t>(nOutput), seed);
394
395 if (!outputWorkspaceProp->isDefault()) {
396 auto table = API::WorkspaceFactory::Instance().createTable();
397 auto column = table->addColumn("str", "Name");
398 column->setPlotType(6);
399 for (size_t i = 0; i < output.size(); ++i) {
400 column = table->addColumn("double", std::to_string(i + 1));
401 column->setPlotType(2);
402 }
403
404 for (size_t i = 0, ia = 0; i < m_function->nParams(); ++i) {
405 if (m_function->isActive(i)) {
406 TableRow row = table->appendRow();
407 row << m_function->parameterName(i);
408 for (auto &j : output) {
409 row << j[ia];
410 }
411 ++ia;
412 }
413 }
414 setProperty("OutputWorkspace", table);
415 }
416 } else {
417 size_t nSelection = static_cast<int>(getProperty("Selection"));
418 size_t nIterations = static_cast<int>(getProperty("NIterations"));
419 runCrossEntropy(*costFunction, ranges, constraints, nSamples, nSelection, nIterations, seed);
420 }
421 bool fixBad = getProperty("FixBadParameters");
422 if (fixBad) {
423 fixBadParameters(*costFunction, ranges);
424 }
425}
426
427} // namespace Mantid::CurveFitting::Algorithms
#define DECLARE_ALGORITHM(classname)
Definition: Algorithm.h:576
size_t m_size
Maximum size of the store.
size_t m_paramIndex
Index of the running parameter.
std::map< double, EigenVector > m_params
Actual storage.
CostFunctions::CostFuncFitting & m_costFunction
The cost function.
double value
The value of the point.
Definition: FitMW.cpp:51
double sigma
Definition: GetAllEi.cpp:156
void declareProperty(std::unique_ptr< Kernel::Property > p, const std::string &doc="") override
Add a property to the list of managed properties.
Definition: Algorithm.cpp:1913
Kernel::Property * getPointerToProperty(const std::string &name) const override
Get a property by name.
Definition: Algorithm.cpp:2033
std::string getPropertyValue(const std::string &name) const override
Get the value of a property as a string.
Definition: Algorithm.cpp:2026
TypedValue getProperty(const std::string &name) const override
Get the value of a property.
Definition: Algorithm.cpp:2076
This class represents an expression made up of names, binary operators and brackets.
Definition: Expression.h:36
const std::vector< Expression > & terms() const
Returns the top level terms of the expression (function arguments).
Definition: Expression.h:77
void parse(const std::string &str)
Parse a string and create an expression.
Definition: Expression.cpp:159
void toList(const std::string &sep=",")
Make sure the expression is a list of expression separated by sep, eg "term1,term2,...
Definition: Expression.cpp:559
An interface to a constraint.
Definition: IConstraint.h:26
TableRow represents a row in a TableWorkspace.
Definition: TableRow.h:39
A property class for workspaces.
A boundary constraint is designed to be used to set either upper or lower (or both) boundaries on a s...
void declareCostFunctionProperty()
Declare a "CostFunction" property.
std::shared_ptr< CostFunctions::CostFuncFitting > getCostFunctionInitialized() const
Create a cost function from the "CostFunction" property and make it ready for evaluation.
std::shared_ptr< API::IFunction > m_function
Pointer to the fitting function.
IPropertyManager * setProperty(const std::string &name, const T &value)
Templated method to set the value of a PropertyWithValue.
ListValidator is a validator that requires the value of a property to be one of a defined list of pos...
Definition: ListValidator.h:29
static T & Instance()
Return a reference to the Singleton instance, creating it if it does not already exist Creation is do...
std::string to_string(const wide_integer< Bits, Signed > &n)
@ Output
An output workspace.
Definition: Property.h:54