diff --git a/Framework/CurveFitting/CMakeLists.txt b/Framework/CurveFitting/CMakeLists.txt index 88ca5fabe5d005b2a18387f90c80d30dcaebab98..57f22e7576c99d51c7daaf1c3f0dd972f4d17ef7 100644 --- a/Framework/CurveFitting/CMakeLists.txt +++ b/Framework/CurveFitting/CMakeLists.txt @@ -19,6 +19,7 @@ set(SRC_FILES src/Algorithms/PawleyFit.cpp src/Algorithms/PlotPeakByLogValue.cpp src/Algorithms/PlotPeakByLogValueHelper.cpp + src/Algorithms/ProfileChiSquared1D.cpp src/Algorithms/QENSFitSequential.cpp src/Algorithms/QENSFitSimultaneous.cpp src/Algorithms/QENSFitUtilities.cpp @@ -187,6 +188,7 @@ set(INC_FILES inc/MantidCurveFitting/Algorithms/PawleyFit.h inc/MantidCurveFitting/Algorithms/PlotPeakByLogValue.h inc/MantidCurveFitting/Algorithms/PlotPeakByLogValueHelper.h + inc/MantidCurveFitting/Algorithms/ProfileChiSquared1D.h inc/MantidCurveFitting/Algorithms/QENSFitSequential.h inc/MantidCurveFitting/Algorithms/QENSFitSimultaneous.h inc/MantidCurveFitting/Algorithms/QENSFitUtilities.h @@ -351,6 +353,7 @@ set(TEST_FILES Algorithms/PawleyFitTest.h Algorithms/PlotPeakByLogValueTest.h Algorithms/PlotPeakByLogValueHelperTest.h + Algorithms/ProfileChiSquared1DTest.h Algorithms/QENSFitSequentialTest.h Algorithms/QENSFitSimultaneousTest.h Algorithms/RefinePowderInstrumentParameters3Test.h diff --git a/Framework/CurveFitting/inc/MantidCurveFitting/Algorithms/CalculateChiSquared.h b/Framework/CurveFitting/inc/MantidCurveFitting/Algorithms/CalculateChiSquared.h index bdf47c12734e9a43646f7db517acf3e9986c0b5e..31ea00716bcee331032d846844ca802ff92fa618 100644 --- a/Framework/CurveFitting/inc/MantidCurveFitting/Algorithms/CalculateChiSquared.h +++ b/Framework/CurveFitting/inc/MantidCurveFitting/Algorithms/CalculateChiSquared.h @@ -6,6 +6,7 @@ // SPDX - License - Identifier: GPL - 3.0 + #pragma once +#include "MantidAPI/IFunction_fwd.h" #include "MantidCurveFitting/IFittingAlgorithm.h" #include "MantidKernel/System.h" @@ -14,29 +15,24 @@ namespace CurveFitting { namespace Algorithms { /** - Calculate chi squared for a function and a data set in a workspace. - Optionally outputs slices of the chi^2 along the parameter axes - and estimates the standard deviations. */ class MANTID_CURVEFITTING_DLL CalculateChiSquared : public IFittingAlgorithm { public: const std::string name() const override; int version() const override; const std::vector<std::string> seeAlso() const override { - return {"CalculateCostFunction", "Fit"}; + return {"CalculateCostFunction", "Fit", "ProfileChiSquared1D"}; } const std::string summary() const override; + static void calcChiSquared(const API::IFunction &fun, size_t nParams, + const API::FunctionDomain &domain, + API::FunctionValues &values, double &chiSquared, + double &chiSquaredWeighted, double &dof); private: void initConcrete() override; void execConcrete() override; - void estimateErrors(); - void unfixParameters(); - void refixParameters(); - - /// Cache indices of fixed parameters - std::vector<size_t> m_fixedParameters; }; } // namespace Algorithms diff --git a/Framework/CurveFitting/inc/MantidCurveFitting/Algorithms/ProfileChiSquared1D.h b/Framework/CurveFitting/inc/MantidCurveFitting/Algorithms/ProfileChiSquared1D.h new file mode 100644 index 0000000000000000000000000000000000000000..762995aff93a7e6e16f97258e7950bb2e8cb116a --- /dev/null +++ b/Framework/CurveFitting/inc/MantidCurveFitting/Algorithms/ProfileChiSquared1D.h @@ -0,0 +1,47 @@ +// Mantid Repository : https://github.com/mantidproject/mantid +// +// Copyright © 2021 ISIS Rutherford Appleton Laboratory UKRI, +// NScD Oak Ridge National Laboratory, European Spallation Source, +// Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS +// SPDX - License - Identifier: GPL - 3.0 + +#pragma once + +#include "MantidCurveFitting/Functions/ChebfunBase.h" +#include "MantidCurveFitting/GSLMatrix.h" +#include "MantidCurveFitting/IFittingAlgorithm.h" +#include "MantidKernel/System.h" + +namespace Mantid { +namespace CurveFitting { +namespace Algorithms { + +/** +Profiles chi2 about its minimum to find parameter errors +*/ +class MANTID_CURVEFITTING_DLL ProfileChiSquared1D : public IFittingAlgorithm { +public: + ProfileChiSquared1D(); + const std::string name() const override; + int version() const override; + const std::vector<std::string> seeAlso() const override { + return {"CalculateChiSquared", "Fit"}; + } + const std::string summary() const override; + +private: + void initConcrete() override; + void execConcrete() override; + void unfixParameters(); + void refixParameters(); + GSLMatrix getCovarianceMatrix(); + std::tuple<double, double> + getChiSquaredRoots(const Functions::ChebfunBase_sptr &approximation, + std::vector<double> &coeffs, double qvalue, double rBound, + double lBound) const; + /// Cache indices of fixed parameters + std::vector<size_t> m_fixedParameters; +}; + +} // namespace Algorithms +} // namespace CurveFitting +} // namespace Mantid diff --git a/Framework/CurveFitting/src/Algorithms/CalculateChiSquared.cpp b/Framework/CurveFitting/src/Algorithms/CalculateChiSquared.cpp index 290ed48e4923513773e12b95e32526b2d45ac834..26812a161aa0390a24c5bf576ec56aa41cfa76ec 100644 --- a/Framework/CurveFitting/src/Algorithms/CalculateChiSquared.cpp +++ b/Framework/CurveFitting/src/Algorithms/CalculateChiSquared.cpp @@ -5,12 +5,6 @@ // Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS // SPDX - License - Identifier: GPL - 3.0 + #include "MantidCurveFitting/Algorithms/CalculateChiSquared.h" -#include "MantidAPI/Column.h" -#include "MantidAPI/ITableWorkspace.h" -#include "MantidAPI/TableRow.h" -#include "MantidAPI/WorkspaceFactory.h" -#include "MantidCurveFitting/Functions/ChebfunBase.h" -#include "MantidCurveFitting/GSLJacobian.h" namespace Mantid { namespace CurveFitting { @@ -18,51 +12,10 @@ namespace Algorithms { using namespace Kernel; using namespace API; -using namespace Functions; // Register the algorithm into the AlgorithmFactory DECLARE_ALGORITHM(CalculateChiSquared) -//---------------------------------------------------------------------------------------------- -namespace { - -/// Caclculate the chi squared, weighted chi squared and the number of degrees -/// of freedom. -/// @param domain :: Function's domain. -/// @param nParams :: Number of free fitting parameters. -/// @param values :: Functin's values. -/// @param chi0 :: Chi squared at the minimum. -/// @param sigma2 :: Estimated variance of the fitted data. -void calcChiSquared(const API::IFunction &fun, size_t nParams, - const API::FunctionDomain &domain, - API::FunctionValues &values, double &chiSquared, - double &chiSquaredWeighted, double &dof) { - - // Calculate function values. - fun.function(domain, values); - - // Calculate the chi squared. - chiSquared = 0.0; - chiSquaredWeighted = 0.0; - dof = -static_cast<double>(nParams); - for (size_t i = 0; i < values.size(); ++i) { - auto weight = values.getFitWeight(i); - if (weight > 0.0) { - double tmp = values.getFitData(i) - values.getCalculated(i); - chiSquared += tmp * tmp; - tmp *= weight; - chiSquaredWeighted += tmp * tmp; - dof += 1.0; - } - } - if (dof <= 0.0) { - dof = 1.0; - } -} -} // namespace - -//---------------------------------------------------------------------------------------------- - /// Algorithms name for identification. @see Algorithm::name const std::string CalculateChiSquared::name() const { return "CalculateChiSquared"; @@ -101,7 +54,6 @@ void CalculateChiSquared::initConcrete() { "Output value of weighted chi squared divided by the " "number of data points).", Direction::Output); - declareProperty("Output", "", "A base name for output workspaces."); declareProperty("Weighted", false, "Option to use the weighted chi squared " "in error estimation. Default is false."); @@ -172,512 +124,43 @@ void CalculateChiSquared::execConcrete() { // Store the result. setProperty("ChiSquaredDividedByDOF", chiSquaredDOF); setProperty("ChiSquaredWeightedDividedByDOF", chiSquaredWeightedDOF); - - std::string baseName = getProperty("Output"); - if (!baseName.empty()) { - estimateErrors(); - } } //---------------------------------------------------------------------------------------------- -namespace { -/// Calculate the negative logarithm of the probability density function (PDF): -/// if a = getDiff(...) then exp(-a) is a value of the PDF. -/// @param domain :: Function's domain. +/// Caclculate the chi squared, weighted chi squared and the number of degrees +/// of freedom. +/// @param fun :: Function's domain. /// @param nParams :: Number of free fitting parameters. -/// @param values :: Functin's values. -/// @param chi0 :: Chi squared at the minimum. -/// @param sigma2 :: Estimated variance of the fitted data. -double getDiff(const API::IFunction &fun, size_t nParams, - const API::FunctionDomain &domain, API::FunctionValues &values, - double chi0, double sigma2) { - double chiSquared = 0.0; - double chiSquaredWeighted = 0.0; - double dof = 0; - calcChiSquared(fun, nParams, domain, values, chiSquared, chiSquaredWeighted, - dof); - double res = 0.0; - if (sigma2 > 0) { - res = (chiSquared - chi0) / 2 / sigma2; - } else { - res = (chiSquaredWeighted - chi0) / 2; - } - return res; -} - -/// Helper class to calculate the chi squared along a direction in the parameter -/// space. -class ChiSlice { -public: - /// Constructor. - /// @param f :: The fitting function - /// @param dir :: A normalised direction vector in the parameter space. - /// @param domain :: Function's domain. - /// @param values :: Functin's values. - /// @param chi0 :: Chi squared at the minimum. - /// @param sigma2 :: Estimated variance of the fitted data. - ChiSlice(IFunction &f, const GSLVector &dir, - const API::FunctionDomain &domain, API::FunctionValues &values, - double chi0, double sigma2) - : m_function(f), m_direction(dir), m_domain(domain), m_values(values), - m_chi0(chi0), m_sigma2(sigma2) {} - /// Calculate the value of chi squared along the chosen direction at a - /// distance from - /// the minimum point. - /// @param p :: A distance from the minimum. - double operator()(double p) { - std::vector<double> par0(m_function.nParams()); - for (size_t ip = 0; ip < m_function.nParams(); ++ip) { - par0[ip] = m_function.getParameter(ip); - m_function.setParameter(ip, par0[ip] + p * m_direction[ip]); - } - double res = getDiff(m_function, m_function.nParams(), m_domain, m_values, - m_chi0, m_sigma2); - for (size_t ip = 0; ip < m_function.nParams(); ++ip) { - m_function.setParameter(ip, par0[ip]); - } - return res; - } - - /// Make an approximation for this slice on an interval. - /// @param lBound :: The left bound of the approximation interval. - /// @param rBound :: The right bound of the approximation interval. - /// @param P :: Output vector with approximation parameters. - /// @param A :: Output vector with approximation parameters. - ChebfunBase_sptr makeApprox(double lBound, double rBound, - std::vector<double> &P, std::vector<double> &A, - bool &ok) { - - auto base = ChebfunBase::bestFitAnyTolerance(lBound, rBound, *this, P, A, - 1.0, 1e-4, 129); - ok = bool(base); - if (!base) { - base = std::make_shared<ChebfunBase>(10, lBound, rBound, 1e-4); - P = base->fit(*this); - A = base->calcA(P); - } - return base; - } - - /// Fiind a displacement in the parameter space from the initial point - /// to a point where the PDF drops significantly. - /// @param shift :: Initial shift form par0 value. - double findBound(double shift) { - double bound0 = 0; - double diff0 = (*this)(0); - double bound = shift; - bool canDecrease = true; - for (size_t i = 0; i < 100; ++i) { - double diff = (*this)(bound); - - bool isIncreasing = fabs(bound) > fabs(bound0) && diff > diff0; - if (canDecrease) { - if (isIncreasing) - canDecrease = false; - } else { - if (!isIncreasing) { - bound = bound0; - break; - } - } - - bound0 = bound; - diff0 = diff; - - if (diff > 3.0) { - if (diff < 4.0) { - break; - } - // diff is too large - bound *= 0.75; - } else { - // diff is too small - bound *= 2; - } - } - return bound; - } - -private: - /// The fitting function - IFunction &m_function; - /// The direction in the parameter space - GSLVector m_direction; - /// The domain - const API::FunctionDomain &m_domain; - /// The values - API::FunctionValues &m_values; - /// The chi squared at the minimum - double m_chi0; - /// The data variance. - double m_sigma2; -}; -} // namespace - -//---------------------------------------------------------------------------------------------- -/// Examine the chi squared as a function of fitting parameters and estimate -/// errors for each parameter. -void CalculateChiSquared::estimateErrors() { - // Number of fiting parameters - auto nParams = m_function->nParams(); - // Create an output table for displaying slices of the chi squared and - // the probabilitydensity function - auto pdfTable = API::WorkspaceFactory::Instance().createTable(); - - std::string baseName = getProperty("Output"); - if (baseName.empty()) { - baseName = "CalculateChiSquared"; - } - declareProperty( - std::make_unique<API::WorkspaceProperty<API::ITableWorkspace>>( - "PDFs", "", Kernel::Direction::Output), - "The name of the TableWorkspace in which to store the " - "pdfs of fit parameters"); - setPropertyValue("PDFs", baseName + "_pdf"); - setProperty("PDFs", pdfTable); - - // Create an output table for displaying the parameter errors. - auto errorsTable = API::WorkspaceFactory::Instance().createTable(); - auto nameColumn = errorsTable->addColumn("str", "Parameter"); - auto valueColumn = errorsTable->addColumn("double", "Value"); - auto minValueColumn = errorsTable->addColumn("double", "Value at Min"); - auto leftErrColumn = errorsTable->addColumn("double", "Left Error"); - auto rightErrColumn = errorsTable->addColumn("double", "Right Error"); - auto quadraticErrColumn = errorsTable->addColumn("double", "Quadratic Error"); - auto chiMinColumn = errorsTable->addColumn("double", "Chi2 Min"); - errorsTable->setRowCount(nParams); - declareProperty( - std::make_unique<API::WorkspaceProperty<API::ITableWorkspace>>( - "Errors", "", Kernel::Direction::Output), - "The name of the TableWorkspace in which to store the " - "values and errors of fit parameters"); - setPropertyValue("Errors", baseName + "_errors"); - setProperty("Errors", errorsTable); - - // Calculate initial values - double chiSquared = 0.0; - double chiSquaredWeighted = 0.0; - double dof = 0; - API::FunctionDomain_sptr domain; - API::FunctionValues_sptr values; - m_domainCreator->createDomain(domain, values); - calcChiSquared(*m_function, nParams, *domain, *values, chiSquared, - chiSquaredWeighted, dof); - // Value of chi squared for current parameters in m_function - double chi0 = chiSquared; - // Fit data variance - double sigma2 = chiSquared / dof; - bool useWeighted = getProperty("Weighted"); +/// @param domain :: Function's domain +/// @param values :: Function's values +/// @param chiSquared :: unweighted chi squared +/// @param chiSquaredWeighted :: weighted chi squared +/// @param dof :: degrees of freedom +void CalculateChiSquared::calcChiSquared( + const API::IFunction &fun, size_t nParams, + const API::FunctionDomain &domain, API::FunctionValues &values, + double &chiSquared, double &chiSquaredWeighted, double &dof) { - if (useWeighted) { - chi0 = chiSquaredWeighted; - sigma2 = 0.0; - } - - if (g_log.is(Kernel::Logger::Priority::PRIO_DEBUG)) { - g_log.debug() << "chi0=" << chi0 << '\n'; - g_log.debug() << "sigma2=" << sigma2 << '\n'; - g_log.debug() << "dof=" << dof << '\n'; - } - - // Parameter bounds that define a volume in the parameter - // space within which the chi squared is being examined. - GSLVector lBounds(nParams); - GSLVector rBounds(nParams); - - // Number of points in lines for plotting - size_t n = 100; - pdfTable->setRowCount(n); - const double fac = 1e-4; - - // Loop over each parameter - for (size_t ip = 0; ip < nParams; ++ip) { - - // Add columns for the parameter to the pdf table. - auto parName = m_function->parameterName(ip); - nameColumn->read(ip, parName); - // Parameter values - auto col1 = pdfTable->addColumn("double", parName); - col1->setPlotType(1); - // Chi squared values - auto col2 = pdfTable->addColumn("double", parName + "_chi2"); - col2->setPlotType(2); - // PDF values - auto col3 = pdfTable->addColumn("double", parName + "_pdf"); - col3->setPlotType(2); - - double par0 = m_function->getParameter(ip); - double shift = fabs(par0 * fac); - if (shift == 0.0) { - shift = fac; - } - - // Make a slice along this parameter - GSLVector dir(nParams); - dir.zero(); - dir[ip] = 1.0; - ChiSlice slice(*m_function, dir, *domain, *values, chi0, sigma2); - - // Find the bounds withn which the PDF is significantly above zero. - // The bounds are defined relative to par0: - // par0 + lBound is the lowest value of the parameter (lBound <= 0) - // par0 + rBound is the highest value of the parameter (rBound >= 0) - double lBound = slice.findBound(-shift); - double rBound = slice.findBound(shift); - lBounds[ip] = lBound; - rBounds[ip] = rBound; - - // Approximate the slice with a polynomial. - // P is a vector of values of the polynomial at special points. - // A is a vector of Chebyshev expansion coefficients. - // The polynomial is defined on interval [lBound, rBound] - // The value of the polynomial at 0 == chi squared at par0 - std::vector<double> P, A; - bool ok = true; - auto base = slice.makeApprox(lBound, rBound, P, A, ok); - if (!ok) { - g_log.warning() << "Approximation failed for parameter " << ip << '\n'; - } - if (g_log.is(Kernel::Logger::Priority::PRIO_DEBUG)) { - g_log.debug() << "Parameter " << ip << '\n'; - g_log.debug() << "Slice approximated by polynomial of order " - << base->size() - 1; - g_log.debug() << " between " << lBound << " and " << rBound << '\n'; - } - - // Write n slice points into the output table. - double dp = (rBound - lBound) / static_cast<double>(n); - for (size_t i = 0; i < n; ++i) { - double par = lBound + dp * static_cast<double>(i); - double chi = base->eval(par, P); - col1->fromDouble(i, par0 + par); - col2->fromDouble(i, chi); - } - - // Check if par0 is a minimum point of the chi squared - std::vector<double> AD; - // Calculate the derivative polynomial. - // AD are the Chebyshev expansion of the derivative. - base->derivative(A, AD); - // Find the roots of the derivative polynomial - std::vector<double> minima = base->roots(AD); - if (minima.empty()) { - minima.emplace_back(par0); - } - - if (g_log.is(Kernel::Logger::Priority::PRIO_DEBUG)) { - g_log.debug() << "Minima: "; - } - - // If only 1 extremum is found assume (without checking) that it's a - // minimum. - // If there are more than 1, find the one with the smallest chi^2. - double chiMin = std::numeric_limits<double>::max(); - double parMin = par0; - for (double minimum : minima) { - double value = base->eval(minimum, P); - if (g_log.is(Kernel::Logger::Priority::PRIO_DEBUG)) { - g_log.debug() << minimum << " (" << value << ") "; - } - if (value < chiMin) { - chiMin = value; - parMin = minimum; - } - } - if (g_log.is(Kernel::Logger::Priority::PRIO_DEBUG)) { - g_log.debug() << '\n'; - g_log.debug() << "Smallest minimum at " << parMin << " is " << chiMin - << '\n'; - } - - // Points of intersections with line chi^2 = 1/2 give an estimate of - // the standard deviation of this parameter if it's uncorrelated with the - // others. - A[0] -= 0.5; // Now A are the coefficients of the original polynomial - // shifted down by 1/2. - std::vector<double> roots = base->roots(A); - std::sort(roots.begin(), roots.end()); - - if (roots.empty()) { - // Something went wrong; use the whole interval. - roots.resize(2); - roots[0] = lBound; - roots[1] = rBound; - } else if (roots.size() == 1) { - // Only one root found; use a bound for the other root. - if (roots.front() < 0) { - roots.emplace_back(rBound); - } else { - roots.insert(roots.begin(), lBound); - } - } else if (roots.size() > 2) { - // More than 2 roots; use the smallest and the biggest - auto smallest = roots.front(); - auto biggest = roots.back(); - roots.resize(2); - roots[0] = smallest; - roots[1] = biggest; - } - - if (g_log.is(Kernel::Logger::Priority::PRIO_DEBUG)) { - g_log.debug() << "Roots: "; - for (double root : roots) { - g_log.debug() << root << ' '; - } - g_log.debug() << '\n'; - } - - // Output parameter info to the table. - valueColumn->fromDouble(ip, par0); - minValueColumn->fromDouble(ip, par0 + parMin); - leftErrColumn->fromDouble(ip, roots[0] - parMin); - rightErrColumn->fromDouble(ip, roots[1] - parMin); - chiMinColumn->fromDouble(ip, chiMin); - - // Output the PDF - for (size_t i = 0; i < n; ++i) { - double chi = col2->toDouble(i); - col3->fromDouble(i, exp(-chi + chiMin)); - } - - // make sure function parameters don't change. - m_function->setParameter(ip, par0); - } - - // Improve estimates for standard deviations. - // If parameters are correlated the found deviations - // most likely underestimate the true values. - unfixParameters(); - GSLJacobian J(*m_function, values->size()); - m_function->functionDeriv(*domain, J); - refixParameters(); - // Calculate the hessian at the current point. - GSLMatrix H; - if (useWeighted) { - H.resize(nParams, nParams); - for (size_t i = 0; i < nParams; ++i) { - for (size_t j = i; j < nParams; ++j) { - double h = 0.0; - for (size_t k = 0; k < values->size(); ++k) { - double w = values->getFitWeight(k); - h += J.get(k, i) * J.get(k, j) * w * w; - } - H.set(i, j, h); - if (i != j) { - H.set(j, i, h); - } - } - } - } else { - H = J.matrix().tr() * J.matrix(); - } - // Square roots of the diagonals of the covariance matrix give - // the standard deviations in the quadratic approximation of the chi^2. - GSLMatrix V(H); - if (!useWeighted) { - V *= 1. / sigma2; - } - V.invert(); - // In a non-quadratic asymmetric case the following procedure can give a - // better result: - // Find the direction in which the chi^2 changes slowest and the positive and - // negative deviations in that direction. The change in a parameter at those - // points can be a better estimate for the standard deviation. - GSLVector v(nParams); - GSLMatrix Q(nParams, nParams); - // One of the eigenvectors of the hessian is the direction of the slowest - // change. - H.eigenSystem(v, Q); - - // Loop over the eigenvectors - for (size_t i = 0; i < nParams; ++i) { - auto dir = Q.copyColumn(i); - if (g_log.is(Kernel::Logger::Priority::PRIO_DEBUG)) { - g_log.debug() << "Direction " << i << '\n'; - g_log.debug() << dir << '\n'; - } - // Make a slice in that direction - ChiSlice slice(*m_function, dir, *domain, *values, chi0, sigma2); - double rBound0 = dir.dot(rBounds); - double lBound0 = dir.dot(lBounds); - if (g_log.is(Kernel::Logger::Priority::PRIO_DEBUG)) { - g_log.debug() << "lBound " << lBound0 << '\n'; - g_log.debug() << "rBound " << rBound0 << '\n'; - } - double lBound = slice.findBound(lBound0); - double rBound = slice.findBound(rBound0); - std::vector<double> P, A; - // Use a polynomial approximation - bool ok = true; - auto base = slice.makeApprox(lBound, rBound, P, A, ok); - if (!ok) { - g_log.warning() << "Approximation failed in direction " << i << '\n'; - } - // Find the deviation points where the chi^2 = 1/2 - A[0] -= 0.5; - std::vector<double> roots = base->roots(A); - std::sort(roots.begin(), roots.end()); - // Sort out the roots - auto nRoots = roots.size(); - if (nRoots == 0) { - roots.resize(2, 0.0); - } else if (nRoots == 1) { - if (roots.front() > 0.0) { - roots.insert(roots.begin(), 0.0); - } else { - roots.emplace_back(0.0); - } - } else if (nRoots > 2) { - roots[1] = roots.back(); - roots.resize(2); - } - if (g_log.is(Kernel::Logger::Priority::PRIO_DEBUG)) { - g_log.debug() << "Roots " << roots[0] << " (" << slice(roots[0]) << ") " - << roots[1] << " (" << slice(roots[1]) << ") \n"; - } - // Loop over the parameters and see if there deviations along - // this direction is greater than any previous value. - for (size_t ip = 0; ip < nParams; ++ip) { - auto lError = roots.front() * dir[ip]; - auto rError = roots.back() * dir[ip]; - if (lError > rError) { - std::swap(lError, rError); - } - if (lError < leftErrColumn->toDouble(ip)) { - if (g_log.is(Kernel::Logger::Priority::PRIO_DEBUG)) { - g_log.debug() << " left for " << ip << ' ' << lError << ' ' - << leftErrColumn->toDouble(ip) << '\n'; - } - leftErrColumn->fromDouble(ip, lError); - } - if (rError > rightErrColumn->toDouble(ip)) { - if (g_log.is(Kernel::Logger::Priority::PRIO_DEBUG)) { - g_log.debug() << " right for " << ip << ' ' << rError << ' ' - << rightErrColumn->toDouble(ip) << '\n'; - } - rightErrColumn->fromDouble(ip, rError); - } - } - // Output the quadratic estimate for comparrison. - quadraticErrColumn->fromDouble(i, sqrt(V.get(i, i))); - } -} + // Calculate function values. + fun.function(domain, values); -/// Temporary unfix any fixed parameters. -void CalculateChiSquared::unfixParameters() { - for (size_t i = 0; i < m_function->nParams(); ++i) { - if (!m_function->isActive(i)) { - m_function->unfix(i); - m_fixedParameters.emplace_back(i); + // Calculate the chi squared. + chiSquared = 0.0; + chiSquaredWeighted = 0.0; + dof = -static_cast<double>(nParams); + for (size_t i = 0; i < values.size(); ++i) { + auto weight = values.getFitWeight(i); + if (weight > 0.0) { + double tmp = values.getFitData(i) - values.getCalculated(i); + chiSquared += tmp * tmp; + tmp *= weight; + chiSquaredWeighted += tmp * tmp; + dof += 1.0; } } -} - -/// Restore the "fixed" status of previously unfixed paramters. -void CalculateChiSquared::refixParameters() { - for (auto &fixedParameter : m_fixedParameters) { - m_function->fix(fixedParameter); + if (dof <= 0.0) { + dof = 1.0; } } diff --git a/Framework/CurveFitting/src/Algorithms/ProfileChiSquared1D.cpp b/Framework/CurveFitting/src/Algorithms/ProfileChiSquared1D.cpp new file mode 100644 index 0000000000000000000000000000000000000000..5d4a624d8b01f9d084da6e6aba55a88600daebd5 --- /dev/null +++ b/Framework/CurveFitting/src/Algorithms/ProfileChiSquared1D.cpp @@ -0,0 +1,491 @@ +// Mantid Repository : https://github.com/mantidproject/mantid +// +// Copyright © 2021 ISIS Rutherford Appleton Laboratory UKRI, +// NScD Oak Ridge National Laboratory, European Spallation Source, +// Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS +// SPDX - License - Identifier: GPL - 3.0 + +#include "MantidCurveFitting/Algorithms/ProfileChiSquared1D.h" +#include "MantidAPI/AlgorithmManager.h" +#include "MantidAPI/Column.h" +#include "MantidAPI/IFunction.h" +#include "MantidAPI/ITableWorkspace.h" +#include "MantidAPI/MatrixWorkspace.h" +#include "MantidAPI/WorkspaceFactory.h" +#include "MantidCurveFitting/Algorithms/CalculateChiSquared.h" +#include "MantidCurveFitting/Algorithms/Fit.h" +#include "MantidCurveFitting/GSLJacobian.h" + +#include <boost/math/distributions/chi_squared.hpp> + +namespace { +// The maximum difference of chi squared to search for +// 10.8276 covers 99.9% of the distrubition +constexpr double MAXCHISQUAREDIFFERENCE = 10.8276; + +/// Calculate the change in chi2 +/// @param domain :: Function's domain. +/// @param nParams :: Number of free fitting parameters. +/// @param values :: Functin's values. +/// @param chi0 :: Chi squared at the minimum. +double getDiff(const Mantid::API::IFunction &fun, size_t nParams, + const Mantid::API::FunctionDomain &domain, + Mantid::API::FunctionValues &values, double chi0) { + double chiSquared = 0.0; + double chiSquaredWeighted = 0.0; + double dof = 0; + Mantid::CurveFitting::Algorithms::CalculateChiSquared::calcChiSquared( + fun, nParams, domain, values, chiSquared, chiSquaredWeighted, dof); + return chiSquaredWeighted - chi0; +} + +} // namespace + +namespace Mantid { +namespace CurveFitting { +namespace Algorithms { + +DECLARE_ALGORITHM(ProfileChiSquared1D) + +using namespace Mantid::API; +/// Helper class to calculate the chi squared along a direction in the parameter +/// space. +class ChiSlice { +public: + /// Constructor. + /// @param inputFunction :: The fitting function + /// @param fixedParameterIndex :: index of the parameter which is fixed + /// @param inputWS :: The input workspace (used for fit algorithm) + /// @param workspaceIndex :: Workspace index (used for fit algorithm) + /// @param domain :: Function's domain. + /// @param values :: Functin's values. + /// @param chi0 :: Chi squared at the minimum. + /// @param freeParameters :: Parameters which are free in the function. + ChiSlice(IFunction_sptr inputFunction, int fixedParameterIndex, + API::MatrixWorkspace_sptr inputWS, int workspaceIndex, + const API::FunctionDomain &domain, API::FunctionValues &values, + double chi0, std::vector<int> &freeParameters) + : m_fixedParameterIndex(fixedParameterIndex), m_domain(domain), + m_values(values), m_chi0(chi0), m_function(inputFunction), + m_ws(inputWS), m_workspaceIndex(workspaceIndex), + m_freeParameters(freeParameters) { + // create a fitting algorithm based on least squares (which is the default) + m_fitalg = AlgorithmFactory::Instance().create("Fit", -1); + m_fitalg->setChild(true); + } + /// Calculate the value of chi squared along the chosen direction at a + /// distance from + /// the minimum point. + /// @param p :: A distance from the minimum. + double operator()(double p) { + m_fitalg->initialize(); + m_fitalg->setProperty("Function", m_function); + m_fitalg->setProperty("InputWorkspace", m_ws); + m_fitalg->setProperty("WorkspaceIndex", m_workspaceIndex); + IFunction_sptr function = m_fitalg->getProperty("Function"); + std::vector<double> originalParamValues(function->nParams()); + for (auto ip = 0u; ip < function->nParams(); ++ip) { + originalParamValues[ip] = function->getParameter(ip); + } + function->setParameter(m_fixedParameterIndex, + originalParamValues[m_fixedParameterIndex] + p); + function->fix(m_fixedParameterIndex); + + // re run the fit to minimze the unfixed parameters + m_fitalg->execute(); + // find change in chi 2 + // num free parameters is the number of global free parameters - the 1 we've + // just fixed + int numFreeParameters = static_cast<int>(m_freeParameters.size() - 1); + double res = + getDiff(*function, numFreeParameters, m_domain, m_values, m_chi0); + // reset fit to original values + for (auto ip = 0u; ip < function->nParams(); ++ip) { + function->setParameter(ip, originalParamValues[ip]); + } + function->unfix(m_fixedParameterIndex); + return res; + } + + /// Make an approximation for this slice on an interval. + /// @param lBound :: The left bound of the approximation interval. + /// @param rBound :: The right bound of the approximation interval. + /// @param P :: Output vector with approximation parameters. + /// @param A :: Output vector with approximation parameters. + Functions::ChebfunBase_sptr makeApprox(double lBound, double rBound, + std::vector<double> &P, + std::vector<double> &A) { + + auto base = Functions::ChebfunBase::bestFitAnyTolerance( + lBound, rBound, *this, P, A, 1.0, 1e-4, 129); + if (!base) { + base = std::make_shared<Functions::ChebfunBase>(10, lBound, rBound, 1e-4); + P = base->fit(*this); + A = base->calcA(P); + } + return base; + } + + /// Fiind a displacement in the parameter space from the initial point + /// to a point where the PDF drops significantly. + /// @param shift :: Initial shift form par0 value. + double findBound(double shift) { + double bound0 = 0; + double diff0 = (*this)(0); + double bound = shift; + bool canDecrease = true; + for (size_t i = 0; i < 100; ++i) { + double diff = (*this)(bound); + + bool isIncreasing = fabs(bound) > fabs(bound0) && diff > diff0; + if (canDecrease) { + if (isIncreasing) + canDecrease = false; + } else { + if (!isIncreasing) { + bound = bound0; + break; + } + } + + bound0 = bound; + diff0 = diff; + + if (diff > MAXCHISQUAREDIFFERENCE - 1) { + if (diff < MAXCHISQUAREDIFFERENCE) { + break; + } + // diff is too large + bound *= 0.75; + } else { + // diff is too small + bound *= 2; + } + } + return bound; + } + +private: + // Fixed parameter index + int m_fixedParameterIndex; + /// The domain + const API::FunctionDomain &m_domain; + /// The values + API::FunctionValues &m_values; + /// The chi squared at the minimum + double m_chi0; + // fitting algorithm + IAlgorithm_sptr m_fitalg; + // Input workspace and function + IFunction_sptr m_function; + MatrixWorkspace_sptr m_ws; + int m_workspaceIndex; + // Vector of free parameter indices + std::vector<int> m_freeParameters; +}; // namespace Algorithms + +/// Default constructor +ProfileChiSquared1D::ProfileChiSquared1D() : IFittingAlgorithm() {} + +const std::string ProfileChiSquared1D::name() const { + return "ProfileChiSquared1D"; +} + +int ProfileChiSquared1D::version() const { return 1; } + +const std::string ProfileChiSquared1D::summary() const { + return "Profiles chi squared about its minimum to obtain parameter errors " + "for the input function."; +} + +void ProfileChiSquared1D::initConcrete() { + declareProperty("Output", "", "A base name for output workspaces."); +} + +void ProfileChiSquared1D::execConcrete() { + // Number of fiting parameters + auto nParams = m_function->nParams(); + // Create an output table for displaying slices of the chi squared and + // the probabilitydensity function + auto pdfTable = API::WorkspaceFactory::Instance().createTable(); + + // Sigma confidence levels, could be an input but for now look for 1 sigma + // (68%) 2 sigma (95) and 3(99%) error bounds chi2 disturbution has 1 degree + // of freedom if we are changing 1 parameter at a time + boost::math::chi_squared chi2Dist(1); + std::array<double, 3> sigmas = {1, 2, 3}; + std::array<double, 3> qvalues; + for (size_t i = 0; i < sigmas.size(); i++) { + double pvalue = std::erf(sigmas[i] / sqrt(2)); + // find chi2 quanitile for given p value + qvalues[i] = boost::math::quantile(chi2Dist, pvalue); + } + + // Find number of free parameter, should be >= 2 + std::vector<int> freeParameters; + for (size_t ip = 0; ip < nParams; ++ip) { + if (m_function->isActive(ip)) { + freeParameters.push_back(static_cast<int>(ip)); + } + } + + if (freeParameters.size() < 2) { + throw std::invalid_argument("Function must have 2 or more free parameters"); + } + + std::string baseName = getProperty("Output"); + Workspace_sptr ws = getProperty("InputWorkspace"); + int workspaceIndex = getProperty("WorkspaceIndex"); + MatrixWorkspace_sptr inputws = std::dynamic_pointer_cast<MatrixWorkspace>(ws); + if (baseName.empty()) { + baseName = "ProfileChiSquared1D"; + } + declareProperty( + std::make_unique<API::WorkspaceProperty<API::ITableWorkspace>>( + "PDFs", "", Kernel::Direction::Output), + "The name of the TableWorkspace in which to store the " + "pdfs of fit parameters"); + setPropertyValue("PDFs", baseName + "_pdf"); + setProperty("PDFs", pdfTable); + + // Create an output table for displaying the parameter errors. + auto errorsTable = API::WorkspaceFactory::Instance().createTable(); + auto nameColumn = errorsTable->addColumn("str", "Parameter"); + auto valueColumn = errorsTable->addColumn("double", "Value"); + auto minValueColumn = errorsTable->addColumn("double", "Value at Min"); + auto leftErrColumn = errorsTable->addColumn("double", "Left Error (1-sigma)"); + auto rightErrColumn = + errorsTable->addColumn("double", "Right Error (1-sigma)"); + auto leftErrColumn_2 = + errorsTable->addColumn("double", "Left Error (2-sigma)"); + auto rightErrColumn_2 = + errorsTable->addColumn("double", "Right Error (2-sigma )"); + auto leftErrColumn_3 = + errorsTable->addColumn("double", "Left Error (3-sigma)"); + auto rightErrColumn_3 = + errorsTable->addColumn("double", "Right Error (3-sigma )"); + auto quadraticErrColumn = + errorsTable->addColumn("double", "Quadratic Error (1-sigma)"); + errorsTable->setRowCount(freeParameters.size()); + declareProperty( + std::make_unique<API::WorkspaceProperty<API::ITableWorkspace>>( + "Errors", "", Kernel::Direction::Output), + "The name of the TableWorkspace in which to store the " + "values and errors of fit parameters"); + setPropertyValue("Errors", baseName + "_errors"); + setProperty("Errors", errorsTable); + + // Calculate initial values + double chiSquared = 0.0; + double chiSquaredWeighted = 0.0; + double dof = 0; + API::FunctionDomain_sptr domain; + API::FunctionValues_sptr values; + m_domainCreator->createDomain(domain, values); + CalculateChiSquared::calcChiSquared(*m_function, nParams, *domain, *values, + chiSquared, chiSquaredWeighted, dof); + // Value of chi squared for current parameters in m_function + double chi0 = chiSquaredWeighted; + + // Parameter bounds that define a volume in the parameter + // space within which the chi squared is being examined. + GSLVector lBounds(nParams); + GSLVector rBounds(nParams); + + // Number of points in lines for plotting + size_t n = 100; + pdfTable->setRowCount(n); + const double fac = 1e-4; + + for (auto p = 0u; p < freeParameters.size(); ++p) { + int row = p; + int ip = freeParameters[p]; + // Add columns for the parameter to the pdf table. + auto parName = m_function->parameterName(ip); + nameColumn->read(row, parName); + // Parameter values + auto col1 = pdfTable->addColumn("double", parName); + col1->setPlotType(1); + // Chi squared values + auto col2 = pdfTable->addColumn("double", parName + "_chi2"); + col2->setPlotType(2); + // PDF values + auto col3 = pdfTable->addColumn("double", parName + "_pdf"); + col3->setPlotType(2); + + double par0 = m_function->getParameter(ip); + double shift = fabs(par0 * fac); + if (shift == 0.0) { + shift = fac; + } + + // Make a slice along this parameter + ChiSlice slice(m_function, ip, inputws, workspaceIndex, *domain, *values, + chi0, freeParameters); + + // Find the bounds withn which the PDF is significantly above zero. + // The bounds are defined relative to par0: + // par0 + lBound is the lowest value of the parameter (lBound <= 0) + // par0 + rBound is the highest value of the parameter (rBound >= 0) + double lBound = slice.findBound(-shift); + double rBound = slice.findBound(shift); + lBounds[ip] = lBound; + rBounds[ip] = rBound; + + // Approximate the slice with a polynomial. + // P is a vector of values of the polynomial at special points. + // A is a vector of Chebyshev expansion coefficients. + // The polynomial is defined on interval [lBound, rBound] + // The value of the polynomial at 0 == chi squared at par0 + std::vector<double> P, A; + auto base = slice.makeApprox(lBound, rBound, P, A); + + // Write n slice points into the output table. + double dp = (rBound - lBound) / static_cast<double>(n); + for (size_t i = 0; i < n; ++i) { + double par = lBound + dp * static_cast<double>(i); + double chi = base->eval(par, P); + col1->fromDouble(i, par0 + par); + col2->fromDouble(i, chi); + } + + // Check if par0 is a minimum point of the chi squared + std::vector<double> AD; + // Calculate the derivative polynomial. + // AD are the Chebyshev expansion of the derivative. + base->derivative(A, AD); + // Find the roots of the derivative polynomial + std::vector<double> minima = base->roots(AD); + if (minima.empty()) { + minima.emplace_back(par0); + } + + // If only 1 extremum is found assume (without checking) that it's a + // minimum. + // If there are more than 1, find the one with the smallest chi^2. + double chiMin = std::numeric_limits<double>::max(); + double parMin = par0; + for (double minimum : minima) { + double value = base->eval(minimum, P); + if (value < chiMin) { + chiMin = value; + parMin = minimum; + } + } + // Get intersection of curve and line of constant q value to get confidence + // interval on parameter ip + valueColumn->fromDouble(row, par0); + minValueColumn->fromDouble(row, par0 + parMin); + for (size_t i = 0; i < qvalues.size(); i++) { + auto [rootsMin, rootsMax] = + getChiSquaredRoots(base, A, qvalues[i], rBound, lBound); + errorsTable->getColumn(3 + 2 * i)->fromDouble(row, rootsMin - parMin); + errorsTable->getColumn(4 + 2 * i)->fromDouble(row, rootsMax - parMin); + } + + // Output the PDF + for (size_t i = 0; i < n; ++i) { + double chi = col2->toDouble(i); + col3->fromDouble(i, exp(-chi + chiMin)); + } + // reset parameter values back to original value + m_function->setParameter(ip, par0); + } + + // Square roots of the diagonals of the covariance matrix give + // the standard deviations in the quadratic approximation of the chi^2. + GSLMatrix V = getCovarianceMatrix(); + for (size_t i = 0; i < freeParameters.size(); ++i) { + int ip = freeParameters[i]; + quadraticErrColumn->fromDouble(i, sqrt(V.get(ip, ip))); + } +} + +GSLMatrix ProfileChiSquared1D::getCovarianceMatrix() { + API::FunctionDomain_sptr domain; + API::FunctionValues_sptr values; + auto nParams = m_function->nParams(); + m_domainCreator->createDomain(domain, values); + unfixParameters(); + GSLJacobian J(*m_function, values->size()); + m_function->functionDeriv(*domain, J); + refixParameters(); + // Calculate the hessian at the current point. + GSLMatrix H; + H.resize(nParams, nParams); + for (size_t i = 0; i < nParams; ++i) { + for (size_t j = i; j < nParams; ++j) { + double h = 0.0; + for (size_t k = 0; k < values->size(); ++k) { + double w = values->getFitWeight(k); + h += J.get(k, i) * J.get(k, j) * w * w; + } + H.set(i, j, h); + if (i != j) { + H.set(j, i, h); + } + } + } + // Covariance matrix is inverse of hessian + GSLMatrix V(H); + V.invert(); + return V; +} + +/// Temporary unfix any fixed parameters. +void ProfileChiSquared1D::unfixParameters() { + for (size_t i = 0; i < m_function->nParams(); ++i) { + if (!m_function->isActive(i)) { + m_function->unfix(i); + m_fixedParameters.emplace_back(i); + } + } +} + +/// Restore the "fixed" status of previously unfixed paramters. +void ProfileChiSquared1D::refixParameters() { + for (auto &fixedParameter : m_fixedParameters) { + m_function->fix(fixedParameter); + } + m_fixedParameters.clear(); +} + +std::tuple<double, double> ProfileChiSquared1D::getChiSquaredRoots( + const Functions::ChebfunBase_sptr &approximation, + std::vector<double> &coeffs, double qvalue, double rBound, + double lBound) const { + // Points of intersections with line chi^2 = 1 give an estimate of + // the standard deviation of this parameter if it's uncorrelated with the + // others. + // Cache original value of A0 + auto Aold = coeffs[0]; + // Now find roots of curve when quantile is subtracted + coeffs[0] = Aold - qvalue; + std::vector<double> roots = approximation->roots(coeffs); + std::sort(roots.begin(), roots.end()); + if (roots.empty()) { + // Something went wrong; use the whole interval. + roots.resize(2); + roots[0] = lBound; + roots[1] = rBound; + } else if (roots.size() == 1) { + // Only one root found; use a bound for the other root. + if (roots.front() < 0) { + roots.emplace_back(rBound); + } else { + roots.insert(roots.begin(), lBound); + } + } else if (roots.size() > 2) { + // More than 2 roots; use the smallest and the biggest + auto smallest = roots.front(); + auto biggest = roots.back(); + roots.resize(2); + roots[0] = smallest; + roots[1] = biggest; + } + coeffs[0] = Aold; + return {roots[0], roots[1]}; +} + +} // namespace Algorithms +} // namespace CurveFitting +} // namespace Mantid \ No newline at end of file diff --git a/Framework/CurveFitting/test/Algorithms/CalculateChiSquaredTest.h b/Framework/CurveFitting/test/Algorithms/CalculateChiSquaredTest.h index 4396cf4c4b745ff48bc932ecfab3b4c6e9e1df15..4c99b5eca7ed75c127045eaf1e52b9a3b18e5feb 100644 --- a/Framework/CurveFitting/test/Algorithms/CalculateChiSquaredTest.h +++ b/Framework/CurveFitting/test/Algorithms/CalculateChiSquaredTest.h @@ -169,99 +169,6 @@ public: TS_ASSERT_DELTA(tester.chiSquared, 7151.0, 1.0); } - void test_errors_Thurber() { - double x[] = {-3.067, -2.981, -2.921, -2.912, -2.840, -2.797, -2.702, - -2.699, -2.633, -2.481, -2.363, -2.322, -1.501, -1.460, - -1.274, -1.212, -1.100, -1.046, -0.915, -0.714, -0.566, - -0.545, -0.400, -0.309, -0.109, -0.103, 0.010, 0.119, - 0.377, 0.790, 0.963, 1.006, 1.115, 1.572, 1.841, - 2.047, 2.200}; - double y[] = {80.574, 84.248, 87.264, 87.195, 89.076, 89.608, - 89.868, 90.101, 92.405, 95.854, 100.696, 101.060, - 401.672, 390.724, 567.534, 635.316, 733.054, 759.087, - 894.206, 990.785, 1090.109, 1080.914, 1122.643, 1178.351, - 1260.531, 1273.514, 1288.339, 1327.543, 1353.863, 1414.509, - 1425.208, 1421.384, 1442.962, 1464.350, 1468.705, 1447.894, - 1457.628}; - std::string fun("name=UserFunction,Formula=(b1 + b2*x + b3*x^2 + b4*x^3) / " - "(1 + b5*x + b6*x^2 + b7*x^3)," - "b1=1.2881396800E+03, b2=1.4910792535E+03, " - "b3=5.8323836877E+02, b4=7.5416644291E+01, " - "b5=9.6629502864E-01, b6=3.9797285797E-01, " - "b7=4.9727297349E-02"); - double sigma[] = {4.6647963344E+00, 3.9571156086E+01, 2.8698696102E+01, - 5.5675370270E+00, 3.1333340687E-02, 1.4984928198E-02, - 6.5842344623E-03}; - size_t ndata = sizeof(x) / sizeof(double); - Tester tester; - tester.setTestCaseForErrorCalculations(ndata, x, y, fun); - tester.runAlgorithm(); - tester.check1DSpectrum(); - tester.checkErrors(sigma); - } - - void test_errors_Chwirut1() { - double x[] = { - 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, - 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.625, 0.625, 0.625, 0.625, - 0.625, 0.75, 0.75, 0.75, 0.75, 0.75, 0.75, 0.75, 0.75, 0.75, 0.75, - 0.75, 0.75, 0.75, 0.75, 0.75, 0.75, 0.75, 0.75, 0.875, 0.875, 0.875, - 0.875, 0.875, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1.25, 1.25, 1.25, 1.25, 1.25, 1.5, 1.5, 1.5, 1.5, - 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.75, 1.75, 1.75, 1.75, - 1.75, 1.75, 1.75, 1.75, 1.75, 1.75, 1.75, 1.75, 1.75, 1.75, 1.75, - 1.75, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2.25, 2.25, 2.25, 2.25, 2.25, 2.25, 2.25, 2.25, 2.25, - 2.25, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.75, - 2.75, 2.75, 2.75, 2.75, 2.75, 2.75, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3.25, 3.25, 3.25, 3.25, 3.25, 3.75, 3.75, 3.75, - 3.75, 3.75, 3.75, 3.75, 4, 4, 4, 4, 4, 4, 4.25, - 4.25, 4.25, 4.25, 4.25, 4.75, 4.75, 4.75, 4.75, 4.75, 5, 5, - 5, 5, 5, 5, 5.25, 5.25, 5.25, 5.25, 5.25, 5.75, 5.75, - 5.75, 5.75, 5.75, 6, 6, 6, 6, 6, 6, 6, 6, - 6, 6, 6, 6, 6}; - double y[] = { - 76.8, 70.5, 78, 70.8, 90.6, 78, 74.1, 81.5, - 81.3, 92.9, 75.8, 66.7, 81, 81.7, 78, 80, - 80.7, 76.8, 79, 66, 76.9, 78.7, 67.3, 59.2, - 60.9, 62, 60, 63.5, 59.9, 61.6, 59.5, 61, - 63.8, 54.7, 71.6, 62, 61.3, 64.2, 62.4, 60.8, - 64.5, 55.5, 63.6, 57.2, 58, 64.9, 48.5, 47.8, - 48.8, 47.7, 53.2, 57.1, 40.8, 54, 47.5, 48, - 50.3, 39.2, 42.5, 43.3, 41, 37.8, 29, 35.5, - 35.2, 35.8, 32.9, 32, 32.5, 33.8, 39.8, 33.2, - 30.7, 29.3625, 29.4, 28.4, 29.175, 25.5, 31.1, 26.85, - 25.95, 28.9, 26.8, 28.95, 29.81, 29.3, 28.69, 29.8, - 31.05, 21.67, 21, 20.32, 20, 22.57, 22.2, 25.7, - 24, 29.8, 29.62, 25.99, 24.56, 21.15, 20.4, 23.6, - 22.125, 21.4, 20.4, 23.775, 21.07, 20.2, 21, 21, - 19.31, 16.95, 18.82, 16.3, 18.67, 17.7, 23.7, 23.81, - 16.7625, 17.5125, 17.17, 16.65, 16.4625, 17.7375, 13.87, 15.64, - 12.75, 13.95, 13.69, 15.64, 14.62, 15.19, 12.94, 13.12, - 12.75, 13.31, 12.56, 13.35, 13.31, 11.81, 16.24, 13.87, - 14.62, 17.7, 12.75, 13.84, 16.12, 12.07, 11.81, 18.07, - 25.76, 15.56, 24.19, 13.12, 12.41, 13.2, 14.25, 12.525, - 13.8, 9.67, 10.875, 9.45, 10.05, 10.39, 11.5875, 10.5375, - 7.76, 8.17, 10.42, 11.25, 8.62, 8.74, 11.55, 9.15, - 9.4125, 8.5875, 5.44, 8.175, 7.9125, 7.725, 7.125, 4.87, - 7.35, 7.31, 9, 7.2, 7.87, 12.07, 8.55, 7.35, - 6.1125, 4.01, 5.9625, 8.475, 5.9625, 3.75, 5.625, 6.1125, - 8.025, 7.42, 6.67, 5.06, 4.87, 5.44, 6.64, 5.63, - 5.44, 8.51, 8.74, 3.94, 5.63, 10.12}; - std::string fun("name=UserFunction,Formula=exp(-b1*x)/" - "(b2+b3*x),b1=1.9027818370E-01,b2=6.1314004477E-03,b3=1." - "0530908399E-02"); - double sigma[] = {2.1938557035E-02, 3.4500025051E-04, 7.9281847748E-04}; - size_t ndata = sizeof(x) / sizeof(double); - Tester tester; - tester.setTestCaseForErrorCalculations(ndata, x, y, fun); - tester.runAlgorithm(); - tester.check1DSpectrum(); - tester.checkErrors(sigma); - } - private: class Tester { // input parameters @@ -281,7 +188,6 @@ private: double EndX; bool ignoreInvalidData; std::string outputName; - bool Weighted; void makeXValues() { size_t dlt = isHisto ? 1 : 0; @@ -325,14 +231,12 @@ private: double chiSquaredDividedByDOF; double chiSquaredWeighted; double chiSquaredWeightedDividedByDOF; - ITableWorkspace_sptr errors; - ITableWorkspace_sptr pdfs; bool isExecuted; Tester(size_t np = 3, size_t nd = 10, bool histo = true) : nParams(np), nData(nd), isHisto(histo), xMin(-10), xMax(10), workspaceIndex(0), StartX(EMPTY_DBL()), EndX(EMPTY_DBL()), - ignoreInvalidData(false), Weighted(false), + ignoreInvalidData(false), // output chiSquared(-1), chiSquaredDividedByDOF(-1), chiSquaredWeighted(-1), chiSquaredWeightedDividedByDOF(-1), isExecuted(false) { @@ -353,10 +257,6 @@ private: TS_ASSERT_THROWS_NOTHING(alg.setProperty("StartX", StartX)); TS_ASSERT_THROWS_NOTHING(alg.setProperty("EndX", EndX)); } - if (!outputName.empty()) { - TS_ASSERT_THROWS_NOTHING(alg.setProperty("Output", outputName)); - TS_ASSERT_THROWS_NOTHING(alg.setProperty("Weighted", Weighted)); - } TS_ASSERT_THROWS_NOTHING(alg.execute()); isExecuted = alg.isExecuted(); if (isExecuted) { @@ -365,12 +265,6 @@ private: chiSquaredWeighted = alg.getProperty("ChiSquaredWeighted"); chiSquaredWeightedDividedByDOF = alg.getProperty("ChiSquaredWeightedDividedByDOF"); - if (!outputName.empty()) { - errors = AnalysisDataService::Instance().retrieveWS<ITableWorkspace>( - "out_errors"); - pdfs = AnalysisDataService::Instance().retrieveWS<ITableWorkspace>( - "out_pdf"); - } } } @@ -482,8 +376,6 @@ private: FunctionValues y(x); function->function(x, y); double tmp = yValues[i] - y[0]; - // std::cerr << "test " << xValue << ' ' << yValues[i] << ' ' << y[0] - // << '\n'; sum2 += tmp * tmp; tmp /= eValues[i]; sum2w += tmp * tmp; @@ -500,20 +392,6 @@ private: TS_ASSERT_DELTA(sum2, chiSquaredDividedByDOF, 1e-10); TS_ASSERT_DELTA(sum2w, chiSquaredWeightedDividedByDOF, 1e-10); } - void checkFailed() { TS_ASSERT(!isExecuted); } - - void checkErrors(double *sigma) { - size_t np = function->nParams(); - TS_ASSERT_EQUALS(np, errors->rowCount()); - for (size_t i = 0; i < np; ++i) { - TS_ASSERT_LESS_THAN(fabs(errors->Double(i, 6)), 1e-4); - TS_ASSERT_DELTA(errors->Double(i, 1) / errors->Double(i, 2), 1.0, 1e-5); - TS_ASSERT_DELTA(0.5 * (errors->Double(i, 4) - errors->Double(i, 3)) / - errors->Double(i, 5), - 1.0, 2.0); - TS_ASSERT_DELTA(errors->Double(i, 5) / sigma[i], 1.0, 0.01); - } - } }; }; diff --git a/Framework/CurveFitting/test/Algorithms/ProfileChiSquared1DTest.h b/Framework/CurveFitting/test/Algorithms/ProfileChiSquared1DTest.h new file mode 100644 index 0000000000000000000000000000000000000000..2580ca461128bf9d0afa9078c7c66ebb4dab5a80 --- /dev/null +++ b/Framework/CurveFitting/test/Algorithms/ProfileChiSquared1DTest.h @@ -0,0 +1,169 @@ +// Mantid Repository : https://github.com/mantidproject/mantid +// +// Copyright © 2021 ISIS Rutherford Appleton Laboratory UKRI, +// NScD Oak Ridge National Laboratory, European Spallation Source, +// Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS +// SPDX - License - Identifier: GPL - 3.0 + +#pragma once + +#include <cxxtest/TestSuite.h> + +#include "MantidAPI/AlgorithmManager.h" +#include "MantidAPI/AnalysisDataService.h" +#include "MantidCurveFitting/Algorithms/ProfileChiSquared1D.h" +#include "MantidDataObjects/TableWorkspace.h" + +using Mantid::CurveFitting::Algorithms::ProfileChiSquared1D; +using namespace Mantid; +using namespace Mantid::API; +using namespace Mantid::DataObjects; + +namespace { +constexpr char *linearFunctionString = + "name = LinearBackground, A0=0.8753627851076761, A1 = " + "2.026706319695708 "; +} + +class ProfileChiSquared1DTest : public CxxTest::TestSuite { +public: + static ProfileChiSquared1DTest *createSuite() { + return new ProfileChiSquared1DTest(); + } + static void destroySuite(ProfileChiSquared1DTest *suite) { + AnalysisDataService::Instance().clear(); + delete suite; + } + + void loadLinearData(const std::string &workspaceName) { + auto algo = AlgorithmManager::Instance().create("Load"); + algo->setPropertyValue("Filename", "ProfileChiSquared1DData_linear.nxs"); + algo->setPropertyValue("OutputWorkspace", workspaceName); + algo->execute(); + } + + void executeAlgorithmOnLinearData(const std::string &outputName) { + std::string wsName = "ProfileChiSquared1DData_linear"; + loadLinearData(wsName); + auto ws = AnalysisDataService::Instance().retrieveWS<Workspace>(wsName); + auto functionString = std::string(linearFunctionString); + auto profileAlg = ProfileChiSquared1D(); + profileAlg.initialize(); + profileAlg.setProperty("Function", functionString); + profileAlg.setProperty("InputWorkspace", ws); + profileAlg.setProperty("Output", outputName); + profileAlg.execute(); + } + + void test_Init() { + ProfileChiSquared1D alg; + TS_ASSERT_THROWS_NOTHING(alg.initialize()) + TS_ASSERT(alg.isInitialized()) + } + + void test_Alg_produces_expected_outputs() { + executeAlgorithmOnLinearData("OutputName1"); + TS_ASSERT(AnalysisDataService::Instance().doesExist("OutputName1_errors")); + TS_ASSERT(AnalysisDataService::Instance().doesExist("OutputName1_pdf")); + + // if name is empty, workspaces will use default name of ProfileChiSquared1D + executeAlgorithmOnLinearData(""); + TS_ASSERT(AnalysisDataService::Instance().doesExist( + "ProfileChiSquared1D_errors")); + TS_ASSERT( + AnalysisDataService::Instance().doesExist("ProfileChiSquared1D_pdf")); + + AnalysisDataService::Instance().clear(); + } + // the tests for these linear problems are comparing against analytical + // calculations which can be solved in closed form for a linear function + void test_errors_for_linear_function_are_correct() { + executeAlgorithmOnLinearData("OutputName1"); + TableWorkspace_sptr errorsTable; + TS_ASSERT_THROWS_NOTHING( + errorsTable = + AnalysisDataService::Instance().retrieveWS<TableWorkspace>( + "OutputName1_errors")); + + TS_ASSERT_EQUALS(errorsTable->String(0, 0), "A0"); + TS_ASSERT_EQUALS(errorsTable->String(1, 0), "A1"); + // check a0 parameter + TS_ASSERT_DELTA(errorsTable->Double(0, 1), 0.8753627851076761, 1e-6); + TS_ASSERT_DELTA(errorsTable->Double(0, 2), 0.8753627851076761, 1e-6); + TS_ASSERT_DELTA(errorsTable->Double(0, 3), -0.03447782006595415, 1e-6); + TS_ASSERT_DELTA(errorsTable->Double(0, 4), 0.03447782006595401, 1e-6); + TS_ASSERT_DELTA(errorsTable->Double(0, 5), -0.06895564013190685, 1e-6); + TS_ASSERT_DELTA(errorsTable->Double(0, 6), 0.06895564013190683, 1e-6); + TS_ASSERT_DELTA(errorsTable->Double(0, 7), -0.10343346019785989, 1e-6); + TS_ASSERT_DELTA(errorsTable->Double(0, 8), 0.1034334601978597, 1e-6); + TS_ASSERT_DELTA(errorsTable->Double(0, 9), 0.03447782006595295, 1e-6); + // check a1 parameter + TS_ASSERT_DELTA(errorsTable->Double(1, 1), 2.026706319695708, 1e-6); + TS_ASSERT_DELTA(errorsTable->Double(1, 2), 2.026706319695708, 1e-6); + TS_ASSERT_DELTA(errorsTable->Double(1, 3), -0.006137378377995283, 1e-6); + TS_ASSERT_DELTA(errorsTable->Double(1, 4), 0.006137378377995297, 1e-6); + TS_ASSERT_DELTA(errorsTable->Double(1, 5), -0.012274756755989097, 1e-6); + TS_ASSERT_DELTA(errorsTable->Double(1, 6), 0.012274756755989113, 1e-6); + TS_ASSERT_DELTA(errorsTable->Double(1, 7), -0.01841213513398322, 1e-6); + TS_ASSERT_DELTA(errorsTable->Double(1, 8), 0.01841213513398322, 1e-6); + TS_ASSERT_DELTA(errorsTable->Double(1, 9), 0.006137378377994362, 1e-6); + AnalysisDataService::Instance().clear(); + } + + void test_pdf_values_for_linear_function_are_correct() { + executeAlgorithmOnLinearData("OutputName2"); + TableWorkspace_sptr pdfTable; + TS_ASSERT_THROWS_NOTHING( + pdfTable = AnalysisDataService::Instance().retrieveWS<TableWorkspace>( + "OutputName2_pdf")); + // check some values of pdf table + TS_ASSERT_DELTA(pdfTable->Double(0, 0), 0.696088486717624, 1e-6); + TS_ASSERT_DELTA(pdfTable->Double(0, 1), 27.03687327118116, 1e-2); + TS_ASSERT_DELTA(pdfTable->Double(0, 3), 2.0007644788036028, 1e-6); + TS_ASSERT_DELTA(pdfTable->Double(0, 4), 17.86634783053958, 1e-2); + + TS_ASSERT_DELTA(pdfTable->Double(25, 0), 0.78572563591265, 1e-6); + TS_ASSERT_DELTA(pdfTable->Double(25, 1), 6.75921831779535, 1e-2); + TS_ASSERT_DELTA(pdfTable->Double(25, 3), 2.0137353992496556, 1e-6); + TS_ASSERT_DELTA(pdfTable->Double(25, 4), 4.466586957634646, 1e-2); + + TS_ASSERT_DELTA(pdfTable->Double(50, 0), 0.8753627851076761, 1e-6); + TS_ASSERT_DELTA(pdfTable->Double(50, 1), -5.32907051820075e-14, 1e-2); + TS_ASSERT_DELTA(pdfTable->Double(50, 3), 2.026706319695708, 1e-6); + TS_ASSERT_DELTA(pdfTable->Double(50, 4), -3.197442310920451e-13, 1e-2); + + TS_ASSERT_DELTA(pdfTable->Double(75, 0), 0.9649999343027021, 1e-6); + TS_ASSERT_DELTA(pdfTable->Double(75, 1), 6.759218317794955, 1e-2); + TS_ASSERT_DELTA(pdfTable->Double(75, 3), 2.0396772401417604, 1e-6); + TS_ASSERT_DELTA(pdfTable->Double(75, 4), 4.4665869576346875, 1e-2); + + TS_ASSERT_DELTA(pdfTable->Double(99, 0), 1.051051597529927, 1e-6); + TS_ASSERT_DELTA(pdfTable->Double(99, 1), 25.96621308964161, 1e-2); + TS_ASSERT_DELTA(pdfTable->Double(99, 3), 2.052129323769971, 1e-6); + TS_ASSERT_DELTA(pdfTable->Double(99, 4), 17.15884045645028, 1e-2); + + AnalysisDataService::Instance().clear(); + } + + void test_errors_table_has_correct_shape() { + executeAlgorithmOnLinearData("OutputName3"); + TableWorkspace_sptr errorsTable; + TS_ASSERT_THROWS_NOTHING( + errorsTable = + AnalysisDataService::Instance().retrieveWS<TableWorkspace>( + "OutputName3_errors")); + TS_ASSERT_EQUALS(errorsTable->columnCount(), 10); + TS_ASSERT_EQUALS(errorsTable->rowCount(), 2); + AnalysisDataService::Instance().clear(); + } + + void test_pdf_table_has_correct_shape() { + executeAlgorithmOnLinearData("OutputName4"); + TableWorkspace_sptr pdfTable; + TS_ASSERT_THROWS_NOTHING( + pdfTable = AnalysisDataService::Instance().retrieveWS<TableWorkspace>( + "OutputName4_pdf")); + TS_ASSERT_EQUALS(pdfTable->columnCount(), 6); + TS_ASSERT_EQUALS(pdfTable->rowCount(), 100); + AnalysisDataService::Instance().clear(); + }; +}; \ No newline at end of file diff --git a/Testing/Data/UnitTest/ProfileChiSquared1DData_linear.nxs.md5 b/Testing/Data/UnitTest/ProfileChiSquared1DData_linear.nxs.md5 new file mode 100644 index 0000000000000000000000000000000000000000..c64a34c81195c515f1767b423cb299fb5df184fe --- /dev/null +++ b/Testing/Data/UnitTest/ProfileChiSquared1DData_linear.nxs.md5 @@ -0,0 +1 @@ +d3526ef8e18fc293ac5c81a5208722bd diff --git a/docs/source/algorithms/CalculateChiSquared-v1.rst b/docs/source/algorithms/CalculateChiSquared-v1.rst index e96c3a6b10ef2af55cd50f433b4d741f5c7cd61a..4e038fab7ccf3008d7abbdb24cd1165f1d5da788 100644 --- a/docs/source/algorithms/CalculateChiSquared-v1.rst +++ b/docs/source/algorithms/CalculateChiSquared-v1.rst @@ -33,35 +33,6 @@ Finally, ChiSquaredWeightedDividedByDOF is :math:`\chi_{4}^{2} = \chi_{3}^{2} / DOF` -Parameter errors -################ - -Setting the Output property to a non-empty string makes the algorithm explore the surface of the :math:`\chi^{2}` -around its minimum and estimate the standard deviations for the parameters. The value of the property is a base name -for two output table workspaces: '<Output>_errors' and '<Output>_pdf'. The former workspace contains parameter error -estimates and the latter shows :math:`\chi^{2}`'s 1d slices along each parameter (keeping all other fixed). - -The error table has the following columns: - -=============== =========== -Column Description -=============== =========== -Parameter Parameter name -Value Parameter value passed with the Function property -Value at Min The minimum point of the 1d slice of the :math:`\chi^{2}`. If the Function is at the minimum then - Value at Min should be equal to Value. -Left Error The negative deviation from the minimum point equivalent to :math:`1\sigma`. Estimated from analisys - of the surface. -Right Error The positive deviation from the minimum point equivalent to :math:`1\sigma`. Estimated from analisys - of the surface. -Quadratic Error :math:`1\sigma` standard deviation in the quadratic approximation of the surface. -Chi2 Min The value of :math:`\chi^{2}` at the minimum relative to the test point. -=============== =========== - -The pdf table contains slices of the :math:`\chi^{2}` along each parameter. It has 3 column per parameter. The first column of the 3 -is the parameter values, the second has the :math:`\chi^{2}` and the third is the probability density function normalised to -have 1 at the maximum. - Usage ----- **Example 1** @@ -119,38 +90,6 @@ Output: Chi squared weighted / DOF is 0.0 Chi squared weighted / NDATA is 0.0 -**Example 2** - -.. testcode:: - - import numpy as np - # Create a workspace and fill it with some gaussian data and some noise - n = 100 - x = np.linspace(-10,10,n) - y = np.exp(-x*x/2) + np.random.normal(0.0, 0.01, n) - e = [1] * n - ws = CreateWorkspace(x,y,e) - - # Gefine a Gaussian with exactly the same parameters that were used to - # generate the data - fun_t = 'name=Gaussian,Height=%s,PeakCentre=%s,Sigma=%s' - fun = fun_t % (1, 0, 1) - # Test the chi squared. - CalculateChiSquared(fun,ws,Output='Test0') - # Check the Test0_errors table and see that the parameters are not at minimum - - # Fit the function - res = Fit(fun,ws,Output='out') - # res[3] is a table with the fitted parameters - nParams = res[3].rowCount() - 1 - params = [res[3].cell(i,1) for i in range(nParams)] - # Build a new function and populate it with the fitted parameters - fun = fun_t % tuple(params) - # Test the chi squared. - CalculateChiSquared(fun,ws,Output='Test1') - # Check the Test1_errors table and see that the parameters are at minimum now - - .. categories:: .. sourcelink:: diff --git a/docs/source/algorithms/ProfileChiSquared1D-v1.rst b/docs/source/algorithms/ProfileChiSquared1D-v1.rst new file mode 100644 index 0000000000000000000000000000000000000000..f2bab9e2a4bb46adf07e93edde56e7c03a143ff1 --- /dev/null +++ b/docs/source/algorithms/ProfileChiSquared1D-v1.rst @@ -0,0 +1,184 @@ + +.. algorithm:: + +.. summary:: + +.. relatedalgorithms:: + +.. properties:: + +Description +----------- + +This algorithm explores the surface of the :math:`\chi^{2}` around its minimum and estimates the standard deviations for the parameters. +The value of the output property is a base name for two output table workspaces: '<Output>_errors' and '<Output>_pdf'. +The former workspace contains parameter error estimates and the latter shows :math:`\chi^{2}`'s 1d slices along each parameter. + +The procedure for calculating errors of parameters is described in Chapter 15 of Numerical recipes in C [1] and Chapter 9 +of Statistical Data Analysis [2]. Here, we summarise the main results. + +Consider the input dataset :math:`D_0`, with fit parameters :math:`\mathbf a_0`. Assuming Gaussian noise in the data, it is possible +to create :math:`n` artificial datasets :math:`D_j` for :math:`j=1,2,..,n`. If we were to run the fit on each dataset, +a set of parameters :math:`\mathbf a_j` would be obtained. The distribution of these parameters, +about our original parameter set :math:`\delta \mathbf a = \mathbf a_j - \mathbf a_0` is described by the multivariate normal distribution, + +.. math:: + P(\delta \mathbf a ) \propto exp (-\Delta \chi^2) + +where :math:`\Delta \chi^2=\chi^2(\mathbf a_J) - \chi^2(\mathbf a_0)`, is the difference between the chi squared statistics obtained for each parameter set, + +.. math:: + \Delta \chi^2 = \sum_{i \in D_0} \left ( \frac{y_i -f(x_i;\mathbf a_J)}{\sigma_i}\right)^2 - \left ( \frac{y_i -f(x_i;\mathbf a_0)}{\sigma_i}\right)^2 + +If we consider the variation of a single parameter :math:`a_k`, while the other parameters are the values that minimize :math:`\chi^2`, +the quantity :math:`\Delta \chi^2` will be distributed as a chi squared distributed, :math:`f_{\chi^2}(x; \nu)` with 1 degree of freedom, +:math:`\nu=1`. From this distribution, the probability of finding increases less than :math:`\Delta \chi^2` can be found from the integral, + +.. math:: + p = P(X < \Delta \chi^2 ) = \int_0^{\Delta \chi^2} f_{\chi^2}(x; 1) dx + +where :math:`p` is the desired confidence level. For instance, if we consider a confidence level of :math:`p=68.3\%` (1-sigma), +we find a critical value for the chi squared fluctuation of :math:`\Delta \chi^2 < 1`. From this desired confidence level, +we can then calculate the parameter variation, (:math:`\sigma_{left}`, :math:`\sigma_{right}`), that increases chi squared by 1 +and obtain a :math:`68.3\%` confidence interval for the single parameter :math:`a_k`: + +.. math:: + P( a_k - \sigma_{left} < a_k < a_k + \sigma_{right}) = 68.3\% + +This algorithm obtains the 1-sigma confidence level by varying a single parameter at a time and recording the extremums +of the values that increase :math:`\chi^2` by :math:`1`. The results are outputted in the table '<Output>_errors', +which reports the left and right 1-sigma errors. This procedure is repeated for 2-sigma and 3-sigma, +with the results displayed in the columns (2-sigma) and (3-sigma). An additional value is also reported, termed the quadratic error. +This is the 1-sigma error obtained from a Taylor expansion of the chi squared function about its minimum: + +.. math:: + \chi^2(\mathbf a + \delta \mathbf a) = \chi^2(\mathbf a)_{min} + \delta \mathbf a^T \mathbf C \delta \mathbf a + +where :math:`\mathbf{C}` is the covariance matrix obtained from least squares fitting of the data, see the :ref:`Fitting <algm-Fit>` documentation for further details. +For a linear model (or approximately linear model) the quadratic error should be equal to the left and right errors, i.e it will be symmetric. +This can be seen in the following example + +.. code-block:: python + + from mantid.simpleapi import * + import matplotlib.pyplot as plt + import numpy as np + + # create the data + x = np.linspace(1, 10, 250) + np.random.seed(0) + y = 1 + 2.0*x + 0.4*np.random.randn(x.size) + ws = CreateWorkspace(x, y) + + # run the fit + func = "name=LinearBackground, A0=1, A1=2"; + func_ = FunctionFactory.Instance().createInitialized(func) + + fit_output = Fit(InputWorkspace=ws, Function=func_, Output="LinearFit") + a0_fit = fit_output.OutputParameters.column(1)[0] + a1_fit = fit_output.OutputParameters.column(1)[1] + + # explore the chi squared profile for the fit parameters + fitted_func = "name=LinearBackground, A0={}, A1={}".format(a0_fit, a1_fit); + ProfileChiSquared1D(fitted_func, ws, Output="LinearProfile") + + # print left and right errors of parameters + # you should note that they are approx equal to the quadratic error for this linear model + error_table = mtd["LinearProfile_errors"] + lerror_a0 = error_table.column(3)[0] + rerror_a0= error_table.column(4)[0] + qerror_a0 = error_table.column(9)[0] + print("1-sigma error bounds of A0 are {} and {}, with quadratic estimate {}".format(lerror_a0, rerror_a0, qerror_a0)) + + lerror_a1 = error_table.column(3)[1] + rerror_a1= error_table.column(4)[1] + qerror_a1 = error_table.column(9)[1] + print("1-sigma error bounds of A1 are {} and {}, with quadratic estimate {}".format(lerror_a1, rerror_a1, qerror_a1)) + + +For a non-linear model, it's possible that the left and right variances will not be equal, leading to an asymmetric error. +This is shown in the example below: + +.. code-block:: python + + # import mantid algorithms, numpy and matplotlib + from mantid.simpleapi import * + import matplotlib.pyplot as plt + import numpy as np + + + # create decaying exponential data + x = np.linspace(1, 10, 250) + np.random.seed(0) + y = 3.0*np.exp(-x/2) + 0.1*np.random.randn(x.size) + ws = CreateWorkspace(x, y) + + # run the fit + func = "name=ExpDecay,Height=3.0, Lifetime=0.5"; + func_ = FunctionFactory.Instance().createInitialized(func) + + fit_output = Fit(InputWorkspace=ws, Function=func_, Output="ExpFit") + height_fit = fit_output.OutputParameters.column(1)[0] + lifetime_fit = fit_output.OutputParameters.column(1)[1] + + # explore the chi squared profile for the fit parameters + fitted_func = "name=ExpDecay, Height={}, Lifetime={}".format(height_fit, lifetime_fit); + ProfileChiSquared1D(fitted_func, ws, Output="ExpProfile") + + # print left and right errors of parameters + # you should note that they differ from the quadratic errors + error_table = mtd["ExpProfile_errors"] + lerror_height = error_table.column(3)[0] + rerror_height= error_table.column(4)[0] + qerror_height = error_table.column(9)[0] + print("1-sigma error bounds of Height are {} and {}, with quadratic estimate {}".format(lerror_height, rerror_height, qerror_height)) + + lerror_lifetime = error_table.column(3)[1] + rerror_lifetime= error_table.column(4)[1] + qerror_lifetime = error_table.column(9)[1] + print("1-sigma error bounds of Lifetime are {} and {}, with quadratic estimate {}".format(lerror_lifetime, rerror_lifetime, qerror_lifetime)) + + +For each problem, the error table has the following columns: + +====================== ============== +Column Description +====================== ============== +Parameter Parameter name +Value Parameter value passed with the Function property +Value at Min The minimum point of the 1d slice of the :math:`\chi^{2}`. If the Function is at the minimum then + Value at Min should be equal to Value. +Left Error (1-sigma) The negative deviation from the minimum point equivalent to :math:`1\sigma`. Estimated from analisys + of the surface. +Right Error (1-sigma) The positive deviation from the minimum point equivalent to :math:`1\sigma`. Estimated from analisys + of the surface. +Left Error (2-sigma) The negative deviation from the minimum point equivalent to :math:`2\sigma`. Estimated from analisys + of the surface. +Right Error (2-sigma) The positive deviation from the minimum point equivalent to :math:`2\sigma`. Estimated from analisys + of the surface. +Left Error (3-sigma) The negative deviation from the minimum point equivalent to :math:`3\sigma`. Estimated from analisys + of the surface. +Right Error (3-sigma) The positive deviation from the minimum point equivalent to :math:`3\sigma`. Estimated from analisys + of the surface. +Quadratic Error :math:`1\sigma` standard deviation in the quadratic approximation of the surface. +====================== ============== + +This algorithm also reports a probability density function (PDF) for each parameter, stored in the '<Output>_pdf' table. +This PDF is found from Eq. (1), which relates the increase in chi squared, to the probability of the parameter variation. +The pdf table also contains slices of the :math:`\chi^{2}` along each parameter. It has 3 column per parameter. The first column of the 3 +is the parameter values, the second has the :math:`\chi^{2}` and the third is the probability density function normalised to +have 1 at the maximum. Plotting the second column of each parameter will show the change in :math:`\chi^{2}` with respect to +the parameter value. + +References +---------- + +[1] William H. Press, Saul A. Teukolsky, William T. Vetterling, and Brian P. Flannery. 1992. +Numerical recipes in C (2nd ed.): the art of scientific computing. Cambridge University Press, USA. + +[2] G. Cowan, Statistical Data Analysis, Clarendon, Oxford, 1998 + + +.. categories:: + +.. sourcelink:: diff --git a/docs/source/concepts/Fitting.rst b/docs/source/concepts/Fitting.rst index a7384ee07aabaa01350386bb9f391ae8078f0c43..0f918a7180eb7a20fbeebf7dcae7e8126d594afa 100644 --- a/docs/source/concepts/Fitting.rst +++ b/docs/source/concepts/Fitting.rst @@ -132,7 +132,7 @@ is :ref:`FitPeak <algm-FitPeak>` which aims to fit single peaks with some checks to ensure results are physical. If you remain unsure if a given fit was successful then e.g. try the -tool :ref:`CalculateChiSquared <algm-CalculateChiSquared>`, which allows +tool :ref:`ProfileChiSquared1D <algm-ProfileChiSquared1D>`, which allows inspection of the cost function in the neighbourhood of a found minimum. diff --git a/docs/source/release/v6.1.0/mantidworkbench.rst b/docs/source/release/v6.1.0/mantidworkbench.rst index e33d38dfcbea5df9ba402e5b291087b365adb3b1..48012792269b866fb0825dd9a04cda8a50e440e8 100644 --- a/docs/source/release/v6.1.0/mantidworkbench.rst +++ b/docs/source/release/v6.1.0/mantidworkbench.rst @@ -18,6 +18,10 @@ Added Floating/On Top setting for all the windows that are opened by workbench ( - A new empty facility with empty instrument is the default facility now, and user has to select their choice of facility (including ISIS) and instrument for the first time + +- Added an algorithm ProfileChiSquared1D to profile chi squared after a fit. This can be used + to find better estimates of parameter errors. + - Instrument view: when in tube selection mode, the sum of pixel counts is now output to the selection pane. Bugfixes