By: ValentinHirschi <valentin.hirschi@gmail.com>
Take the contour box's r1 for far roots from log(-z) + log(1 - x/z)
The contour box's tfun integrates along one of two contours, chosen by the
distances of its poles to the triangle; for complex-conjugate roots beta the
two distances agree analytically and rounding picks one. Both contours give
the same value at 256 bits, but in binary64 one of them could be wrong: in
the box p² = (6.686564391522424, 4, 0, 0.6479305255028737), s12 = 0, s23 = 4
(1 - 1e-12), m² = (0.6479..., 0.1067..., 0.5221..., 0.6479...) the D0 was
off by 5.7e-4 on every route, and by up to 6.7e-7 for other s23 = 4 (1 ± r).
There the line coefficient g + h = p3 - p5 - p0 of the zero-external chart
is tiny, and an edge of one contour has the far real root -j/(g + h).
`r1` loses such roots, independently of the chart: for a far real root the
quotients (y - z)/(-z) and (y - z)/(1 - z) of its dilogarithm terms lie
within y/z of 1 on sheets with windings, each term carries i pi n log(1 - q)
with a divided difference of O(1/|y|), and the two cancel while 1 - q formed
from the rounded quotient costs eps |z|. On random poles r1 was off by 1e-12
at |z| = 1e3, 1e-6 at 1e9 and O(1) at 1e15, on the hand-written kernel and
the binary64 expression alike (complex far roots were fine). In the
`ordered` chart a small external invariant p puts such a root in the edge
quadratic p x² + ...: of 240 random boxes with four nonzero externals and
one invariant between 1e-12 and 1e-4, 13 lost 1e-10 to 2.6e-7 (all contour
boxes).
On (0, 1), log(x - z) = log(-z) + log(1 - x/z) on the continued sheets, so
r1 = fy log(-z) - sum_n M_n/(n zⁿ) with the moments of 1/((x - y1)(x - y2)):
M_0 = fy, M_1 = y1 fy + log((1 - y2)/(-y2)), and M_n = 1/(n - 1) + (y1 + y2)
M_{n-1} - y1 y2 M_{n-2}. `r1` takes this eight-term form (`far_root_series`)
when 16 (max(1, |y1|², |y2|²)/|z|²)⁴ leaves 1 unchanged at the working
precision (`far_root`; in binary64 for |z| beyond about 140 max(1, |y|), at
128 bits and more only for huge roots), like `near_difference` chooses its
expansion. The moments are expanded as polynomials in y1 + y2, y1 y2, M_0
and M_1: built recursively they repeated each other and made every evaluator
containing r1 eight seconds slower to build. The hand-written kernel skips
the test below |z|² = 2^14, where the bound cannot vanish.
r1 is now within 2e-16 for |z| from 1e2 to 1e17 on both routes (5e-15 at |z|
= 10 with the general form). The s23 = 4 (1 ± r) boxes, r = 1e-15 to 1e-3,
keep 1.3e-13 (SymJIT 1.1e-12) on every route whichever contour the tie
picks, and the 13 small-invariant boxes 1e-14; so the tie needs no rule.
Tests: `far_root_r1_matches_high_precision` (r1 against 256 bits) and
`near_vanishing_line_coefficient_in_binary64`,
`small_invariant_far_roots_in_binary64` in tests/d0_contour_thresholds.rs.
Tests: full suite with generated evaluators passes (224 passed, 7 ignored,
on top of db151cd).. Native code regenerated, D0 SymJIT asset rebuilt.
Oracle sweeps (D0, four seeds, native, generated and expression routes,
against 9c5490b): no row fixed or regressed on any route. Retired
instructions per call on the recorded NinjaRS streams: D0 -0.71% against
4d438cf (geomean; at most +0.36%, the four-photon box, of which +0.24% from
the far-root test; -4.6% on syn-n4-r4, where far roots skip the
dilogarithms), C0 unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>