Skip to content

Commit 4b8e4b2

Browse files
Size the T_l recurrence passes of the analytic add-back by their growth rate
1 parent b823d61 commit 4b8e4b2

2 files changed

Lines changed: 94 additions & 137 deletions

File tree

src/vmecpp/cpp/vmecpp/free_boundary/singular_integrals/singular_integrals.cc

Lines changed: 58 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,66 @@
88
#include <cmath>
99
#include <cstdlib>
1010
#include <cstring>
11+
#include <numbers>
1112
#include <vector>
1213

1314
namespace vmecpp {
1415

16+
namespace {
17+
18+
// Logarithm of the growth of the homogeneous solutions of the T_l recurrence
19+
// over a forward pass, sqrt(B/A)^kL, above which the recurrence is run
20+
// backward instead; a forward pass amplifies the rounding of its inputs by at
21+
// most this factor, ten.
22+
constexpr double kMaxForwardLogGrowth = std::numbers::ln10;
23+
24+
// The zero seed of a backward pass contaminates T_l by (A/B)^{(top - l)/2} of
25+
// T_top; the pass starts far enough above kL to bring that below 1e-17 at kL.
26+
constexpr double kMinSeedLogDecay = 17.0 * std::numbers::ln10;
27+
28+
// T_l = int_{-1}^{1} t^l / sqrt(A t^2 + 2 d t + B) dt for l = 0, ..., kL at
29+
// tangential grid point kl from the three-term recurrence
30+
// (l + 1) A T_{l+1} + (2 l + 1) d T_l + l B T_{l-1} = sqrtc2 - (-1)^l sqrta2
31+
// with T_0 given. The characteristic roots of the homogeneous recurrence are a
32+
// complex pair of modulus sqrt(B/A), so a forward pass amplifies the rounding
33+
// of T_0 and of the right-hand sides by sqrt(B/A)^l and is used while that
34+
// stays below exp(kMaxForwardLogGrowth); otherwise the recurrence runs
35+
// backward from a zero seed, whose contamination decays by sqrt(A/B) per step.
36+
void ComputeTl(double A, double B, double d, double sqrtc2, double sqrta2,
37+
double T0, int kL, int kl, std::vector<Eigen::VectorXd>& m_T) {
38+
const auto rhs = [sqrtc2, sqrta2](int l) {
39+
return sqrtc2 + (l % 2 == 0 ? -sqrta2 : sqrta2);
40+
};
41+
m_T[0][kl] = T0;
42+
const double log_ratio = std::log(B / A);
43+
if (kL * log_ratio <= 2.0 * kMaxForwardLogGrowth) {
44+
double T_prev = 0.0; // T_{-1}
45+
for (int l = 0; l < kL; ++l) {
46+
const double T_next =
47+
(rhs(l) - (2 * l + 1) * d * m_T[l][kl] - l * B * T_prev) /
48+
((l + 1) * A);
49+
T_prev = m_T[l][kl];
50+
m_T[l + 1][kl] = T_next;
51+
}
52+
return;
53+
}
54+
const int tail =
55+
static_cast<int>(std::ceil(2.0 * kMinSeedLogDecay / log_ratio));
56+
double T_hi = 0.0; // T_{l+1}
57+
double T_cur = 0.0; // T_l
58+
for (int l = kL + tail; l >= 1; --l) {
59+
const double T_lo =
60+
(rhs(l) - (2 * l + 1) * d * T_cur - (l + 1) * A * T_hi) / (l * B);
61+
T_hi = T_cur;
62+
T_cur = T_lo;
63+
if (1 <= l - 1 && l - 1 <= kL) {
64+
m_T[l - 1][kl] = T_lo;
65+
}
66+
}
67+
}
68+
69+
} // namespace
70+
1571
SingularIntegrals::SingularIntegrals(const Sizes* s,
1672
const FourierBasisFastToroidal* fb,
1773
const TangentialPartitioning* tp,
@@ -220,117 +276,9 @@ void SingularIntegrals::prepareUpdate(
220276
(sqrtam * sqrta2[kl] - am[kl] + d[kl])) /
221277
sqrtam;
222278

223-
// Fill all Tlp[0..L] and Tlm[0..L] by picking the numerically stable
224-
// direction of the three-term recurrence on a per-(+/-), per-kl basis.
225-
//
226-
// The characteristic roots of the homogeneous recurrence satisfy
227-
// A*r^2 + 2*d*r + B = 0 -> |r1 r2| = B/A.
228-
// For T^+: (A, B) = (ap, am), so |r1 r2| = am/ap.
229-
// For T^-: (A, B) = (am, ap), so |r1 r2| = ap/am.
230-
// If B > A (at least one |r| > 1), forward iteration is unstable and
231-
// backward (Miller's algorithm) is used instead; otherwise forward is fine.
232-
//
233-
// T^{\pm}_0 is analytic (above); T^{\pm}_{-1} = 0. Forward produces
234-
// T_{l+1} from T_l and T_{l-1}; backward produces T_{l-1} from T_l and
235-
// T_{l+1} via the same recurrence solved in reverse. For backward,
236-
// iteration starts from a zero seed far above the required L; the result
237-
// is then normalized to match the analytic T^{\pm}_0.
238-
//
239-
// rhs(l+1) = sqrtc2 + (-1)^{l+1}*sqrta2 (same for T^+ and T^-).
240279
const int kL = mf + nf;
241-
// The spurious solution is damped by (A/B)^kTailExtra per pass.
242-
// For the worst realistic ratio (A/B ~ 0.5) suppression is ~0.5^50 ~ 1e-16.
243-
const int kTailExtra = 50;
244-
const int kLtail = kL + kTailExtra;
245-
246-
// Only switch to backward when the forward spurious-mode growth
247-
// (|r1 r2| = B/A) would actually exceed double precision over kL steps.
248-
// Threshold: forward is considered stable as long as (B/A)^kL < 1e10,
249-
// i.e. spurious amplitude stays within ~1e10 of the particular solution.
250-
// Near-degenerate kl (|r1|~|r2|~1) fall in the forward branch, where
251-
// zero-seed Miller is known to misconverge (spurious modes never damp).
252-
// Formula: kL * ln(B/A) < ln(1e10) -> B/A < exp(ln(1e10)/kL).
253-
constexpr double kLogGrowthThreshold = 10.0 * 2.30258509299; // ln(1e10)
254-
const double logRatioP =
255-
(am[kl] > ap[kl] && ap[kl] > 0.0) ? std::log(am[kl] / ap[kl]) : 0.0;
256-
const bool useBackwardP =
257-
static_cast<double>(kL) * logRatioP > kLogGrowthThreshold;
258-
const double logRatioM =
259-
(ap[kl] > am[kl] && am[kl] > 0.0) ? std::log(ap[kl] / am[kl]) : 0.0;
260-
const bool useBackwardM =
261-
static_cast<double>(kL) * logRatioM > kLogGrowthThreshold;
262-
263-
// --- T^+: A = ap, B = am ---
264-
Tlp[0][kl] = T0p;
265-
if (useBackwardP) {
266-
// forward unstable -> use backward recurrence.
267-
double T_hi = 0.0;
268-
double T_cur = 1.0e-300;
269-
for (int l = kLtail; l >= 1; --l) {
270-
const double rhs = sqrtc2[kl] + (l % 2 == 0 ? -1.0 : 1.0) * sqrta2[kl];
271-
const double T_lo =
272-
(rhs - (2 * l + 1) * d[kl] * T_cur - (l + 1) * ap[kl] * T_hi) /
273-
(l * am[kl]);
274-
T_hi = T_cur;
275-
T_cur = T_lo;
276-
if (l - 1 <= kL) {
277-
Tlp[l - 1][kl] = T_lo;
278-
}
279-
}
280-
const double scaleP = T0p / Tlp[0][kl];
281-
for (int l = 0; l <= kL; ++l) {
282-
Tlp[l][kl] *= scaleP;
283-
}
284-
} else {
285-
// forward stable.
286-
double T_prev = 0.0; // T^+_{-1}
287-
int sgn = 1;
288-
for (int fl = 0; fl < kL; ++fl) {
289-
sgn = -sgn;
290-
const double rhs = sqrtc2[kl] + sgn * sqrta2[kl];
291-
const double T_next =
292-
(rhs - (2 * fl + 1) * d[kl] * Tlp[fl][kl] - fl * am[kl] * T_prev) /
293-
(ap[kl] * (fl + 1));
294-
T_prev = Tlp[fl][kl];
295-
Tlp[fl + 1][kl] = T_next;
296-
}
297-
}
298-
299-
// --- T^-: A = am, B = ap ---
300-
Tlm[0][kl] = T0m;
301-
if (useBackwardM) {
302-
// forward unstable -> use backward recurrence.
303-
double T_hi = 0.0;
304-
double T_cur = 1.0e-300;
305-
for (int l = kLtail; l >= 1; --l) {
306-
const double rhs = sqrtc2[kl] + (l % 2 == 0 ? -1.0 : 1.0) * sqrta2[kl];
307-
const double T_lo =
308-
(rhs - (2 * l + 1) * d[kl] * T_cur - (l + 1) * am[kl] * T_hi) /
309-
(l * ap[kl]);
310-
T_hi = T_cur;
311-
T_cur = T_lo;
312-
if (l - 1 <= kL) {
313-
Tlm[l - 1][kl] = T_lo;
314-
}
315-
}
316-
const double scaleM = T0m / Tlm[0][kl];
317-
for (int l = 0; l <= kL; ++l) {
318-
Tlm[l][kl] *= scaleM;
319-
}
320-
} else {
321-
// forward stable.
322-
double T_prev = 0.0; // T^-_{-1}
323-
int sgn = 1;
324-
for (int fl = 0; fl < kL; ++fl) {
325-
sgn = -sgn;
326-
const double rhs = sqrtc2[kl] + sgn * sqrta2[kl];
327-
const double T_next =
328-
(rhs - (2 * fl + 1) * d[kl] * Tlm[fl][kl] - fl * ap[kl] * T_prev) /
329-
(am[kl] * (fl + 1));
330-
T_prev = Tlm[fl][kl];
331-
Tlm[fl + 1][kl] = T_next;
332-
}
333-
}
280+
ComputeTl(ap[kl], am[kl], d[kl], sqrtc2[kl], sqrta2[kl], T0p, kL, kl, Tlp);
281+
ComputeTl(am[kl], ap[kl], d[kl], sqrtc2[kl], sqrta2[kl], T0m, kL, kl, Tlm);
334282
} // kl
335283
} // prepareUpdate
336284

