[libcxx-commits] [libcxx] [libc++][math] Mathematical special functions: Implementing `std::legendre` (PR #92175)

via libcxx-commits libcxx-commits at lists.llvm.org
Sun Jul 21 09:33:49 PDT 2024


https://github.com/PaulXiCao updated https://github.com/llvm/llvm-project/pull/92175

>From 9c6716e910664ae0c3348f400a7728ef8d2109ff Mon Sep 17 00:00:00 2001
From: Paul <{ID}+{username}@users.noreply.github.com>
Date: Sun, 12 May 2024 21:48:25 +0200
Subject: [PATCH 1/2] implementation + unit

---
 libcxx/include/__math/special_functions.h     |  49 +++++++-
 libcxx/include/cmath                          |   8 ++
 libcxx/modules/std/cmath.inc                  |   2 +
 .../std/numerics/c.math/legendre.pass.cpp     | 111 ++++++++++++++++++
 4 files changed, 169 insertions(+), 1 deletion(-)
 create mode 100644 libcxx/test/std/numerics/c.math/legendre.pass.cpp

diff --git a/libcxx/include/__math/special_functions.h b/libcxx/include/__math/special_functions.h
index 0b1c753a659ad..8b2b962a85a4d 100644
--- a/libcxx/include/__math/special_functions.h
+++ b/libcxx/include/__math/special_functions.h
@@ -11,11 +11,13 @@
 #define _LIBCPP___MATH_SPECIAL_FUNCTIONS_H
 
 #include <__config>
+#include <__math/abs.h>
 #include <__math/copysign.h>
 #include <__math/traits.h>
 #include <__type_traits/enable_if.h>
 #include <__type_traits/is_integral.h>
 #include <limits>
+#include <stdexcept>
 
 #if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
 #  pragma GCC system_header
@@ -49,7 +51,7 @@ _LIBCPP_HIDE_FROM_ABI _Real __hermite(unsigned __n, _Real __x) {
   }
 
   if (!__math::isfinite(__H_n)) {
-    // Overflow occured. Two possible cases:
+    // Overflow occurred. Two possible cases:
     //    n is odd:  return infinity of the same sign as x.
     //    n is even: return +Inf
     _Real __inf = std::numeric_limits<_Real>::infinity();
@@ -77,6 +79,51 @@ _LIBCPP_HIDE_FROM_ABI double hermite(unsigned __n, _Integer __x) {
   return std::hermite(__n, static_cast<double>(__x));
 }
 
+template <class _Real>
+_LIBCPP_HIDE_FROM_ABI _Real __legendre(unsigned __n, _Real __x) {
+  // The Legendre polynomial P_n(x).
+  // The implementation is based on the recurrence formula: (n+1) P_{n+1}(x) = (2n+1) x P_n(x) - n P_{n-1}(x).
+  // Press, William H., et al. Numerical recipes 3rd edition: The art of scientific computing.
+  // Cambridge university press, 2007, p. 183.
+
+  // NOLINTBEGIN(readability-identifier-naming)
+  if (__math::isnan(__x))
+    return __x;
+
+  if (__math::fabs(__x) > 1)
+    std::__throw_domain_error("Argument `x` of Legendre function is out of range: `|x| <= 1`.");
+
+  _Real __P_0{1};
+  if (__n == 0)
+    return __P_0;
+
+  _Real __P_n_prev = __P_0;
+  _Real __P_n      = __x;
+  for (unsigned __i = 1; __i < __n; ++__i) {
+    _Real __P_n_next = ((2 * __i + 1) * __x * __P_n - __i * __P_n_prev) / static_cast<_Real>(__i + 1);
+    __P_n_prev       = __P_n;
+    __P_n            = __P_n_next;
+  }
+
+  return __P_n;
+  // NOLINTEND(readability-identifier-naming)
+}
+
+inline _LIBCPP_HIDE_FROM_ABI double legendre(unsigned __n, double __x) { return std::__legendre(__n, __x); }
+
+inline _LIBCPP_HIDE_FROM_ABI float legendre(unsigned __n, float __x) { return std::__legendre(__n, __x); }
+
+inline _LIBCPP_HIDE_FROM_ABI long double legendre(unsigned __n, long double __x) { return std::__legendre(__n, __x); }
+
+inline _LIBCPP_HIDE_FROM_ABI float legendref(unsigned __n, float __x) { return std::legendre(__n, __x); }
+
+inline _LIBCPP_HIDE_FROM_ABI long double legendrel(unsigned __n, long double __x) { return std::legendre(__n, __x); }
+
+template <class _Integer, std::enable_if_t<std::is_integral_v<_Integer>, int> = 0>
+_LIBCPP_HIDE_FROM_ABI double legendre(unsigned __n, _Integer __x) {
+  return std::legendre(__n, static_cast<double>(__x));
+}
+
 #endif // _LIBCPP_STD_VER >= 17
 
 _LIBCPP_END_NAMESPACE_STD
diff --git a/libcxx/include/cmath b/libcxx/include/cmath
index 3c22604a683c3..4473b356ca63c 100644
--- a/libcxx/include/cmath
+++ b/libcxx/include/cmath
@@ -224,6 +224,14 @@ int ilogb (arithmetic x);
 int ilogbf(float x);
 int ilogbl(long double x);
 
+double         legendre(unsigned n, double x);                    // C++17
+float          legendre(unsigned n, float x);                     // C++17
+long double    legendre(unsigned n, long double x);               // C++17
+float          legendref(unsigned n, float x);                    // C++17
+long double    legendrel(unsigned n, long double x);              // C++17
+template <class Integer>
+double         legendre(unsigned n, Integer x);                   // C++17
+
 floating_point lgamma (arithmetic x);
 float          lgammaf(float x);
 long double    lgammal(long double x);
diff --git a/libcxx/modules/std/cmath.inc b/libcxx/modules/std/cmath.inc
index fe8ac773c9d1c..8767a4ac0273c 100644
--- a/libcxx/modules/std/cmath.inc
+++ b/libcxx/modules/std/cmath.inc
@@ -346,12 +346,14 @@ export namespace std {
   using std::laguerre;
   using std::laguerref;
   using std::laguerrel;
+#endif
 
   // [sf.cmath.legendre], Legendre polynomials
   using std::legendre;
   using std::legendref;
   using std::legendrel;
 
+#if 0
   // [sf.cmath.riemann.zeta], Riemann zeta function
   using std::riemann_zeta;
   using std::riemann_zetaf;
diff --git a/libcxx/test/std/numerics/c.math/legendre.pass.cpp b/libcxx/test/std/numerics/c.math/legendre.pass.cpp
new file mode 100644
index 0000000000000..06a3e731c844e
--- /dev/null
+++ b/libcxx/test/std/numerics/c.math/legendre.pass.cpp
@@ -0,0 +1,111 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14
+
+// <cmath>
+
+// double         legendre(unsigned n, double x);
+// float          legendre(unsigned n, float x);
+// long double    legendre(unsigned n, long double x);
+// float          legendref(unsigned n, float x);
+// long double    legendrel(unsigned n, long double x);
+// template <class Integer>
+// double         legendre(unsigned n, Integer x);
+
+#include <cassert>
+#include <cmath>
+#include <limits>
+
+template <class T>
+void testLegendreNaNPropagation() {
+  const unsigned MaxN = 127;
+  const T x           = std::numeric_limits<T>::quiet_NaN();
+  for (unsigned n = 0; n <= MaxN; ++n) {
+    assert(std::isnan(std::legendre(n, x)));
+  }
+}
+
+template <class T>
+void testLegendreNotNaN(const T x) {
+  assert(!std::isnan(x));
+  const unsigned MaxN = 127;
+  for (unsigned n = 0; n <= MaxN; ++n) {
+    assert(!std::isnan(std::legendre(n, x)));
+  }
+}
+
+template <class T>
+void testLegendreThrows(const T x) {
+#ifndef _LIBCPP_NO_EXCEPTIONS
+  const unsigned MaxN = 127;
+  for (unsigned n = 0; n <= MaxN; ++n) {
+    bool Throws = false;
+    try {
+      std::legendre(n, x);
+    } catch (const std::domain_error&) {
+      Throws = true;
+    }
+    assert(Throws);
+  }
+#endif // _LIBCPP_NO_EXCEPTIONS
+}
+
+template <class T>
+void testLegendreAnalytic(const T x, const T AbsTolerance, const T RelTolerance) {
+  assert(!std::isnan(x));
+  const auto compareFloatingPoint = [AbsTolerance, RelTolerance](const T Result, const T ExpectedResult) {
+    if (std::isinf(ExpectedResult) && std::isinf(Result))
+      return true;
+
+    if (std::isnan(ExpectedResult) || std::isnan(Result))
+      return false;
+
+    const T Tolerance = AbsTolerance + std::abs(ExpectedResult) * RelTolerance;
+    return std::abs(Result - ExpectedResult) < Tolerance;
+  };
+
+  const auto l0 = [](T) { return T(1); };
+  const auto l1 = [](T y) { return y; };
+  const auto l2 = [](T y) { return (T(3) * y * y - T(1)) / T(2); };
+  const auto l3 = [](T y) { return (T(5) * y * y - T(3)) * y / T(2); };
+  const auto l4 = [](T y) { return (T(35) * y * y * y * y - T(30) * y * y + T(3)) / T(8); };
+  const auto l5 = [](T y) { return (T(63) * y * y * y * y - T(70) * y * y + T(15)) * y / T(8); };
+  const auto l6 = [](T y) {
+    const T y2 = y * y;
+    return (T(231) * y2 * y2 * y2 - T(315) * y2 * y2 + T(105) * y2 - T(5)) / T(16);
+  };
+
+  assert(compareFloatingPoint(std::legendre(0, x), l0(x)));
+  assert(compareFloatingPoint(std::legendre(1, x), l1(x)));
+  assert(compareFloatingPoint(std::legendre(2, x), l2(x)));
+  assert(compareFloatingPoint(std::legendre(3, x), l3(x)));
+  assert(compareFloatingPoint(std::legendre(4, x), l4(x)));
+  assert(compareFloatingPoint(std::legendre(5, x), l5(x)));
+  assert(compareFloatingPoint(std::legendre(6, x), l6(x)));
+}
+
+template <class T>
+void testLegendre(const T AbsTolerance, const T RelTolerance) {
+  testLegendreNaNPropagation<T>();
+  testLegendreThrows<T>(T(-5));
+  testLegendreThrows<T>(T(5));
+
+  const T Samples[] = {T(-1.0), T(-0.5), T(-0.1), T(0.0), T(0.1), T(0.5), T(1.0)};
+
+  for (T x : Samples) {
+    testLegendreNotNaN(x);
+    testLegendreAnalytic(x, AbsTolerance, RelTolerance);
+  }
+}
+
+int main() {
+  testLegendre<float>(1e-6f, 1e-6f);
+  testLegendre<double>(1e-9, 1e-9);
+  testLegendre<long double>(1e-12, 1e-12);
+}

>From 06b211375d094dfbd798178f10a9f0afc09bffa0 Mon Sep 17 00:00:00 2001
From: Paul <{ID}+{username}@users.noreply.github.com>
Date: Sun, 12 May 2024 23:29:51 +0200
Subject: [PATCH 2/2] structured tests; fixed implementation

---
 .../std/numerics/c.math/legendre.pass.cpp     | 206 ++++++++++++------
 1 file changed, 137 insertions(+), 69 deletions(-)

diff --git a/libcxx/test/std/numerics/c.math/legendre.pass.cpp b/libcxx/test/std/numerics/c.math/legendre.pass.cpp
index 06a3e731c844e..aee7850f0a5e8 100644
--- a/libcxx/test/std/numerics/c.math/legendre.pass.cpp
+++ b/libcxx/test/std/numerics/c.math/legendre.pass.cpp
@@ -18,94 +18,162 @@
 // template <class Integer>
 // double         legendre(unsigned n, Integer x);
 
+#include <array>
 #include <cassert>
 #include <cmath>
 #include <limits>
 
+#include "type_algorithms.h"
+
+inline constexpr unsigned g_max_n = 128;
+
 template <class T>
-void testLegendreNaNPropagation() {
-  const unsigned MaxN = 127;
-  const T x           = std::numeric_limits<T>::quiet_NaN();
-  for (unsigned n = 0; n <= MaxN; ++n) {
-    assert(std::isnan(std::legendre(n, x)));
-  }
+std::array<T, 7> sample_points() {
+  return {-1.0, -0.5, -0.1, 0.0, 0.1, 0.5, 1.0};
 }
 
-template <class T>
-void testLegendreNotNaN(const T x) {
-  assert(!std::isnan(x));
-  const unsigned MaxN = 127;
-  for (unsigned n = 0; n <= MaxN; ++n) {
-    assert(!std::isnan(std::legendre(n, x)));
+template <class Real>
+class CompareFloatingValues {
+private:
+  Real tol;
+
+public:
+  CompareFloatingValues() {
+    if (std::is_same_v<Real, float>)
+      tol = 1e-6f;
+    else if (std::is_same_v<Real, double>)
+      tol = 1e-9;
+    else
+      tol = 1e-12l;
   }
-}
 
-template <class T>
-void testLegendreThrows(const T x) {
-#ifndef _LIBCPP_NO_EXCEPTIONS
-  const unsigned MaxN = 127;
-  for (unsigned n = 0; n <= MaxN; ++n) {
-    bool Throws = false;
-    try {
-      std::legendre(n, x);
-    } catch (const std::domain_error&) {
-      Throws = true;
-    }
-    assert(Throws);
+  bool operator()(Real result, Real expected) const {
+    if (std::isinf(expected) && std::isinf(result))
+      return result == expected;
+
+    if (std::isnan(expected) || std::isnan(result))
+      return false;
+
+    return std::abs(result - expected) < (tol + std::abs(expected) * tol);
+  }
+};
+
+template <class Real>
+void test() {
+  { // checks if NaNs are reported correctly (i.e. output == input for input == NaN)
+    using nl = std::numeric_limits<Real>;
+    for (Real NaN : {nl::quiet_NaN(), nl::signaling_NaN()})
+      for (unsigned n = 0; n < g_max_n; ++n)
+        assert(std::isnan(std::legendre(n, NaN)));
   }
+
+  { // simple sample points for n=0..127 should not produce NaNs.
+    for (Real x : sample_points<Real>())
+      for (unsigned n = 0; n < g_max_n; ++n)
+        assert(!std::isnan(std::legendre(n, x)));
+  }
+
+  { // For x with abs(x) > 1 an domain_error exception should be thrown.
+#ifndef _LIBCPP_NO_EXCEPTIONS
+    for (double absX : {2.0, 7.77, 42.42, std::numeric_limits<double>::infinity()})
+      for (Real x : {-absX, absX})
+        for (unsigned n = 0; n < g_max_n; ++n) {
+          bool throws = false;
+          try {
+            std::legendre(n, x);
+          } catch (const std::domain_error&) {
+            throws = true;
+          }
+          assert(throws);
+        }
 #endif // _LIBCPP_NO_EXCEPTIONS
-}
+  }
 
-template <class T>
-void testLegendreAnalytic(const T x, const T AbsTolerance, const T RelTolerance) {
-  assert(!std::isnan(x));
-  const auto compareFloatingPoint = [AbsTolerance, RelTolerance](const T Result, const T ExpectedResult) {
-    if (std::isinf(ExpectedResult) && std::isinf(Result))
-      return true;
+  { // check against analytic polynoms for order n=0..6
+    for (Real x : sample_points<Real>()) {
+      const auto l0 = [](Real) -> Real { return 1; };
+      const auto l1 = [](Real y) -> Real { return y; };
+      const auto l2 = [](Real y) -> Real { return (3 * y * y - 1) / 2; };
+      const auto l3 = [](Real y) -> Real { return (5 * y * y - 3) * y / 2; };
+      const auto l4 = [](Real y) -> Real { return (35 * std::pow(y, 4) - 30 * y * y + 3) / 8; };
+      const auto l5 = [](Real y) -> Real { return (63 * std::pow(y, 4) - 70 * y * y + 15) * y / 8; };
+      const auto l6 = [](Real y) -> Real {
+        return (231 * std::pow(y, 6) - 315 * std::pow(y, 4) + 105 * y * y - 5) / 16;
+      };
+
+      const CompareFloatingValues<Real> compare;
+      assert(compare(std::legendre(0, x), l0(x)));
+      assert(compare(std::legendre(1, x), l1(x)));
+      assert(compare(std::legendre(2, x), l2(x)));
+      assert(compare(std::legendre(3, x), l3(x)));
+      assert(compare(std::legendre(4, x), l4(x)));
+      assert(compare(std::legendre(5, x), l5(x)));
+      assert(compare(std::legendre(6, x), l6(x)));
+    }
+  }
 
-    if (std::isnan(ExpectedResult) || std::isnan(Result))
-      return false;
+  { // checks std::legendref for bitwise equality with std::legendre(unsigned, float)
+    if constexpr (std::is_same_v<Real, float>)
+      for (unsigned n = 0; n < g_max_n; ++n)
+        for (float x : sample_points<float>())
+          assert(std::legendre(n, x) == std::legendref(n, x));
+  }
 
-    const T Tolerance = AbsTolerance + std::abs(ExpectedResult) * RelTolerance;
-    return std::abs(Result - ExpectedResult) < Tolerance;
-  };
-
-  const auto l0 = [](T) { return T(1); };
-  const auto l1 = [](T y) { return y; };
-  const auto l2 = [](T y) { return (T(3) * y * y - T(1)) / T(2); };
-  const auto l3 = [](T y) { return (T(5) * y * y - T(3)) * y / T(2); };
-  const auto l4 = [](T y) { return (T(35) * y * y * y * y - T(30) * y * y + T(3)) / T(8); };
-  const auto l5 = [](T y) { return (T(63) * y * y * y * y - T(70) * y * y + T(15)) * y / T(8); };
-  const auto l6 = [](T y) {
-    const T y2 = y * y;
-    return (T(231) * y2 * y2 * y2 - T(315) * y2 * y2 + T(105) * y2 - T(5)) / T(16);
-  };
-
-  assert(compareFloatingPoint(std::legendre(0, x), l0(x)));
-  assert(compareFloatingPoint(std::legendre(1, x), l1(x)));
-  assert(compareFloatingPoint(std::legendre(2, x), l2(x)));
-  assert(compareFloatingPoint(std::legendre(3, x), l3(x)));
-  assert(compareFloatingPoint(std::legendre(4, x), l4(x)));
-  assert(compareFloatingPoint(std::legendre(5, x), l5(x)));
-  assert(compareFloatingPoint(std::legendre(6, x), l6(x)));
-}
+  { // checks std::legendrel for bitwise equality with std::legendre(unsigned, long double)
+    if constexpr (std::is_same_v<Real, long double>)
+      for (unsigned n = 0; n < g_max_n; ++n)
+        for (long double x : sample_points<long double>()) {
+          assert(std::legendre(n, x) == std::legendrel(n, x));
+          assert(std::legendre(n, x) == std::legendrel(n, x));
+        }
+  }
 
-template <class T>
-void testLegendre(const T AbsTolerance, const T RelTolerance) {
-  testLegendreNaNPropagation<T>();
-  testLegendreThrows<T>(T(-5));
-  testLegendreThrows<T>(T(5));
+  { // evaluation at x=1: P_n(1) = 1
+    const CompareFloatingValues<Real> compare;
+    for (unsigned n = 0; n < g_max_n; ++n)
+      assert(compare(std::legendre(n, Real{1}), 1));
+  }
 
-  const T Samples[] = {T(-1.0), T(-0.5), T(-0.1), T(0.0), T(0.1), T(0.5), T(1.0)};
+  { // evaluation at x=-1: P_n(-1) = (-1)^n
+    const CompareFloatingValues<Real> compare;
+    for (unsigned n = 0; n < g_max_n; ++n)
+      assert(compare(std::legendre(n, Real{-1}), std::pow(-1, n)));
+  }
 
-  for (T x : Samples) {
-    testLegendreNotNaN(x);
-    testLegendreAnalytic(x, AbsTolerance, RelTolerance);
+  { // evaluation at x=0:
+    //    P_{2n  }(0) = (-1)^n (2n-1)!! / (2n)!!
+    //    P_{2n+1}(0) = 0
+    const CompareFloatingValues<Real> compare;
+    Real doubleFactorials{1};
+    for (unsigned n = 0; n < g_max_n; ++n) {
+      if (n & 1) // odd
+        doubleFactorials *= n;
+      else if (n != 0) // even and not zero
+        doubleFactorials /= n;
+
+      assert(compare(std::legendre(n, Real{0}), (n & 1) ? Real{0} : std::pow(-1, n / 2) * doubleFactorials));
+    }
   }
 }
 
+struct TestFloat {
+  template <class Real>
+  void operator()() {
+    test<Real>();
+  }
+};
+
+struct TestInt {
+  template <class Integer>
+  void operator()() {
+    // checks that std::legendre(unsigned, Integer) actually wraps std::legendre(unsigned, double)
+    for (unsigned n = 0; n < g_max_n; ++n)
+      for (Integer x : {-1, 0, 1})
+        assert(std::legendre(n, x) == std::legendre(n, static_cast<double>(x)));
+  }
+};
+
 int main() {
-  testLegendre<float>(1e-6f, 1e-6f);
-  testLegendre<double>(1e-9, 1e-9);
-  testLegendre<long double>(1e-12, 1e-12);
+  types::for_each(types::floating_point_types(), TestFloat());
+  types::for_each(types::type_list<short, int, long, long long>(), TestInt());
 }



More information about the libcxx-commits mailing list