diff --git a/dumux/common/math.hh b/dumux/common/math.hh index 2b8679b1891ef37363b59fa5daaafcbc246a8501..99799fbc2aac2e45af60ec7528194635be679556 100644 --- a/dumux/common/math.hh +++ b/dumux/common/math.hh @@ -330,8 +330,14 @@ int invertCubicPolynomial(SolContainer *sol, // calculate the cube root of - q/2 + sqrt(q^2/4 + p^3/27) using std::cbrt; using std::sqrt; - Scalar u = cbrt(-q/2 + sqrt(wDisc)); + // Choose the root that is safe against the loss of precision + // that can cause wDisc to be equal to q*q/4 despite p != 0. + // We do not want u to be zero in that case. Mathematically, + // we are happy with either root. + const Scalar u = [&]{ + return q > 0 ? cbrt(-0.5*q - sqrt(wDisc)) : cbrt(-0.5*q + sqrt(wDisc)); + }(); // at this point, u != 0 since p^3 = 0 is necessary in order // for u = 0 to hold, so sol[0] = u - p/(3*u) - b/3; diff --git a/test/common/math/test_math.cc b/test/common/math/test_math.cc index c4a93b702467c6ddd287b2f7bd73d416d16b93d9..00d3b929080a4e4ea3ca032d07bb52ec68f0b922 100644 --- a/test/common/math/test_math.cc +++ b/test/common/math/test_math.cc @@ -33,6 +33,9 @@ #include <iostream> #include <utility> #include <algorithm> +#include <array> +#include <random> +#include <tuple> #include <dune/common/float_cmp.hh> #include <dune/common/fmatrix.hh> @@ -235,4 +238,100 @@ int main() DUNE_THROW(Dune::Exception, "[linearRegreesion] Wrong slope " << slope << ", should be 2.0."); } + ////////////////////////////////////////////////////////////////// + ///// Dumux::invertCubicPolynomial /////////////////////////////// + ////////////////////////////////////////////////////////////////// + + const auto cubicCoefficientsFromRoots = [](auto x, auto y, auto z, double scale = 1.0){ + const auto b = -(x + y + z); + const auto c = x*y + x*z + y*z; + const auto d = -x*y*z; + // scaling doesn't change the roots + return std::make_tuple(1.0*scale, b*scale, c*scale, d*scale); + }; + + const auto quadCoefficientsFromRoots = [](auto x, auto y, double scale = 1.0){ + const auto b = -(x + y); + const auto c = x*y; + return std::make_tuple(1.0*scale, b*scale, c*scale); + }; + + const auto linearCoefficientsFromRoots = [](auto x, double scale = 1.0){ + return std::make_tuple(1.0*scale, -x*scale); + }; + + std::mt19937 gen(std::random_device{}()); + std::uniform_real_distribution<> dis(-10.0, 10.0); + auto threeRandomNumbers = [&]() mutable { + const auto x = dis(gen); + auto y = dis(gen); while (x == y) { y = dis(gen); } + auto z = dis(gen); while (x == z || y == z) { z = dis(gen); } + return std::make_tuple(x, y, z); + }; + + const auto checkRoots = [](const std::array<double, 3>& roots, + const std::array<double, 3>& ref, + int n) + { + for (int i = 0; i < n; i++) + { + using std::isfinite; + if (!isfinite(roots[i])) + DUNE_THROW(Dune::Exception, "Expecting finite root"); + if (std::none_of(ref.begin(), ref.end(), [&](auto r){ return Dune::FloatCmp::eq(roots[i], r, 1e-10); })) + DUNE_THROW(Dune::Exception, "[invertCubicPolynomial] Root " + << i << " of " << n << ": " << roots[i] + << " does not match reference"); + } + }; + + std::array<double, 3> roots{}; + for (double scaling : {1.0, -1.0, 5.0}) + { + for (int i = 0; i < 1000; ++i) + { + const auto [x, y, z] = threeRandomNumbers(); + + // test for three unique roots + { + const auto [a, b, c, d] = cubicCoefficientsFromRoots(x, y, z, scaling); + int numRoots = Dumux::invertCubicPolynomial(roots.data(), a, b, c, d); + if (numRoots != 3) + DUNE_THROW(Dune::Exception, "Expecting three roots! Got " << numRoots); + checkRoots(roots, {x, y, z}, numRoots); + } + + // test for two unique roots + { + const auto [b, c, d] = quadCoefficientsFromRoots(x, z, scaling); + int numRoots = Dumux::invertCubicPolynomial(roots.data(), 0.0, b, c, d); + if (numRoots != 2) + DUNE_THROW(Dune::Exception, "Expecting two roots! Got " << numRoots); + checkRoots(roots, {x, z}, numRoots); + } + + // test for one unique root + { + const auto [c, d] = linearCoefficientsFromRoots(x, scaling); + int numRoots = Dumux::invertCubicPolynomial(roots.data(), 0.0, 0.0, c, d); + if (numRoots != 1) + DUNE_THROW(Dune::Exception, "Expecting one root! Got " << numRoots); + checkRoots(roots, {x}, numRoots); + } + } + } + + // Test one corner case (see MR!2594) + int nroots = Dumux::invertCubicPolynomial(roots.data(), + 1.0, -0.96165703943410097, 0.30826068470787077, 0.01340255221155587 + ); + if (nroots != 1) + DUNE_THROW(Dune::Exception, "Expecting one root"); + using std::isfinite; + if (!isfinite(roots[0])) + DUNE_THROW(Dune::Exception, "Expecting finite root"); + if (!Dune::FloatCmp::eq(roots[0], -3.863448718244389e-02, 1e-14)) + DUNE_THROW(Dune::Exception, "Root of cubic equation does not match reference"); + + return 0; }