src/vmecpp/cpp/vmecpp/free_boundary/singular_integrals/singular_integrals_test.cc

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -164,24 +164,33 @@ TEST(TestSingularIntegrals, CheckConstants) {
164164
} // CheckConstants
165165

166166
// Verify that prepareUpdate computes T^{+/-}_l accurately for ALL l in [0, L]
167-
// at a range of Fourier resolutions.
167+
// at a range of Fourier resolutions and metric coefficients.
168168
//
169-
// Resolutions span the low-kL regime (forward recurrence is accurate, so the
170-
// forward branch of prepareUpdate is exercised) all the way up to high-kL
171-
// (Miller backward recurrence fires; forward would lose all digits).
169+
// The cases span the low-kL regime, where the forward recurrence is accurate,
170+
// up to kL = 45, where one direction of each recurrence loses all digits and
171+
// the other has to run backward from a seed far above kL.
172172
//
173173
// Reference: 64-point Gauss-Legendre quadrature of the defining integral.
174174
// This is independent of both recurrence directions and reaches ~1e-13
175175
// relative accuracy at the chosen coefficients up to l = 45.
176-
class TlpTlmAccuracyTest
177-
: public ::testing::TestWithParam<std::pair<int, int>> {};
176+
struct TlCase {
177+
int mpol;
178+
int ntor;
179+
// metric coefficients (a, b2, c) = (guu, guv, gvv) of the tangent plane
180+
double a;
181+
double b2;
182+
double c;
183+
};
184+
185+
class TlpTlmAccuracyTest : public ::testing::TestWithParam<TlCase> {};
178186

