Skip to content

Commit 46b9c28

Browse files
authored
BUG: missing complex lqmn derivatives when |z|<1.001 (#62)
1 parent d1ce937 commit 46b9c28

File tree

2 files changed

+140
-9
lines changed

2 files changed

+140
-9
lines changed

include/xsf/legendre.h

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,17 +1046,17 @@ void lqmn(std::complex<T> z, OutputMat1 cqm, OutputMat2 cqd) {
10461046
cq1 = cqf;
10471047
}
10481048
}
1049+
}
10491050

1050-
cqd(0, 0) = static_cast<T>(ls) / zs;
1051-
for (j = 1; j <= n; j++) {
1052-
cqd(0, j) = ls * static_cast<T>(j) * (cqm(0, j - 1) - z * cqm(0, j)) / zs;
1053-
}
1051+
cqd(0, 0) = static_cast<T>(ls) / zs;
1052+
for (j = 1; j <= n; j++) {
1053+
cqd(0, j) = ls * static_cast<T>(j) * (cqm(0, j - 1) - z * cqm(0, j)) / zs;
1054+
}
10541055

1055-
for (i = 1; i <= m; i++) {
1056-
for (j = 0; j <= n; j++) {
1057-
cqd(i, j) = static_cast<T>(ls * i) * z / zs * cqm(i, j) +
1058-
static_cast<T>((i + j) * (j - i + 1)) / zq * cqm(i - 1, j);
1059-
}
1056+
for (i = 1; i <= m; i++) {
1057+
for (j = 0; j <= n; j++) {
1058+
cqd(i, j) = static_cast<T>(ls * i) * z / zs * cqm(i, j) +
1059+
static_cast<T>((i + j) * (j - i + 1)) / zq * cqm(i - 1, j);
10601060
}
10611061
}
10621062
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#include "../testing_utils.h"
2+
3+
#include <xsf/legendre.h>
4+
5+
// backport of std::mdspan (since C++23)
6+
#define MDSPAN_USE_PAREN_OPERATOR 1
7+
#include <xsf/third_party/kokkos/mdspan.hpp>
8+
9+
// From https://github.com/scipy/scipy/blob/bdd3b0e/scipy/special/tests/test_legendre.py#L693-L697
10+
TEST_CASE("lqmn TestLegendreFunctions.test_lqmn", "[lqmn][lqn][real][smoketest]") {
11+
constexpr double atol = 1.5e-4;
12+
13+
constexpr int m = 0;
14+
constexpr int n = 2;
15+
constexpr double x = 0.5;
16+
17+
constexpr int m1p = m + 1;
18+
constexpr int n1p = n + 1;
19+
// lqmn requires buffer space for at least 2x2
20+
constexpr int bufsize = std::max(2, m1p) * std::max(2, n1p);
21+
22+
// lqmnf = special.lqmn(0, 2, .5)
23+
double lqmnf0_data[bufsize], lqmnf1_data[bufsize];
24+
auto lqmnf0 = std::mdspan(lqmnf0_data, m1p, n1p);
25+
auto lqmnf1 = std::mdspan(lqmnf1_data, m1p, n1p);
26+
xsf::lqmn(x, lqmnf0, lqmnf1);
27+
28+
// lqf = special.lqn(2, .5)
29+
double lqf0_data[n1p], lqf1_data[n1p];
30+
auto lqf0 = std::mdspan(lqf0_data, n1p);
31+
auto lqf1 = std::mdspan(lqf1_data, n1p);
32+
xsf::lqn(x, lqf0, lqf1);
33+
34+
// assert_allclose(lqmnf[0][0], lqf[0], atol=1.5e-4, rtol=0)
35+
for (int n = 0; n < n1p; ++n) {
36+
auto error0 = xsf::extended_absolute_error(lqmnf0(0, n), lqf0(n));
37+
CAPTURE(n, lqmnf0(0, n), lqf0(n), error0);
38+
REQUIRE(error0 <= atol);
39+
}
40+
41+
// assert_allclose(lqmnf[1][0], lqf[1], atol=1.5e-4, rtol=0)
42+
for (int n = 0; n < n1p; ++n) {
43+
auto error1 = xsf::extended_absolute_error(lqmnf1(0, n), lqf1(n));
44+
CAPTURE(n, lqmnf1(0, n), lqf1(n), error1);
45+
REQUIRE(error1 <= atol);
46+
}
47+
48+
// some additional smoke checks
49+
for (int n = 0; n < n1p; ++n) {
50+
CAPTURE(n, lqmnf0(0, n));
51+
REQUIRE(lqmnf0(0, n) != 0.0);
52+
REQUIRE(std::isfinite(lqmnf0(0, n)));
53+
}
54+
for (int n = 0; n < n1p; ++n) {
55+
CAPTURE(n, lqmnf1(0, n));
56+
REQUIRE(lqmnf1(0, n) != 0.0);
57+
REQUIRE(std::isfinite(lqmnf1(0, n)));
58+
}
59+
}
60+
61+
// From https://github.com/scipy/scipy/blob/bdd3b0e/scipy/special/tests/test_legendre.py#L699-L708
62+
TEST_CASE("lqmn TestLegendreFunctions.test_lqmn_gt1", "[lqmn][real][smoketest]") {
63+
constexpr double atol = 1.5e-7;
64+
65+
constexpr int m = 2;
66+
constexpr int n = 1;
67+
constexpr int m1p = m + 1;
68+
constexpr int n1p = n + 1;
69+
70+
double lqmnf0_data[m1p * n1p], lqmnf1_data[m1p * n1p];
71+
auto lqmnf0 = std::mdspan(lqmnf0_data, m1p, n1p);
72+
auto lqmnf1 = std::mdspan(lqmnf1_data, m1p, n1p);
73+
74+
// algorithm for real arguments changes at 1.0001
75+
// test against analytical result for m=2, n=1
76+
auto x0 = 1.0001;
77+
auto delta = 0.00002;
78+
79+
// for x in (x0-delta, x0+delta):
80+
for (double x : {x0 - delta, x0 + delta}) {
81+
// lq = special.lqmn(2, 1, x)[0][-1, -1]
82+
xsf::lqmn(x, lqmnf0, lqmnf1);
83+
double lq = lqmnf0(m, n); // _[-1, -1] corresponds to _[m, n]
84+
85+
// expected = 2/(x*x-1)
86+
double expected = 2.0 / (x * x - 1.0);
87+
88+
// assert_allclose(lq, expected, atol=1.5e-7, rtol=0)
89+
auto error = xsf::extended_absolute_error(lq, expected);
90+
CAPTURE(x, lq, expected, error, atol);
91+
REQUIRE(error <= atol);
92+
}
93+
}
94+
95+
TEST_CASE("lqmn complex", "[lqmn][complex][smoketest]") {
96+
constexpr double atol = 1e-16;
97+
constexpr double x = 0.5;
98+
// lqmn requires buffer space for at least 2x2
99+
constexpr int bufsize = 2 * 2;
100+
101+
// (q_mn, qp_mn) = lqmn(0, 0, 0.5)
102+
double q_data[bufsize], qp_data[bufsize];
103+
auto q_mn = std::mdspan(q_data, 1, 1);
104+
auto qp_mn = std::mdspan(qp_data, 1, 1);
105+
xsf::lqmn(x, q_mn, qp_mn);
106+
auto q = q_mn(0, 0);
107+
auto qp = qp_mn(0, 0);
108+
109+
// (cq_mn, cqp_mn) = lqmn(0, 0, 0.5 + 0j)
110+
std::complex<double> cq_data[bufsize], cqp_data[bufsize];
111+
auto cq_mn = std::mdspan(cq_data, 1, 1);
112+
auto cqp_mn = std::mdspan(cqp_data, 1, 1);
113+
xsf::lqmn(std::complex<double>(x, 0.0), cq_mn, cqp_mn);
114+
auto cq = cq_mn(0, 0);
115+
auto cqp = cqp_mn(0, 0);
116+
117+
// abs(_.imag) <= atol
118+
CAPTURE(q, qp, cq, cqp, atol);
119+
CHECK(std::abs(std::imag(cq)) <= atol);
120+
CHECK(std::abs(std::imag(cqp)) <= atol);
121+
122+
// abs(q - cq.real) <= atol
123+
auto err_q = xsf::extended_absolute_error(q, std::real(cq));
124+
CAPTURE(err_q);
125+
REQUIRE(err_q <= atol);
126+
127+
// abs(qp - cqp.real) <= atol
128+
auto err_qp = xsf::extended_absolute_error(qp, std::real(cqp));
129+
CAPTURE(err_qp);
130+
REQUIRE(err_qp <= atol);
131+
}

0 commit comments

Comments
 (0)