From be6a03bff92396cd00c97135a7094bd295ed41cd Mon Sep 17 00:00:00 2001 From: Timo Koch <timo.koch@iws.uni-stuttgart.de> Date: Thu, 24 Sep 2020 18:22:30 +0200 Subject: [PATCH] [common] Add inverse of monotone cubic spline --- dumux/common/monotonecubicspline.hh | 61 +++++++++++++++++++ .../common/spline/test_monotonecubicspline.cc | 28 +++++++++ 2 files changed, 89 insertions(+) diff --git a/dumux/common/monotonecubicspline.hh b/dumux/common/monotonecubicspline.hh index 8a6bcdc7a3..0af5c0ad43 100644 --- a/dumux/common/monotonecubicspline.hh +++ b/dumux/common/monotonecubicspline.hh @@ -35,6 +35,9 @@ // Hermite basis functions #include <dumux/common/cubicsplinehermitebasis.hh> +// for inversion +#include <dumux/nonlinear/findscalarroot.hh> + namespace Dumux { /*! @@ -83,6 +86,9 @@ public: // the number of control points numPoints_ = x.size(); + // whether we are increasing + increasing_ = y_.back() > y_.front(); + // the slope at every control point m_.resize(numPoints_); @@ -150,11 +156,66 @@ public: } } + /*! + * \brief Evaluate the inverse function + * \param x the x-coordinate + * \note We extrapolate linearly if out of bounds + * \note Throws exception if inverse could not be found (e.g. not unique) + */ + Scalar evalInverse(const Scalar y) const + { + if (increasing_) + { + if (y <= y_.front()) + return x_.front() + (y - y_.front())/m_.front(); + else if (y > y_.back()) + return x_.back() + (y - y_.back())/m_.back(); + else + { + const auto lookUpIndex = std::distance(y_.begin(), std::lower_bound(y_.begin(), y_.end(), y)); + assert(lookUpIndex != 0); + + return evalInverse_(y, lookUpIndex); + } + } + + else + { + if (y >= y_.front()) + return x_.front() + (y - y_.front())/m_.front(); + else if (y < y_.back()) + return x_.back() + (y - y_.back())/m_.back(); + else + { + const auto lookUpIndex = y_.size() - std::distance(y_.rbegin(), std::lower_bound(y_.rbegin(), y_.rend(), y)); + assert(lookUpIndex != 0); + + return evalInverse_(y, lookUpIndex); + } + } + } + private: + Scalar evalInverse_(const Scalar y, const std::size_t lookUpIndex) const + { + auto localPolynomial = [&](const auto x) { + // interpolate parametrization parameter t in [0,1] + const auto h = (x_[lookUpIndex] - x_[lookUpIndex-1]); + const auto t = (x - x_[lookUpIndex-1])/h; + return y - (y_[lookUpIndex-1]*Basis::h00(t) + h*m_[lookUpIndex-1]*Basis::h10(t) + + y_[lookUpIndex]*Basis::h01(t) + h*m_[lookUpIndex]*Basis::h11(t)); + }; + + // use an epsilon for the bracket + const auto eps = (x_[lookUpIndex]-x_[lookUpIndex-1])*1e-5; + return findScalarRootBrent(x_[lookUpIndex-1]-eps, x_[lookUpIndex]+eps, localPolynomial); + } + std::vector<Scalar> x_; //!< the x-coordinates std::vector<Scalar> y_; //!< the y-coordinates std::vector<Scalar> m_; //!< the slope for each control point std::size_t numPoints_; //!< the number of control points + bool increasing_; //!< if we are increasing monotone or not }; } // end namespace Dumux diff --git a/test/common/spline/test_monotonecubicspline.cc b/test/common/spline/test_monotonecubicspline.cc index 378efe2648..4ff3623881 100644 --- a/test/common/spline/test_monotonecubicspline.cc +++ b/test/common/spline/test_monotonecubicspline.cc @@ -79,6 +79,34 @@ int main(int argc, char** argv) if (maxNorm > 0.0008 || maxNormDeriv > 0.013) DUNE_THROW(Dune::Exception, "Maximum error in spline interpolation too large!"); + // test inverse by evaluating (x = f^-1(f(x))) for monotonically increasing function + { + const auto resultX = eval([&](double x){ return spline.evalInverse(spline.eval(x)); }, testPoints); + auto diffInverse = resultX; + std::transform(resultX.begin(), resultX.end(), testPoints.begin(), diffInverse.begin(), [](auto a, auto b){ return std::abs(a-b); }); + const auto maxNormInverse = std::accumulate(diffInverse.begin(), diffInverse.end(), diffInverse[0], [](auto a, auto b){ return std::max(a, b); }) + /(*std::max_element(testPoints.begin(), testPoints.end())); + + + std::cout << "Maximum error in identity using the inverse (mon. incr.): " << std::scientific << maxNormInverse << "\n"; + if (maxNormInverse > 1e-13) + DUNE_THROW(Dune::Exception, "Maximum error in spline interpolation too large!"); + } + // test inverse by evaluating (x = f^-1(f(x))) for monotonically decreasing function + { + auto reverseTest = testPoints; + std::reverse(reverseTest.begin(), reverseTest.end()); + const auto resultX = eval([&](double x){ return spline.evalInverse(spline.eval(x)); }, reverseTest); + auto diffInverse = resultX; + std::transform(resultX.begin(), resultX.end(), reverseTest.begin(), diffInverse.begin(), [](auto a, auto b){ return std::abs(a-b); }); + const auto maxNormInverse = std::accumulate(diffInverse.begin(), diffInverse.end(), diffInverse[0], [](auto a, auto b){ return std::max(a, b); }) + /(*std::max_element(reverseTest.begin(), reverseTest.end())); + + std::cout << "Maximum error in identity using the inverse (mon. decr.): " << std::scientific << maxNormInverse << "\n"; + if (maxNormInverse > 1e-13) + DUNE_THROW(Dune::Exception, "Maximum error in spline interpolation too large!"); + } + // plot with Gnuplot (plot a bit more so we can see the linear extension) const auto plotPoints = Dumux::linspace(-1.0, 5.0, 1000); const auto refPlot = eval(f, plotPoints); -- GitLab