179187
TEST_P(TlpTlmAccuracyTest, MatchesQuadrature) {
180188
// GL-64 reference noise at l near kL is ~1e-13; 1e-11 leaves a safe margin
181-
// while staying far below the forward-recurrence error that Miller corrects.
189+
// while staying far below the error of a recurrence run in the wrong
190+
// direction or started too close above kL.
182191
static constexpr double kTolerance = 1.0e-11;
183192

184-
const auto [mpol, ntor] = GetParam();
193+
const auto [mpol, ntor, a_val, b2_val, c_val] = GetParam();
185194

186195
const bool lasym = false;
187196
const int nfp = 5;
@@ -200,21 +209,14 @@ TEST_P(TlpTlmAccuracyTest, MatchesQuadrature) {
200209
const int kL = mf + nf;
201210
SingularIntegrals si(&s, &fb, &tp, &sg, nf, mf);
202211

203-
// Geometry coefficients chosen so am/ap ~ 4.7 and the discriminant
204-
// d^2 - ap*am = 0.16 - 2.31 < 0 (so the integrand is smooth on [-1, 1]).
205-
// ap = a + b2 + c = 0.7
206-
// am = a - b2 + c = 3.3
207-
// d = c - a = 0.4
208-
// At these coefficients the forward recurrence loses ~kL * log10(am/ap)
209-
// ~ 0.67 * kL significant digits; for kL >= 15 it loses > 10 digits and the
210-
// Miller activation threshold (growth > 1e10) triggers.
211-
const double a_val = 0.8;
212-
const double b2_val = -1.3;
213-
const double c_val = 1.2;
214212
const double ap = a_val + b2_val + c_val;
215213
const double am = a_val - b2_val + c_val;
216214
const double d = c_val - a_val;
217-
ASSERT_GT(am, ap) << "test setup: need am > ap to exercise Miller path";
215+
// The homogeneous solutions of the T^+ (T^-) recurrence grow by
216+
// sqrt(am/ap) (sqrt(ap/am)) per forward step, so one of the two has to run
217+
// backward once kL is large enough.
218+
ASSERT_GT(std::max(am / ap, ap / am), 2.0)
219+
<< "test setup: need one unstable forward direction";
218220
ASSERT_LT(d * d, ap * am) << "test setup: need smooth integrand on [-1,1]";
219221

220222
const int numLocal = tp.ztMax - tp.ztMin;
@@ -253,15 +255,22 @@ TEST_P(TlpTlmAccuracyTest, MatchesQuadrature) {
253255
}
254256
}
255257

256-
// (mpol, ntor) pairs spanning the forward-stable regime (kL=15, Miller just
257-
// barely fires) up to high-kL where forward would lose >30 digits.
258+
// The first three cases share the coefficients ap = 0.7, am = 3.3, d = 0.4
259+
// (am/ap = 4.7) at kL = 15, 27 and 45; the last one takes the metric of the
260+
// cth_like_free_bdy boundary at the point of its largest cross term
261+
// (ap/am = 2.2) at kL = 33, where a backward pass seeded 50 steps above kL
262+
// leaves 2e-9 of the seed in T^-_33.
258263
INSTANTIATE_TEST_SUITE_P(
259264
ResolutionSweep, TlpTlmAccuracyTest,
260-
::testing::Values(std::pair<int, int>{6, 8}, std::pair<int, int>{12, 14},
261-
std::pair<int, int>{20, 24}),
262-
[](const ::testing::TestParamInfo<std::pair<int, int>>& info) {
263-
return "mpol" + std::to_string(info.param.first) + "_ntor" +
264-
std::to_string(info.param.second);
265+
::testing::Values(TlCase{6, 8, 0.8, -1.3, 1.2},
266+
TlCase{12, 14, 0.8, -1.3, 1.2},
267+
TlCase{20, 24, 0.8, -1.3, 1.2},
268+
TlCase{16, 16, 1.360e-02, 2.241e-02, 4.626e-02}),
269+
[](const ::testing::TestParamInfo<TlCase>& info) {
270+
return "mpol" + std::to_string(info.param.mpol) + "_ntor" +
271+
std::to_string(info.param.ntor) + "_ap" +
272+
std::to_string(static_cast<int>(
273+
1000.0 * (info.param.a + info.param.b2 + info.param.c)));
265274
});
266275

267276
} // namespace vmecpp

0 commit comments

Comments
 (0)