Skip to content

Commit 677b8ab

Browse files
committed
FEAT: Python/Sollya driver that used for generates LUT/Constants
Argument reduction & tables: - Payne–Hanek tables via chunked 4/π per exponent range (`reduction.h.sol`) - π splits for FMA/non-FMA and float/double (`constants.h.sol`) - Compact 4-term (value + derivatives) LUTs (`approx.h.sol`) - Precomputed sin/cos(k·π/16) for quadrant handling (`kpi16-inl.h.sol`) Sollya-driven codegen pipeline: - Tooling lives in `tools/sollya` with shared helpers - CLI: `spin sollya [-f]` (via `.spin/cmds.py`) to (re)generate headers - Project wiring: `pyproject.toml` matters for spin
1 parent 1acd145 commit 677b8ab

11 files changed

Lines changed: 1170 additions & 0 deletions

File tree

.gitignore

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Temporary #
2+
#############
3+
.cache
4+
.direnv
5+
6+
# Compiled source #
7+
###################
8+
*.a
9+
*.dll
10+
*.exe
11+
*.o
12+
*.o.d
13+
*.py[ocd]
14+
*.so
15+
*.mod
16+
17+
# Logs #
18+
########
19+
*.log
20+
21+
# Patches #
22+
###########
23+
*.patch
24+
*.diff

.spin/cmds.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import pathlib
2+
import sys
3+
import click
4+
5+
curdir = pathlib.Path(__file__).parent
6+
rootdir = curdir.parent
7+
toolsdir = rootdir / "tools"
8+
sys.path.insert(0, str(toolsdir))
9+
10+
11+
@click.command(help="Generate sollya c++/python based files")
12+
@click.option("-f", "--force", is_flag=True, help="Force regenerate all files")
13+
def sollya(*, force):
14+
import sollya # type: ignore[import]
15+
16+
sollya.main(force=force)

npsr/trig/data/approx.h.sol

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Suppress rounding mode information messages and NaN warnings for boundary angles
2+
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
4+
5+
// 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
10+
//
11+
// The table format per angle is: [deriv, sigma, high, low]
12+
// 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
17+
//
18+
// 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) {
23+
var r, i, $;
24+
// Table size: 512 entries for 64-bit, 256 for 32-bit
25+
// More entries for double precision to maintain accuracy
26+
// These sizes balance table memory usage with interpolation accuracy:
27+
// - 256 entries = 1.4° spacing for float (sufficient for 24-bit mantissa)
28+
// - 512 entries = 0.7° spacing for double (needed for 53-bit mantissa)
29+
$.num_lut = match pT.kSize
30+
with 64: (2^9)
31+
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
37+
with 64: ([|24, RZ|])
38+
default: ([|pT.kDigits, RN|]);
39+
40+
// Scale factor to convert table index to angle in radians
41+
$.scale = 2.0 * pi / $.num_lut;
42+
43+
r = [||];
44+
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
53+
$.high = pT.kRound($.exact);
54+
55+
// Low part stores the residual, rounded to reduced precision
56+
// This allows accurate reconstruction: value ≈ high + low
57+
$.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+
74+
r = r @ [|$.deriv, $.sigma, $.high, $.low|];
75+
};
76+
77+
// Format as C array with 4 elements per table entry
78+
return CArrayT(pT, r, 4) @ ";";
79+
};
80+
81+
// Generate C++ header content with specialized lookup tables
82+
// for both float and double precision sine and cosine
83+
// Template declarations (empty for unsupported types)
84+
Append(
85+
"template <typename T> inline constexpr char kSinApproxTable[] = {};",
86+
"template <> inline constexpr float kSinApproxTable<float>[] = ",
87+
ApproxLut4_(Float32, sin(x), cos(x)), // sin table with cos derivative
88+
"",
89+
"template <> inline constexpr double kSinApproxTable<double>[] = ",
90+
ApproxLut4_(Float64, sin(x), cos(x)),
91+
"",
92+
"template <typename T> inline constexpr char kCosApproxTable[] = {};",
93+
"template <> inline constexpr float kCosApproxTable<float>[] = ",
94+
ApproxLut4_(Float32, cos(x), -sin(x)), // cos table with -sin derivative
95+
"",
96+
"template <> inline constexpr double kCosApproxTable<double>[] = ",
97+
ApproxLut4_(Float64, cos(x), -sin(x)),
98+
""
99+
);
100+
101+
WriteCPPHeader("npsr::trig::data");

