Skip to content

Commit f9df5ab

Browse files
committed
BUG: Regenerate trig LUT/constants from updated Sollya sources
Match SVML rounding DAG in the reduction LUT and use exact-product constants for the non-FMA f64 path.
1 parent e9da4dc commit f9df5ab

2 files changed

Lines changed: 91 additions & 65 deletions

File tree

npsr/trig/data/approx.h.sol

Lines changed: 81 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,104 @@
1-
// Suppress rounding mode information messages and NaN warnings for boundary angles
1+
// Suppress rounding mode information messages for irrational table values
22
suppressmessage(184, 185, 186); // suppress info no rounding, round-up, round-down
3-
suppressmessage(419); // expected nan when angle is at specific multiples causing derivative singularities
3+
suppressmessage(160); // inequality decided by faithful evaluation
44

55
// Generates a 4-element lookup table for fast trigonometric function approximation.
6-
//
7-
// This procedure creates optimized lookup tables that store:
8-
// - Function values split into high/low precision components
9-
// - Derivative information with power-of-2 scaling for efficient interpolation
106
//
117
// The table format per angle is: [deriv, sigma, high, low]
128
// where:
13-
// deriv = actual_derivative - sigma (reduces storage requirements)
14-
// sigma = 2^k, where k = ceil(log2(|actual_derivative|))
15-
// high = main part of function value
16-
// low = residual for extended precision
9+
// sigma + deriv = f'(angle) sigma = sign * 2^k nearest the derivative;
10+
// being a power of two, sigma*z is exact for any
11+
// small remainder z, confining the interpolation
12+
// rounding error to the tiny deriv*z term
13+
// high + low = f(angle) double-float split of the function value
14+
//
15+
// The values reproduce Intel SVML's AVX-512 high-accuracy sin breakpoint
16+
// tables byte-for-byte (SVML packs the fields in a different, per-precision
17+
// order; this table keeps one order for both precisions). Two conventions
18+
// are per-precision, pinned down by byte comparison against SVML:
19+
// - low rounding: float RN full precision / double RZ 24-bit
20+
// - sigma cut: bump 2^k -> 2^(k+1) when |derivative| > cut * 2^k,
21+
// cut = sqrt(2) for float / 1.5 for double
22+
//
23+
// Two structural tricks keep every rounding and comparison decidable
24+
// (Sollya brackets irrational values but can never prove one is exactly
25+
// zero, nor decide a comparison whose sides are exactly equal):
26+
// - Quadrant reduction: evaluate sin/cos only in [0, pi/2) and select by
27+
// quadrant. Where f or f' is exactly 0/+-1 (angle a multiple of pi/2)
28+
// the reduced angle is the exact rational 0, so sin(0)/cos(0) evaluate
29+
// symbolically and those entries store clean +0.0.
30+
// - Cut nudge: scaling the float cut by (1 - 2^-200) resolves the exact
31+
// tie |derivative| = 2^(-1/2) upward like SVML does; no other entry on
32+
// either grid comes anywhere near a cut point.
1733
//
1834
// Parameters:
19-
// pT - Type descriptor with .kSize, .kDigits, .kRound
20-
// pFunc - Function to approximate (sin or cos)
21-
// pFuncDriv - Derivative of pFunc (cos or -sin)
22-
procedure ApproxLut4_(pT, pFunc, pFuncDriv) {
35+
// pT - Type descriptor with .kSize, .kDigits, .kRound
36+
// pQuad - Quadrant offset of the function to approximate:
37+
// 0 for sin (derivative cos), 1 for cos (derivative -sin),
38+
// using cos(x) = sin(x + pi/2)
39+
procedure ApproxLut4_(pT, pQuad) {
2340
var r, i, $;
2441
// Table size: 512 entries for 64-bit, 256 for 32-bit
25-
// More entries for double precision to maintain accuracy
2642
// These sizes balance table memory usage with interpolation accuracy:
2743
// - 256 entries = 1.4° spacing for float (sufficient for 24-bit mantissa)
2844
// - 512 entries = 0.7° spacing for double (needed for 53-bit mantissa)
2945
$.num_lut = match pT.kSize
3046
with 64: (2^9)
3147
default: (2^8);
32-
33-
// Low part rounding configuration:
34-
// - 64-bit: Round to 24 bits with round-to-zero (faster, sufficient for residual)
35-
// - 32-bit: Use full precision with round-to-nearest
36-
$.low_round = match pT.kSize
48+
49+
// Low part rounding: full precision RN for float, truncated 24-bit for double
50+
$.low_round = match pT.kSize
3751
with 64: ([|24, RZ|])
3852
default: ([|pT.kDigits, RN|]);
39-
40-
// Scale factor to convert table index to angle in radians
41-
$.scale = 2.0 * pi / $.num_lut;
42-
53+
54+
// Sigma cut: geometric nearest power of two for float, linear for double,
55+
// nudged below the float tie point (see header)
56+
$.cut = match pT.kSize
57+
with 64: (3 / 2)
58+
default: (sqrt(2));
59+
$.cut = $.cut * (1 - 2^(-200));
60+
4361
r = [||];
4462
for i from 0 to $.num_lut - 1 do {
45-
// Sample angle uniformly distributed from 0 to 2π
46-
$.angle = i * $.scale;
47-
48-
// Compute exact function value
49-
$.exact = pFunc($.angle);
50-
51-
// Split into high and low parts for extended precision
52-
// High part gets the main value rounded to type precision
63+
// Quadrant reduction: angle = quad*pi/2 + reduced with reduced in [0, pi/2)
64+
$.quadrant = floor(4 * i / $.num_lut);
65+
$.reduced = 2 * pi * (i - $.quadrant * $.num_lut / 4) / $.num_lut;
66+
$.sin_red = sin($.reduced);
67+
$.cos_red = cos($.reduced);
68+
$.sin_by_quad = [| $.sin_red, $.cos_red, -$.sin_red, -$.cos_red |];
69+
$.cos_by_quad = [| $.cos_red, -$.sin_red, -$.cos_red, $.sin_red |];
70+
71+
// Shift by the function's quadrant offset:
72+
// f(angle) = sin(angle + pQuad*pi/2), f'(angle) = cos(angle + pQuad*pi/2)
73+
$.quadrant = $.quadrant + pQuad;
74+
if ($.quadrant > 3) then $.quadrant = $.quadrant - 4;
75+
76+
// Exact function value, split into high and low parts:
77+
// value ≈ high + low with high rounded to type precision
78+
$.exact = $.sin_by_quad[$.quadrant];
5379
$.high = pT.kRound($.exact);
54-
55-
// Low part stores the residual, rounded to reduced precision
56-
// This allows accurate reconstruction: value ≈ high + low
5780
$.low = pT.kRound(round($.exact - $.high, $.low_round[0], $.low_round[1]));
58-
59-
// Compute derivative for interpolation
60-
$.deriv_exact = pFuncDriv($.angle);
61-
62-
// Find power-of-2 scale factor closest to derivative magnitude
63-
// This allows efficient storage and reconstruction
64-
$.k = ceil(log2(abs($.deriv_exact)));
65-
if ($.deriv_exact < 0) then $.k = -$.k;
66-
67-
// Sigma is the power-of-2 scale factor
68-
$.sigma = 2.0^$.k;
69-
70-
// Store derivative minus sigma (typically a small value)
71-
// Actual derivative = sigma + stored_deriv
72-
$.deriv = pT.kRound($.deriv_exact - $.sigma);
73-
81+
82+
// Exact derivative for interpolation
83+
$.deriv_exact = $.cos_by_quad[$.quadrant];
84+
85+
if ($.deriv_exact == 0) then {
86+
// Derivative term vanishes (angle at pi/2 or 3pi/2 relative to f)
87+
$.sigma = 0;
88+
$.deriv = 0;
89+
} else {
90+
// Power-of-2 scale factor nearest the derivative under the cut rule;
91+
// actual derivative = sigma + stored deriv
92+
$.k = floor(log2(abs($.deriv_exact)));
93+
$.sigma = 2.0^$.k;
94+
if (abs($.deriv_exact) > $.cut * $.sigma) then $.sigma = 2 * $.sigma;
95+
if ($.deriv_exact < 0) then $.sigma = -$.sigma;
96+
$.deriv = pT.kRound($.deriv_exact - $.sigma);
97+
};
98+
7499
r = r @ [|$.deriv, $.sigma, $.high, $.low|];
75100
};
76-
101+
77102
// Format as C array with 4 elements per table entry
78103
return CArrayT(pT, r, 4) @ ";";
79104
};
@@ -84,17 +109,17 @@ procedure ApproxLut4_(pT, pFunc, pFuncDriv) {
84109
Append(
85110
"template <typename T> inline constexpr char kSinApproxTable[] = {};",
86111
"template <> inline constexpr float kSinApproxTable<float>[] = ",
87-
ApproxLut4_(Float32, sin(x), cos(x)), // sin table with cos derivative
112+
ApproxLut4_(Float32, 0), // sin table with cos derivative
88113
"",
89114
"template <> inline constexpr double kSinApproxTable<double>[] = ",
90-
ApproxLut4_(Float64, sin(x), cos(x)),
115+
ApproxLut4_(Float64, 0),
91116
"",
92117
"template <typename T> inline constexpr char kCosApproxTable[] = {};",
93118
"template <> inline constexpr float kCosApproxTable<float>[] = ",
94-
ApproxLut4_(Float32, cos(x), -sin(x)), // cos table with -sin derivative
119+
ApproxLut4_(Float32, 1), // cos table with -sin derivative
95120
"",
96121
"template <> inline constexpr double kCosApproxTable<double>[] = ",
97-
ApproxLut4_(Float64, cos(x), -sin(x)),
122+
ApproxLut4_(Float64, 1),
98123
""
99124
);
100125

npsr/trig/data/constants.h.sol

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ Append(
3939

4040
"template <> inline constexpr double kPi<double, false>[] = " @
4141
KArray_(Float64, pi, [|RN, 24, 24, 24|], [|RN, 53|]), // no FMA: 3x24-bit + 1x53-bit
42-
// The 24-bit pieces ensure n*πᵢ is exact for |n| < 2^29
42+
// The 24-bit pieces ensure n*πᵢ is exact (no rounding) for the low path's
43+
// largest quotient; the 53-bit tail sits at 2^-76 so its inexact product
44+
// rounds ~2^-105, harmless even for tiny reduced arguments near k*π/2
4345
"",
4446

4547
// Special 35-bit precision π for specific algorithms
@@ -61,18 +63,17 @@ Append(
6163
);
6264

6365
// Non-FMA version of π/16 for High precision implementation
64-
// Special handling: components are reordered [0,2,3,1] for proper evaluation
65-
// Without FMA, multiplication order matters to minimize rounding errors
66-
vNFma = Constants(pi/16, [|RN, 27, 27|], [|RN, 29|], [|RN, 53|]);
66+
// Pieces are applied in this natural (descending) order; each subtraction's
67+
// rounding error is captured into r_lo by the reduction DAG. The 27/27/29-bit
68+
// heads keep n*πᵢ/16 exact for |n| <= 85445660 (= round(2^24 * 16/π), the
69+
// largest quotient on the high path); the 53-bit tail sits at 2^-91 so its
70+
// inexact product rounds ~2^-117, harmless even for tiny reduced arguments
6771
Append(
6872
"template <bool FMA> inline constexpr double kPiDiv16Prec29[] = " @
6973
KArray_(Float64, pi/16, [|RN, 53|], [|RN, 29|], [|RN, 53|]),
70-
71-
// Non-FMA version reorders components: [0], [2], [3], [1]
72-
// This ordering ensures proper evaluation without FMA:
73-
// r = x - n*π₁/16 - n*π₃/16 - n*π₄/16 - n*π₂/16
74+
7475
"template <> inline constexpr double kPiDiv16Prec29<false>[] = " @
75-
CArray([|vNFma[0], vNFma[2], vNFma[3], vNFma[1]|], 4) @ ";",
76+
KArray_(Float64, pi/16, [|RN, 27, 27|], [|RN, 29|], [|RN, 53|]),
7677
"",
7778

7879
// Simple scalar constants

0 commit comments

Comments
 (0)