(x: bigint, bits: number)
| 274 | * @returns exp(x/2^bits) * 2^bits as a bigint |
| 275 | */ |
| 276 | export function fpexp(x: bigint, bits: number): bigint { |
| 277 | // exp(0) = 1 |
| 278 | if (x === 0n) return 1n << BigInt(bits); |
| 279 | |
| 280 | // Elevated internal scale. `targetHalvings` extra halvings shorten the Taylor |
| 281 | // series to O(√bits) terms; `guard` guard bits absorb the ~2^targetHalvings |
| 282 | // error amplification of the squaring-back phase (and the final downshift's |
| 283 | // rounding). |
| 284 | const targetHalvings = Math.round(EXP_REDUCE_COEF * Math.sqrt(bits)); |
| 285 | const guard = targetHalvings + 8; |
| 286 | const bits2 = bits + guard; |
| 287 | const B = BigInt(bits2); |
| 288 | const scale = 1n << B; |
| 289 | |
| 290 | // Lift the input to the elevated scale. |
| 291 | const g = BigInt(guard); |
| 292 | let r = x << g; |
| 293 | |
| 294 | // Argument reduction: halve until |r/scale| < 2^−targetHalvings. The shift |
| 295 | // count is derived from the ACTUAL magnitude of r, so a small argument (e.g. |
| 296 | // fpln's Newton seed near ln 1 = 0) is never over-reduced to 0 — the shift |
| 297 | // always leaves ~bits2 − targetHalvings significant bits. Sign is carried |
| 298 | // separately (a right-shift of a negative bigint would round toward −∞). |
| 299 | const neg = r < 0n; |
| 300 | let ra = neg ? -r : r; |
| 301 | const kExtra = Math.max(0, bitLength(ra) - bits2 + targetHalvings); |
| 302 | ra >>= BigInt(kExtra); |
| 303 | r = neg ? -ra : ra; |
| 304 | |
| 305 | // Taylor series: exp(r/scale) = 1 + r/scale + r²/(2!·scale²) + ... |
| 306 | // In fixed-point: sum = scale + r + r²/(2·scale) + r³/(6·scale²) + ... |
| 307 | // Incremental: term_n = term_{n-1} * r / (n * scale) |
| 308 | // base-2: ((term * r) >> bits2) / n — shift + small-divisor division |
| 309 | let sum = scale; // 1.0 |
| 310 | let term = r; // r/scale in fixed-point |
| 311 | sum += term; |
| 312 | |
| 313 | for (let n = 2; ; n++) { |
| 314 | term = ((term * r) >> B) / BigInt(n); |
| 315 | if (term === 0n) break; |
| 316 | sum += term; |
| 317 | } |
| 318 | |
| 319 | // Squaring phase: exp(x/scale) = exp((x/scale)/2^kExtra)^(2^kExtra). |
| 320 | for (let i = 0; i < kExtra; i++) { |
| 321 | sum = (sum * sum) >> B; |
| 322 | } |
| 323 | |
| 324 | // Downshift from the elevated scale back to `bits`, rounding to nearest. |
| 325 | return (sum + (1n << (g - 1n))) >> g; |
| 326 | } |
| 327 | |
| 328 | // AGM-vs-direct-log crossover, in bits. Below LN_AGM_MIN_BITS the direct-log |
| 329 | // kernel (`fplnDirect`: one √-reduced fpexp + a short log1p series) wins; above |
no test coverage detected