npsr/trig/data/constants.h.sol

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Suppress rounding mode information messages that are expected during constant generation
2+
suppressmessage(185, 186); // suppress expected info round-up, round-down
3+
4+
// Helper procedure to format constants array for C++ output
5+
// Takes a type descriptor followed by constant values and formats them
6+
// as a C array with 4 elements per line
7+
procedure KArray_(pArgs = ...) {
8+
var pT;
9+
pT = head(pArgs);
10+
return CArrayT(pT, Constants @ tail(pArgs), 4) @ ";";
11+
};
12+
13+
// Generate C++ header with various π-related constants for Cody-Waite reduction
14+
// These constants enable accurate computation of r = x - n*π with extended precision
15+
//
16+
// The Cody-Waite method splits π into multiple parts:
17+
// r = x - n*π₁ - n*π₂ - n*π₃ ...
18+
// where each πᵢ has limited precision to ensure exact multiplication
19+
//
20+
// Different versions are provided for:
21+
// - FMA (Fused Multiply-Add) vs non-FMA architectures
22+
// - float vs double precision
23+
// - Various precision requirements
24+
Append(
25+
// Generic template declaration
26+
"template <typename T, bool FMA> inline constexpr char kPi[] = {};",
27+
28+
// Float π constants for Low precision implementation
29+
"template <> inline constexpr float kPi<float, true>[] = " @
30+
KArray_(Float32, pi, [|RN, 24, 24, 24|]), // FMA: 3x24-bit pieces (full precision each)
31+
32+
"template <> inline constexpr float kPi<float, false>[] = " @
33+
KArray_(Float32, pi, [|RD, 11, 11, 11|], [|RN, 24|]), // no FMA: 3x11-bit + 1x24-bit
34+
// The 11-bit pieces ensure n*πᵢ is exact (no rounding) for |n| < 2^13
35+
36+
// Double π constants for Low precision implementation
37+
"template <> inline constexpr double kPi<double, true>[] = " @
38+
KArray_(Float64, pi, [|RN, 53|], [|RD, 53|], [|RU, 53|]), // FMA: Different roundings for error compensation
39+
40+
"template <> inline constexpr double kPi<double, false>[] = " @
41+
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
43+
"",
44+
45+
// Special 35-bit precision π for specific algorithms
46+
"template <bool FMA> inline constexpr double kPiPrec35[] = " @
47+
KArray_(Float64, pi, [|RN, 35|], [|RD, 53|]),
48+
49+
"template <> inline constexpr double kPiPrec35<false>[] = " @
50+
KArray_(Float64, pi, [|RN, 24, 24, 24|]),
51+
"",
52+
53+
// 2π constants for angle wrapping
54+
"template <typename T> inline constexpr char kPiMul2[] = {};",
55+
"template <> inline constexpr float kPiMul2<float>[] = " @
56+
KArray_(Float32, pi*2, [|RN, 24, 24|]), // 2x24-bit pieces
57+
58+
"template <> inline constexpr double kPiMul2<double>[] = " @
59+
KArray_(Float64, pi*2, [|RN, 53, 53|]), // 2x53-bit pieces
60+
""
61+
);
62+
63+
// 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|]);
67+
Append(
68+
"template <bool FMA> inline constexpr double kPiDiv16Prec29[] = " @
69+
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+
"template <> inline constexpr double kPiDiv16Prec29<false>[] = " @
75+
CArray([|vNFma[0], vNFma[2], vNFma[3], vNFma[1]|], 4) @ ";",
76+
"",
77+
78+
// Simple scalar constants
79+
"template <typename T> inline constexpr char kInvPi = '_';",
80+
"template <> inline constexpr float kInvPi<float> = " @ single(1/pi) @ "f;",
81+
"template <> inline constexpr double kInvPi<double> = " @ double(1/pi) @ ";",
82+
"",
83+
84+
"template <typename T> inline constexpr char kHalfPi = '_';",
85+
"template <> inline constexpr float kHalfPi<float> = " @ single(pi/2) @ "f;",
86+
"template <> inline constexpr double kHalfPi<double> = " @ double(pi/2) @ ";",
87+
"",
88+
89+
"template <typename T> inline constexpr char k16DivPi = '_';",
90+
"template <> inline constexpr float k16DivPi<float> = " @ single(16/pi) @ "f;",
91+
"template <> inline constexpr double k16DivPi<double> = " @ double(16/pi) @ ";",
92+
""
93+
);
94+
// Dump();
95+
96+
WriteCPPHeader("npsr::trig::data");
97+

npsr/trig/data/data.h.sol

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
var header;
3+
Append("#include \"npsr/lut-inl.h\"");
4+
for header in [|"constants", "kpi16-inl", "approx", "reduction"|] do {
5+
Append(
6+
"#include \"npsr/trig/data/" @ header @ ".h\""
7+
);
8+
};
9+
};
10+
11+
Write();
12+
13+

npsr/trig/data/kpi16-inl.h.sol

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Generates lookup table for sin(k·π/16) and cos(k·π/16) values
2+
// Used in the high-precision trigonometric implementation for range reduction
3+
//
4+
// This table supports the algorithm where input x is reduced to:
5+
// x = n*(π/16) + r, where |r| < π/16
6+
// Then sin(x) and cos(x) are reconstructed using angle addition formulas
7+
//
8+
// Parameters:
9+
// pT - Type descriptor (Float64 in this case)
10+
// pFunc - Function to evaluate (sin or cos)
11+
// pBy - Divisor for π (16 in this case, giving π/16 intervals)
12+
procedure PiDivTable_(pT, pFunc, pBy) {
13+
var r, i, pi_by;
14+
pi_by = pi / pBy;
15+
r = [||];
16+
17+
// Generate function values at k*π/16 for k = 0, 1, ..., 15
18+
for i from 0 to pBy - 1 do {
19+
r = r :. pT.kRound(pFunc(i * pi_by));
20+
};
21+
22+
// Format as C array with 4 elements per line
23+
return CArrayT(pT, r, 4);
24+
};
25+
26+
// Generates packed low-precision parts of sin and cos values
27+
// This packing scheme saves memory by storing two 32-bit values in one 64-bit word
28+
//
29+
// The packing works as follows for double (64-bit):
30+
// - sin_low occupies bits [31:0] (lower 32 bits)
31+
// - cos_low occupies bits [63:32] (upper 32 bits)
32+
//
33+
// This is why in the C++ code:
34+
// - cos_lo can be used directly (it's already in the upper bits)
35+
// - sin_lo needs to be extracted with a 32-bit left shift
36+
procedure PiDivPackLowTable_(pT, pFunc0, pFunc1, pBy) {
37+
var r, i, digits, $;
38+
$.pi_by = pi / pBy;
39+
r = [||];
40+
41+
// First, compute the low precision parts (residuals after high precision)
42+
for i from 0 to pBy - 1 do {
43+
$.hi0 = pT.kRound(pFunc0(i * $.pi_by)); // High precision sin
44+
$.hi1 = pT.kRound(pFunc1(i * $.pi_by)); // High precision cos
45+
// Low precision parts: exact value minus high precision part
46+
$.hi0_low = pT.kRound(pFunc0(i * $.pi_by) - $.hi0); // sin_low
47+
$.hi1_low = pT.kRound(pFunc1(i * $.pi_by) - $.hi1); // cos_low
48+
r = r @ [|$.hi0_low, $.hi1_low|];
49+
};
50+
51+
// Convert to binary representation for bit manipulation
52+
digits = ToDigits(pT, r);
53+
$.half_size = pT.kSize / 2; // 32 for double
54+
$.lower_bits = 2^$.half_size; // Mask for lower 32 bits
55+
56+
r = [||];
57+
// Pack pairs of values into single 64-bit words
58+
for i from 0 to length(digits) - 1 by 2 do {
59+
$.hi0 = digits[i]; // sin_low bits
60+
$.hi1 = digits[i + 1]; // cos_low bits
61+
$.pack = mod(RightShift($.hi0, $.half_size), $.lower_bits);
62+
$.pack = $.pack + $.hi1 - mod($.hi1, $.lower_bits);
63+
r = r :. $.pack;
64+
};
65+
66+
// Convert back from binary representation
67+
r = FromDigits(pT, r);
68+
return CArrayT(pT, r, 4);
69+
};
70+
71+
Append(
72+
"inline constexpr auto kKPi16Table = MakeLut<double>(",
73+
"// High parts of sin(k·π/16) where k = 0, 1, ..., 15",
74+
PiDivTable_(Float64, sin(x), 16) @ ",",
75+
"// High parts of cos(k·π/16) where k = 0, 1, ..., 15",
76+
PiDivTable_(Float64, cos(x), 16) @ ",",
77+
"// Lower parts of sin(k·π/16) and cos(k·π/16) packed together",
78+
"// Format: bits [63:32] = cos_low, bits [31:0] = sin_low",
79+
"// This packing saves 16×8 = 128 bytes of memory",
80+
PiDivPackLowTable_(Float64, sin(x), cos(x), 16),
81+
"",
82+
");"
83+
);
84+
85+
WriteHighwayHeader("npsr::HWY_NAMESPACE::trig");
86+

0 commit comments

Comments
 (0)