// Copyright (C) 2026 Kiyotsugu Arai // SPDX-License-Identifier: LGPL-3.0-or-later // FloatMath.cpp // Implementation of the multiple-precision floating-point math function library // Arbitrary-precision implementation via Taylor series + argument halving/squaring reconstruction #include #include #include #include #include #include #include #include #include #include #include #include #include namespace sangi { //============================================================================= // Utility functions //============================================================================= // Determine the computation precision (decimal digits) based on the input's effective_bits_. // If the input precision is sufficiently lower than the requested precision, compute with input precision + guard for speed. static int effectiveComputePrecision(int input_eff, int requested_precision) { // Transcendental functions must return a result accurate to the // requested precision even when the input is an exact value (e.g. 1.5 = 2 bits). // If the input's effective bits are less than half the requested precision, disable precision reduction. constexpr int GUARD = 32; int req_bits = Float::precisionToBits(requested_precision); if (input_eff >= INT_MAX || input_eff + GUARD >= req_bits) { return requested_precision; } // Even if the input's effective bits are few, do not drop below the requested precision // (a transcendental function's output precision is not limited by the input precision) return requested_precision; } // Analytically determine the working precision required to compute the Taylor series of sin/cos // (assuming |x| ≤ π/2 after argument reduction) to target_precision decimal digits. // The internal term count N truncates at |R_N(x)| ≤ |x|^N / N! ≤ 10^-target, so use a // conservative loop bound of 1.2·target + 10. The roundoff accumulation inside the loop is N ulps, // so add an extra guard of log10(N) digits plus a small safety margin. // The estimate may be rough (a ceiling-determined quantity), and double arithmetic is sufficient. static int taylorWorkingPrecision(int target_precision) { if (target_precision < 1) target_precision = 1; // Safe upper bound on the term count (actually terminates early at term.nw==0, but here an upper bound) double est_N = static_cast(target_precision) * 1.2 + 10.0; // Obtain log10(N) via ceil (small, so double suffices; safe up to N < ~10^10) int log10_N = static_cast(std::ceil(std::log10(est_N))); if (log10_N < 1) log10_N = 1; constexpr int SAFETY = 4; // Safety margin for small constants (~13 bits) int guard = log10_N + SAFETY; // For compatibility, keep the legacy fixed +10 as a lower bound if (guard < 10) guard = 10; return target_precision + guard; } // Set the result's precision field and reflect the input's effective bits. // If input_eff < requested, set effective_bits_ to input_eff. static void finalizeResult(Float& result, int input_eff, int precision) { result.setResultPrecision(precision); int req_bits = Float::precisionToBits(precision); if (input_eff < INT_MAX && input_eff < req_bits) { result.setEffectiveBits(input_eff); } } //============================================================================= // Ziv's iteration strategy — correct rounding of transcendental functions //============================================================================= // Determine whether the rounding direction is fixed when the mantissa of the result y is rounded at target_bits. // Returns true if the guard_bits guard bits are sufficiently far from the rounding boundary. static bool canRoundCorrectly(const Float& y, int target_bits, int guard_bits, RoundingMode mode) { if (y.isZero() || y.isNaN() || y.isInfinity()) return true; int bit_length = static_cast(y.mantissa().bitLength()); if (bit_length <= target_bits) return true; // No rounding needed // Number of guard bits usable for the decision (margin=2 accounting for computation error < 1 ulp) constexpr int MARGIN = 2; int usable = guard_bits - MARGIN; if (usable < 1) return false; // Guard bit region: bit positions [shift - guard_bits, shift - 1] // shift = bit_length - target_bits int shift = bit_length - target_bits; const uint64_t* data = y.mantissa().data(); // Read the usable guard bits // Position: bit (shift - 1) is the most significant guard bit, bit (shift - usable) the least significant // If these are all 0 or all 1, we are too close to the rounding boundary int hi_pos = shift - 1; // Most significant guard bit position int lo_pos = shift - usable; // Least significant guard bit position if (lo_pos < 0) lo_pos = 0; // Check whether all bits are 0 bool all_zero = true; // Check whether all bits are 1 bool all_one = true; // Scan word by word size_t lo_word = static_cast(lo_pos) / 64; size_t hi_word = static_cast(hi_pos) / 64; for (size_t w = lo_word; w <= hi_word; ++w) { uint64_t word = data[w]; // Build the mask for the bit range examined in this word unsigned lo_bit = (w == lo_word) ? static_cast(lo_pos % 64) : 0; unsigned hi_bit = (w == hi_word) ? static_cast(hi_pos % 64) : 63; unsigned width = hi_bit - lo_bit + 1; uint64_t mask; if (width >= 64) { mask = ~uint64_t(0); } else { mask = ((uint64_t(1) << width) - 1) << lo_bit; } uint64_t bits = word & mask; if (bits != 0) all_zero = false; if (bits != mask) all_one = false; // Early exit if both are false if (!all_zero && !all_one) return true; } // All 0 or all 1 → too close to the rounding boundary, direction unknown return false; } // Ziv's iteration strategy template. // Calls computeFunc(x, eff_x, precision) → Float and guarantees correct rounding. // computeFunc's precision is in decimal digits. Assumes +10 guard digits are added internally. template static Float zivRound(ComputeFunc&& computeFunc, const Float& x, int eff_x, int precision) { constexpr int MAX_ITER = 6; int target_bits = Float::precisionToBits(precision); RoundingMode mode = Float::roundingMode(); int extra_guard = 10; // First pass: 10 guard digits ≈ 33 bits for (int iter = 0; iter < MAX_ITER; ++iter) { int wp = precision + extra_guard; Float y = computeFunc(x, eff_x, wp); int actual_bits = static_cast(y.mantissa().bitLength()); int guard_bits = actual_bits - target_bits; if (guard_bits > 0 && canRoundCorrectly(y, target_bits, guard_bits, mode)) { finalizeResult(y, eff_x, precision); return y; } // Increase guard bits and retry extra_guard *= 2; } // Fallback: faithful rounding (practically never reached) Float y = computeFunc(x, eff_x, precision + extra_guard); finalizeResult(y, eff_x, precision); return y; } //============================================================================= // Direct mpn computation — eliminating allocation in the Taylor series loop //============================================================================= // The mpn namespace is sangi::mpn (defined in MpnOps.hpp) // Lightweight floating point: pointer into the arena + word count + exponent struct RawFloat { uint64_t* d; // Mantissa (in arena, d[0]=LSB) size_t nw; // Actual word count (leading zeros removed) int64_t exp; // Binary exponent (value = mantissa * 2^exp) }; // Mantissa normalization: place the MSB at bit 63 of the most significant word // Call after rf_mul/rf_divmod_1 to keep the exponent consistent static void rf_normalize(RawFloat& a) { if (a.nw == 0) return; // Remove leading zero words a.nw = mpn::normalized_size(a.d, a.nw); if (a.nw == 0) return; // Bit level: left-shift by the leading zeros of the most significant word unsigned lz = static_cast(std::countl_zero(a.d[a.nw - 1])); if (lz > 0) { for (size_t i = a.nw - 1; i > 0; --i) { a.d[i] = (a.d[i] << lz) | (a.d[i - 1] >> (64 - lz)); } a.d[0] <<= lz; a.exp -= static_cast(lz); } } // Multiply + truncate: dst = a * b, keeping only the high target_nw words static void rf_mul(RawFloat& dst, const RawFloat& a, const RawFloat& b, uint64_t* prod_buf, uint64_t* scratch, size_t target_nw) { if (a.nw == 0 || b.nw == 0) { dst.nw = 0; dst.exp = 0; return; } int64_t prod_exp = a.exp + b.exp; // mulhigh_n optimization: if both are target_nw size, compute only the high part (cost ≈ 2/3) if (a.nw == target_nw && b.nw == target_nw && target_nw >= 8) { // Verify scratch is sufficient (multiply_scratch_size >= mulhigh_scratch_size) mpn::mulhigh_n(prod_buf, a.d, b.d, target_nw, scratch); // mulhigh_n yields rp[0..n-1] = the high n words (approximate, O(1) error) std::memcpy(dst.d, prod_buf, target_nw * sizeof(uint64_t)); dst.nw = mpn::normalized_size(dst.d, target_nw); // The mulhigh result is an approximation of a*b >> (target_nw * 64) dst.exp = prod_exp + static_cast(target_nw) * 64; rf_normalize(dst); return; } size_t prod_n = a.nw + b.nw; mpn::multiply(prod_buf, a.d, a.nw, b.d, b.nw, scratch); size_t actual = mpn::normalized_size(prod_buf, prod_n); if (actual > target_nw) { size_t drop = actual - target_nw; std::memcpy(dst.d, prod_buf + drop, target_nw * sizeof(uint64_t)); dst.nw = mpn::normalized_size(dst.d, target_nw); dst.exp = prod_exp + static_cast(drop) * 64; } else { std::memcpy(dst.d, prod_buf, actual * sizeof(uint64_t)); dst.nw = actual; dst.exp = prod_exp; } rf_normalize(dst); } // Cached multiply: dst = a * b, caching the forward NTT of b // When b is constant and multiplied repeatedly, the forward NTT can be skipped once static void rf_mul_cached(RawFloat& dst, const RawFloat& a, const RawFloat& b, uint64_t* prod_buf, size_t target_nw, prime_ntt::NttCache& cache) { if (a.nw == 0 || b.nw == 0) { dst.nw = 0; dst.exp = 0; return; } // Below the NTT threshold, fall back to ordinary multiplication size_t min_nw = std::min(a.nw, b.nw); if (min_nw < 3000) { // scratch needed → allocate thread_local thread_local std::vector scratch_buf; size_t scratch_need = mpn::multiply_scratch_size( std::max(a.nw, b.nw), min_nw); if (scratch_buf.size() < scratch_need) scratch_buf.resize(scratch_need); rf_mul(dst, a, b, prod_buf, scratch_buf.data(), target_nw); return; } size_t prod_n = a.nw + b.nw; prime_ntt::mul_prime_ntt_cached(prod_buf, a.d, a.nw, b.d, b.nw, cache); size_t actual = mpn::normalized_size(prod_buf, prod_n); int64_t prod_exp = a.exp + b.exp; if (actual > target_nw) { size_t drop = actual - target_nw; std::memcpy(dst.d, prod_buf + drop, target_nw * sizeof(uint64_t)); dst.nw = mpn::normalized_size(dst.d, target_nw); dst.exp = prod_exp + static_cast(drop) * 64; } else { std::memcpy(dst.d, prod_buf, actual * sizeof(uint64_t)); dst.nw = actual; dst.exp = prod_exp; } rf_normalize(dst); } // Square + truncate: dst = a^2, keeping only the high target_nw words // mpn::square is faster than multiply (skips the half-diagonal additions) static void rf_sqr(RawFloat& dst, const RawFloat& a, uint64_t* prod_buf, uint64_t* scratch, size_t target_nw) { if (a.nw == 0) { dst.nw = 0; dst.exp = 0; return; } size_t prod_n = 2 * a.nw; mpn::square(prod_buf, a.d, a.nw, scratch); size_t actual = mpn::normalized_size(prod_buf, prod_n); int64_t prod_exp = a.exp + a.exp; if (actual > target_nw) { size_t drop = actual - target_nw; std::memcpy(dst.d, prod_buf + drop, target_nw * sizeof(uint64_t)); dst.nw = mpn::normalized_size(dst.d, target_nw); dst.exp = prod_exp + static_cast(drop) * 64; } else { std::memcpy(dst.d, prod_buf, actual * sizeof(uint64_t)); dst.nw = actual; dst.exp = prod_exp; } rf_normalize(dst); } // Single-limb division (in place): a /= divisor static void rf_divmod_1(RawFloat& a, uint64_t divisor) { if (a.nw == 0) return; mpn::divmod_1(a.d, a.d, a.nw, divisor); rf_normalize(a); } // Single-limb multiply (in place): a *= multiplier static void rf_mul_1(RawFloat& a, uint64_t multiplier) { if (a.nw == 0 || multiplier == 0) { a.nw = 0; a.exp = 0; return; } if (multiplier == 1) return; uint64_t carry = mpn::mul_1(a.d, a.d, a.nw, multiplier); if (carry) { a.d[a.nw] = carry; a.nw++; } rf_normalize(a); } // Right-shift src by shift_bits and add to dst (in place) // The overlapping part of dst[0..dst_nw-1] += (src >> shift_bits) // Return value: carry beyond dst_nw (0 or 1) static uint64_t shift_add(uint64_t* dst, size_t dst_nw, const uint64_t* src, size_t src_nw, size_t shift_bits) { size_t word_shift = shift_bits / 64; unsigned bit_shift = static_cast(shift_bits % 64); if (word_shift >= src_nw) return 0; size_t remaining = src_nw - word_shift; size_t overlap = std::min(remaining, dst_nw); if (overlap == 0) return 0; uint64_t carry = 0; if (bit_shift == 0) { carry = mpn::add(dst, dst, overlap, src + word_shift, overlap); } else { for (size_t i = 0; i < overlap; ++i) { uint64_t lo = src[word_shift + i] >> bit_shift; uint64_t hi = (word_shift + i + 1 < src_nw) ? (src[word_shift + i + 1] << (64 - bit_shift)) : 0; uint64_t shifted = lo | hi; uint64_t sum = dst[i] + shifted; uint64_t c = (sum < dst[i]) ? 1ULL : 0ULL; sum += carry; c += (sum < carry) ? 1ULL : 0ULL; dst[i] = sum; carry = c; } } // Carry propagation for (size_t i = overlap; carry && i < dst_nw; ++i) { dst[i] += carry; carry = (dst[i] < carry) ? 1ULL : 0ULL; } return carry; } // Right-shift src by shift_bits and subtract from dst (in place) // The overlapping part of dst[0..dst_nw-1] -= (src >> shift_bits) // Return value: borrow (0 or 1) static uint64_t shift_sub(uint64_t* dst, size_t dst_nw, const uint64_t* src, size_t src_nw, size_t shift_bits) { size_t word_shift = shift_bits / 64; unsigned bit_shift = static_cast(shift_bits % 64); if (word_shift >= src_nw) return 0; size_t remaining = src_nw - word_shift; size_t overlap = std::min(remaining, dst_nw); if (overlap == 0) return 0; uint64_t borrow = 0; if (bit_shift == 0) { borrow = mpn::sub(dst, dst, overlap, src + word_shift, overlap); } else { for (size_t i = 0; i < overlap; ++i) { uint64_t lo = src[word_shift + i] >> bit_shift; uint64_t hi = (word_shift + i + 1 < src_nw) ? (src[word_shift + i + 1] << (64 - bit_shift)) : 0; uint64_t shifted = lo | hi; uint64_t diff = dst[i] - shifted; uint64_t b = (diff > dst[i]) ? 1ULL : 0ULL; uint64_t diff2 = diff - borrow; b += (diff2 > diff) ? 1ULL : 0ULL; dst[i] = diff2; borrow = b; } } // Borrow propagation for (size_t i = overlap; borrow && i < dst_nw; ++i) { uint64_t old_val = dst[i]; dst[i] -= borrow; borrow = (dst[i] > old_val) ? 1ULL : 0ULL; } return borrow; } // Addition (in place): result += term (bit-level alignment) // Return value: if false, term is negligible (used for convergence detection) static bool rf_add(RawFloat& result, const RawFloat& term, size_t nw_max) { if (term.nw == 0) return false; if (result.nw == 0) { std::memcpy(result.d, term.d, term.nw * sizeof(uint64_t)); result.nw = term.nw; result.exp = term.exp; return true; } int64_t exp_diff = result.exp - term.exp; if (exp_diff < 0) { // term has the larger exponent: result = term + (old result >> |exp_diff|) size_t neg_diff = static_cast(-exp_diff); if (neg_diff / 64 >= result.nw) { // result is negligible → overwrite with term std::memcpy(result.d, term.d, term.nw * sizeof(uint64_t)); result.nw = term.nw; result.exp = term.exp; return true; } // Save the old result (rare case — only on the first Taylor pass) std::vector old_data(result.d, result.d + result.nw); size_t old_nw = result.nw; // result ← term std::memset(result.d, 0, nw_max * sizeof(uint64_t)); std::memcpy(result.d, term.d, term.nw * sizeof(uint64_t)); result.nw = term.nw; result.exp = term.exp; // result += (old result >> neg_diff) uint64_t carry = shift_add(result.d, result.nw, old_data.data(), old_nw, neg_diff); if (carry && result.nw < nw_max) { result.d[result.nw] = carry; result.nw++; } return true; } // exp_diff >= 0: result += (term >> exp_diff) size_t total_shift = static_cast(exp_diff); if (total_shift / 64 >= term.nw) return false; // negligible uint64_t carry = shift_add(result.d, result.nw, term.d, term.nw, total_shift); if (carry && result.nw < nw_max) { result.d[result.nw] = carry; result.nw++; } return true; } // Subtraction (in place): result -= term (for sin/cos) static bool rf_sub(RawFloat& result, const RawFloat& term, size_t nw_max) { if (term.nw == 0) return false; if (result.nw == 0) return false; int64_t exp_diff = result.exp - term.exp; if (exp_diff < 0) return true; // term > result: theoretically unreachable size_t total_shift = static_cast(exp_diff); if (total_shift / 64 >= term.nw) return false; // negligible shift_sub(result.d, result.nw, term.d, term.nw, total_shift); result.nw = mpn::normalized_size(result.d, result.nw); return true; } // Float → RawFloat extraction (normalized, allocated in the arena) static RawFloat rf_extract(const Float& x, int nw, ScratchArena& arena) { auto x_vec = x.mantissa().words(); size_t x_nw_orig = x_vec.size(); int64_t x_exp = x.exponent(); uint64_t* x_d; size_t x_nw; if (x_nw_orig > static_cast(nw)) { x_d = arena.alloc_limbs(nw); size_t drop = x_nw_orig - nw; std::memcpy(x_d, x_vec.data() + drop, nw * sizeof(uint64_t)); x_exp += static_cast(drop) * 64; x_nw = mpn::normalized_size(x_d, nw); } else { x_d = arena.alloc_limbs(x_nw_orig); std::memcpy(x_d, x_vec.data(), x_nw_orig * sizeof(uint64_t)); x_nw = x_nw_orig; } RawFloat rf{x_d, x_nw, x_exp}; rf_normalize(rf); return rf; } // RawFloat → Float conversion static Float rf_to_float(const RawFloat& rf, bool negative, int working_precision) { if (rf.nw == 0) return Float(); std::vector words(rf.d, rf.d + rf.nw); Int mantissa = Int::fromRawWords(words, 1); Float result(mantissa, rf.exp, negative); int wp_bits = Float::precisionToBits(working_precision); result.setEffectiveBits(wp_bits); result.truncateToApprox(working_precision); return result; } //============================================================================= // Exponential function (exp) — Taylor series + argument halving/squaring reconstruction //============================================================================= // Naive Taylor series (RawFloat output version, handles the sign of x) // Version with divmod_1 + normalize separated (normalize can be deferred until before multiplication) static void rf_divmod_1_no_norm(RawFloat& a, uint64_t divisor) { if (a.nw == 0) return; mpn::divmod_1(a.d, a.d, a.nw, divisor); // Do not call normalize — only remove leading zero words a.nw = mpn::normalized_size(a.d, a.nw); } static void rf_exp_naive(RawFloat& result, const RawFloat& x_rf, bool x_negative, int nw, uint64_t* prod_buf, uint64_t* scratch, ScratchArena& arena, int working_precision) { uint64_t* term_d = arena.alloc_limbs(nw + 2); // term = 1.0 std::memset(term_d, 0, (nw + 2) * sizeof(uint64_t)); term_d[nw - 1] = uint64_t(1) << 63; RawFloat term{term_d, static_cast(nw), -static_cast(nw * 64 - 1)}; // result = 1.0 std::memset(result.d, 0, (nw + 2) * sizeof(uint64_t)); result.d[nw - 1] = uint64_t(1) << 63; result.nw = static_cast(nw); result.exp = -static_cast(nw * 64 - 1); int max_terms = static_cast(working_precision * 1.2) + 10; for (int n = 1; n <= max_terms; ++n) { // term = term * x / n // rf_mul normalizes internally (the multiplication result must be normalized) rf_mul(term, term, x_rf, prod_buf, scratch, nw); // divmod_1: skip normalize, only remove leading zero words // (the next rf_mul will normalize, so bit-level normalize is unnecessary) // However, since rf_add/rf_sub determine position from exp_diff, // a correct exp is needed → normalize before add rf_divmod_1_no_norm(term, static_cast(n)); if (term.nw == 0) break; // normalize before add/sub (exp accuracy is required) rf_normalize(term); if (x_negative && (n & 1)) { if (!rf_sub(result, term, nw + 1)) break; } else { if (!rf_add(result, term, nw + 1)) break; } } } // Paterson-Stockmeyer Taylor series (assumes x > 0) // O(sqrt(l)) full-size multiplications + O(l) single-word divisions // Algorithm equivalent to MPFR exp2_aux2 static void rf_exp_ps(RawFloat& result, const RawFloat& x_rf, int nw, uint64_t* prod_buf, uint64_t* scratch, ScratchArena& arena, int working_precision) { int target_bits = nw * 64; // Term count estimate: |x| ≈ 2^x_msb, each term decreases by ~|x_msb| bits int64_t x_msb = x_rf.exp + static_cast(x_rf.nw) * 64; int x_log2 = (x_msb <= 0) ? static_cast(-x_msb) : 1; if (x_log2 < 1) x_log2 = 1; int l_est = target_bits / x_log2 + 10; int m = static_cast(std::sqrt(static_cast(l_est))); if (m < 2) m = 2; if (m > 256) m = 256; // Precompute R[0..m]: R[i] = x^i (arena + fixed array to avoid heap) constexpr int R_MAX = 260; // The upper bound on m is 256 RawFloat R[R_MAX]; for (int i = 0; i <= m; ++i) { R[i].d = arena.alloc_limbs(nw + 2); std::memset(R[i].d, 0, (nw + 2) * sizeof(uint64_t)); R[i].nw = 0; R[i].exp = 0; } // R[0] = 1.0 R[0].d[nw - 1] = uint64_t(1) << 63; R[0].nw = static_cast(nw); R[0].exp = -static_cast(nw * 64 - 1); // R[1] = x std::memcpy(R[1].d, x_rf.d, x_rf.nw * sizeof(uint64_t)); R[1].nw = x_rf.nw; R[1].exp = x_rf.exp; // R[2..m]: even indices via squaring (rf_sqr), odd indices via R[i-1]*R[1] // NTT cache of R[1] (reused for computing odd R[i]) prime_ntt::NttCache cache_r1; rf_sqr(R[2], R[1], prod_buf, scratch, nw); for (int i = 3; i <= m; ++i) { if ((i & 1) == 0) { rf_sqr(R[i], R[i/2], prod_buf, scratch, nw); } else { rf_mul_cached(R[i], R[i-1], R[1], prod_buf, nw, cache_r1); } } // NTT cache of R[m] (used repeatedly in the giant steps) prime_ntt::NttCache cache_rm; // rr = 1.0 (tracks x^l / l!) RawFloat rr; rr.d = arena.alloc_limbs(nw + 2); std::memset(rr.d, 0, (nw + 2) * sizeof(uint64_t)); rr.d[nw - 1] = uint64_t(1) << 63; rr.nw = static_cast(nw); rr.exp = -static_cast(nw * 64 - 1); // t = baby step working space RawFloat t; t.d = arena.alloc_limbs(nw + 2); // result = 0 std::memset(result.d, 0, (nw + 2) * sizeof(uint64_t)); result.nw = 0; result.exp = 0; int l = 0; int max_giant_steps = l_est / m + 5; for (int gs = 0; gs < max_giant_steps; ++gs) { // Baby step: evaluate sum_{i=0}^{m-1} R[i] * l!/(l+i)! via Horner std::memcpy(t.d, R[m-1].d, R[m-1].nw * sizeof(uint64_t)); t.nw = R[m-1].nw; t.exp = R[m-1].exp; for (int i = m - 2; i >= 0; --i) { rf_divmod_1(t, static_cast(l + i + 1)); if (t.nw == 0) { std::memcpy(t.d, R[i].d, R[i].nw * sizeof(uint64_t)); t.nw = R[i].nw; t.exp = R[i].exp; } else { rf_add(t, R[i], nw + 1); } } // t *= rr (on the first giant step rr = 1, so no multiplication needed) if (gs > 0) { rf_mul(t, t, rr, prod_buf, scratch, nw); } if (t.nw == 0) break; // result += t rf_add(result, t, nw + 1); // Update rr: rr = rr * R[m] / ((l+1)(l+2)...(l+m)) rf_mul_cached(rr, rr, R[m], prod_buf, nw, cache_rm); for (int i = 1; i <= m; ++i) { rf_divmod_1(rr, static_cast(l + i)); } l += m; if (rr.nw == 0) break; } } // Term count estimate for sin/cos Taylor (accounting for Stirling approximation) // Solve u^l / (2l)! < 2^(-target_bits) // log2(u^l / (2l)!) ≈ l*log2(u) - 2l*log2(2l/e) = -l*(u_log2 + 2*log2(2l/e)) static int estimate_sincos_terms(int target_bits, int u_log2) { double tb = static_cast(target_bits); double ul = static_cast(u_log2); // Initial estimate (no factorial) double l_approx = tb / std::max(1.0, ul); // Converge via Stirling iteration for (int iter = 0; iter < 8; ++iter) { double two_l = 2.0 * l_approx; double factorial_bits = two_l * std::log2(std::max(2.0, two_l / 2.718281828459045)); double bits_per_term = ul + factorial_bits / l_approx; double new_l = tb / std::max(1.0, bits_per_term); if (std::abs(new_l - l_approx) < 1.0) break; l_approx = new_l; } return static_cast(l_approx) + 10; } // Paterson-Stockmeyer for the sin Taylor series // sin(x) = x * Σ_{k=0}^{∞} (-1)^k * u^k / (2k+1)! where u = x² // O(sqrt(l)) full-size multiplications + O(l) single-word divisions static void rf_sin_ps(RawFloat& result, const RawFloat& x_rf, const RawFloat& x2, int nw, uint64_t* prod_buf, uint64_t* scratch, ScratchArena& arena, int working_precision) { int target_bits = nw * 64; // Estimate term count from the MSB of u = x² (Stirling approximation) int64_t u_msb = x2.exp + static_cast(x2.nw) * 64; int u_log2 = (u_msb <= 0) ? static_cast(-u_msb) : 1; if (u_log2 < 1) u_log2 = 1; int l_est = estimate_sincos_terms(target_bits, u_log2); int m = static_cast(std::sqrt(static_cast(l_est))); if (m < 2) m = 2; if (m > 256) m = 256; // Precompute R[0..m]: R[i] = u^i std::vector R(m + 1); for (int i = 0; i <= m; ++i) { R[i].d = arena.alloc_limbs(nw + 2); std::memset(R[i].d, 0, (nw + 2) * sizeof(uint64_t)); R[i].nw = 0; R[i].exp = 0; } R[0].d[nw - 1] = uint64_t(1) << 63; R[0].nw = static_cast(nw); R[0].exp = -static_cast(nw * 64 - 1); std::memcpy(R[1].d, x2.d, x2.nw * sizeof(uint64_t)); R[1].nw = x2.nw; R[1].exp = x2.exp; // NTT cache of R[1] prime_ntt::NttCache sin_cache_r1; rf_sqr(R[2], R[1], prod_buf, scratch, nw); for (int i = 3; i <= m; ++i) { if ((i & 1) == 0) rf_sqr(R[i], R[i/2], prod_buf, scratch, nw); else rf_mul_cached(R[i], R[i-1], R[1], prod_buf, nw, sin_cache_r1); } // NTT cache of R[m] (reused in the giant steps) prime_ntt::NttCache sin_cache_rm; // rr = 1.0 (tracks |u^l / (2l+1)!|) RawFloat rr; rr.d = arena.alloc_limbs(nw + 2); std::memset(rr.d, 0, (nw + 2) * sizeof(uint64_t)); rr.d[nw - 1] = uint64_t(1) << 63; rr.nw = static_cast(nw); rr.exp = -static_cast(nw * 64 - 1); // t = baby step working space, tmp = temporary buffer for R[i]-t RawFloat t; t.d = arena.alloc_limbs(nw + 2); uint64_t* tmp_d = arena.alloc_limbs(nw + 2); // result = 0 std::memset(result.d, 0, (nw + 2) * sizeof(uint64_t)); result.nw = 0; result.exp = 0; int l = 0; int max_giant_steps = l_est / m + 5; for (int gs = 0; gs < max_giant_steps; ++gs) { // Baby step: evaluate Σ_{i=0}^{m-1} (-1)^i * u^i / (partial denominator product) via Horner // t = R[m-1]; for i=m-2..0: t /= (2(l+i+1))(2(l+i+1)+1); t = R[i] - t std::memcpy(t.d, R[m-1].d, R[m-1].nw * sizeof(uint64_t)); t.nw = R[m-1].nw; t.exp = R[m-1].exp; for (int i = m - 2; i >= 0; --i) { uint64_t d1 = static_cast(2*(l+i+1)); uint64_t d2 = d1 + 1; rf_divmod_1(t, d1 * d2); if (t.nw == 0) { std::memcpy(t.d, R[i].d, R[i].nw * sizeof(uint64_t)); t.nw = R[i].nw; t.exp = R[i].exp; } else { // t = R[i] - t (alternating series sign handling) std::memset(tmp_d, 0, (nw + 2) * sizeof(uint64_t)); std::memcpy(tmp_d, R[i].d, R[i].nw * sizeof(uint64_t)); RawFloat tmp{tmp_d, R[i].nw, R[i].exp}; rf_sub(tmp, t, nw + 1); std::memcpy(t.d, tmp.d, tmp.nw * sizeof(uint64_t)); std::memset(t.d + tmp.nw, 0, (nw + 2 - tmp.nw) * sizeof(uint64_t)); t.nw = tmp.nw; t.exp = tmp.exp; } } // t *= rr (on the first giant step rr = 1, so no multiplication needed) if (gs > 0) { rf_mul(t, t, rr, prod_buf, scratch, nw); } if (t.nw == 0) break; // Add or subtract to result (depending on (-1)^l) if (l % 2 == 0) { rf_add(result, t, nw + 1); } else { rf_sub(result, t, nw + 1); } // Update rr: rr *= R[m] / Π_{j=0}^{m-1} ((2(l+j)+2)(2(l+j)+3)) rf_mul_cached(rr, rr, R[m], prod_buf, nw, sin_cache_rm); for (int j = 0; j < m; ++j) { uint64_t d1 = static_cast(2*(l+j) + 2); uint64_t d2 = d1 + 1; rf_divmod_1(rr, d1 * d2); } l += m; if (rr.nw == 0) break; } // sin(x) = x * S(u): result *= x rf_mul(result, result, x_rf, prod_buf, scratch, nw); } // Paterson-Stockmeyer for the cos Taylor series // cos(x) = Σ_{k=0}^{∞} (-1)^k * u^k / (2k)! where u = x² static void rf_cos_ps(RawFloat& result, const RawFloat& x2, int nw, uint64_t* prod_buf, uint64_t* scratch, ScratchArena& arena, int working_precision) { int target_bits = nw * 64; int64_t u_msb = x2.exp + static_cast(x2.nw) * 64; int u_log2 = (u_msb <= 0) ? static_cast(-u_msb) : 1; if (u_log2 < 1) u_log2 = 1; int l_est = estimate_sincos_terms(target_bits, u_log2); int m = static_cast(std::sqrt(static_cast(l_est))); if (m < 2) m = 2; if (m > 256) m = 256; // R[0..m] = u^i std::vector R(m + 1); for (int i = 0; i <= m; ++i) { R[i].d = arena.alloc_limbs(nw + 2); std::memset(R[i].d, 0, (nw + 2) * sizeof(uint64_t)); R[i].nw = 0; R[i].exp = 0; } R[0].d[nw - 1] = uint64_t(1) << 63; R[0].nw = static_cast(nw); R[0].exp = -static_cast(nw * 64 - 1); std::memcpy(R[1].d, x2.d, x2.nw * sizeof(uint64_t)); R[1].nw = x2.nw; R[1].exp = x2.exp; // NTT cache of R[1] prime_ntt::NttCache cos_cache_r1; rf_sqr(R[2], R[1], prod_buf, scratch, nw); for (int i = 3; i <= m; ++i) { if ((i & 1) == 0) rf_sqr(R[i], R[i/2], prod_buf, scratch, nw); else rf_mul_cached(R[i], R[i-1], R[1], prod_buf, nw, cos_cache_r1); } // NTT cache of R[m] prime_ntt::NttCache cos_cache_rm; RawFloat rr; rr.d = arena.alloc_limbs(nw + 2); std::memset(rr.d, 0, (nw + 2) * sizeof(uint64_t)); rr.d[nw - 1] = uint64_t(1) << 63; rr.nw = static_cast(nw); rr.exp = -static_cast(nw * 64 - 1); RawFloat t; t.d = arena.alloc_limbs(nw + 2); uint64_t* tmp_d = arena.alloc_limbs(nw + 2); std::memset(result.d, 0, (nw + 2) * sizeof(uint64_t)); result.nw = 0; result.exp = 0; int l = 0; int max_giant_steps = l_est / m + 5; for (int gs = 0; gs < max_giant_steps; ++gs) { // Baby step: Horner // cos denominator: (2(l+i+1)-1)(2(l+i+1)) = (2l+2i+1)(2l+2i+2) std::memcpy(t.d, R[m-1].d, R[m-1].nw * sizeof(uint64_t)); t.nw = R[m-1].nw; t.exp = R[m-1].exp; for (int i = m - 2; i >= 0; --i) { uint64_t d1 = static_cast(2*(l+i+1) - 1); uint64_t d2 = d1 + 1; rf_divmod_1(t, d1 * d2); if (t.nw == 0) { std::memcpy(t.d, R[i].d, R[i].nw * sizeof(uint64_t)); t.nw = R[i].nw; t.exp = R[i].exp; } else { std::memset(tmp_d, 0, (nw + 2) * sizeof(uint64_t)); std::memcpy(tmp_d, R[i].d, R[i].nw * sizeof(uint64_t)); RawFloat tmp{tmp_d, R[i].nw, R[i].exp}; rf_sub(tmp, t, nw + 1); std::memcpy(t.d, tmp.d, tmp.nw * sizeof(uint64_t)); std::memset(t.d + tmp.nw, 0, (nw + 2 - tmp.nw) * sizeof(uint64_t)); t.nw = tmp.nw; t.exp = tmp.exp; } } if (gs > 0) { rf_mul(t, t, rr, prod_buf, scratch, nw); } if (t.nw == 0) break; if (l % 2 == 0) { rf_add(result, t, nw + 1); } else { rf_sub(result, t, nw + 1); } // Update rr: rr *= R[m] / Π_{j=0}^{m-1} ((2(l+j)+1)(2(l+j)+2)) rf_mul_cached(rr, rr, R[m], prod_buf, nw, cos_cache_rm); for (int j = 0; j < m; ++j) { uint64_t d1 = static_cast(2*(l+j) + 1); uint64_t d2 = d1 + 1; rf_divmod_1(rr, d1 * d2); } l += m; if (rr.nw == 0) break; } } // Forward declarations: Q128/Q256 fast path (definitions come after q128_mul etc.) static Float exp_small(const Float& x, int precision); static Float exp_medium(const Float& x, int precision); static Float sin_core(Float x, int eff_x, int precision); static Float cos_core(Float x, int eff_x, int precision); // Fast Taylor series via direct mpn computation (zero allocation inside the loop) // Legacy interface compatible — for low-precision fallback static Float expTaylor(const Float& x, int working_precision) { int wp_bits = Float::precisionToBits(working_precision); int nw = (wp_bits + 63) / 64; ScratchScope scope; auto& arena = getThreadArena(); // Pre-allocate buffers uint64_t* term_d = arena.alloc_limbs(nw + 2); uint64_t* result_d = arena.alloc_limbs(nw + 2); size_t prod_alloc = 2 * static_cast(nw) + 4; uint64_t* prod_buf = arena.alloc_limbs(prod_alloc); size_t scratch_sz = mpn::multiply_scratch_size(nw + 1, nw + 1); uint64_t* scratch = arena.alloc_limbs(scratch_sz > 0 ? scratch_sz : 1); // Extract the mantissa/exponent of x (normalized) RawFloat x_rf = rf_extract(x, nw, arena); bool x_negative = x.isNegative(); // term = 1.0: mantissa = 2^(nw*64-1), exponent = -(nw*64-1) std::memset(term_d, 0, (nw + 2) * sizeof(uint64_t)); term_d[nw - 1] = uint64_t(1) << 63; RawFloat term{term_d, static_cast(nw), -static_cast(nw * 64 - 1)}; // result = 1.0 std::memset(result_d, 0, (nw + 2) * sizeof(uint64_t)); result_d[nw - 1] = uint64_t(1) << 63; RawFloat result{result_d, static_cast(nw), -static_cast(nw * 64 - 1)}; int max_terms = static_cast(working_precision * 1.2) + 10; for (int n = 1; n <= max_terms; ++n) { rf_mul(term, term, x_rf, prod_buf, scratch, nw); rf_divmod_1(term, static_cast(n)); if (term.nw == 0) break; // When x < 0, odd terms are negative (x^n/n! with n odd) if (x_negative && (n & 1)) { if (!rf_sub(result, term, nw + 1)) break; } else { if (!rf_add(result, term, nw + 1)) break; } } return rf_to_float(result, false, working_precision); } // exp computation by squaring: halve K times → Taylor → reconstruct by squaring K times // exp(x) = exp(x/2^K)^{2^K} // Taylor: switches between the naive method (low precision) and Paterson-Stockmeyer (high precision). // The squaring reconstruction is done with RawFloat to avoid Float overhead. static Float expDoubling(const Float& x, int working_precision) { int wp_bits = Float::precisionToBits(working_precision); // P-S threshold: use P-S + cuberoot K at ~2000 digits (7000 bits) or above // Below that, the PS baby step scalar cost (m × divmod_1 + m × rf_add // per giant step) outweighs the multiplication cost reduction, so naive Taylor is faster bool use_ps = (wp_bits >= 7000); int K; if (use_ps) { K = static_cast(std::cbrt(4.0 * wp_bits)); } else { // Cost-model optimization: K ≈ α·√B // Minimize the trade-off between Taylor term count ≈ B/(K+x_shift) vs K squaring reconstructions. // Accounting for sqr_cost ≈ 0.7×mul_cost gives K_opt = √(B/0.7) ≈ 1.2×√B. // However, at high precision the nw increase from guard bits (K+log2(K)+10) dominates, so // adjust the coefficient by precision: ≤2000 bits → 1.2, >2000 bits → 1.0 double alpha = (wp_bits <= 2000) ? 1.2 : 1.0; K = static_cast(alpha * std::sqrt(static_cast(wp_bits))); } // Small-argument optimization: when |x| ≈ 2^{-B}, x is already small, so // K squaring reconstructions are unnecessary. Reduce K by B to save squaring cost. // Example: |x| ≈ 2^{-45} with K=51 → K=6, saving 45 squarings. // The Taylor/P-S term count is p/(B+K), and the larger B is, the fewer are needed. if (!x.isZero()) { int64_t msb_pos = x.exponent() + static_cast(x.mantissa().bitLength()); if (msb_pos < 0) { int arg_zeros = static_cast( std::min(-msb_pos, static_cast(K))); K -= arg_zeros; } } if (K < 1) return expTaylor(x, working_precision); // Guard bits: add K+α bits since the squaring reconstruction amplifies error by up to 2^K int guard_bits = K + static_cast(std::ceil(std::log2(K + 1))) + 10; int wp_inner = working_precision + Float::bitsToPrecision(guard_bits); int wp_inner_bits = Float::precisionToBits(wp_inner); int nw = (wp_inner_bits + 63) / 64; Float x_red = ldexp(x, -K); // x / 2^K — O(1) exponent operation x_red.truncateToApprox(wp_inner); x_red.setEffectiveBits(wp_inner_bits); ScratchScope scope; auto& arena = getThreadArena(); RawFloat x_rf = rf_extract(x_red, nw, arena); bool x_negative = x.isNegative(); size_t prod_alloc = 2 * static_cast(nw) + 4; uint64_t* prod_buf = arena.alloc_limbs(prod_alloc); size_t mul_scratch = mpn::multiply_scratch_size(nw + 1, nw + 1); size_t sqr_scratch = mpn::square_scratch_size(nw + 1); size_t scratch_sz = std::max(mul_scratch, sqr_scratch); uint64_t* scratch_buf = arena.alloc_limbs(scratch_sz > 0 ? scratch_sz : 1); uint64_t* result_d = arena.alloc_limbs(nw + 2); RawFloat result{result_d, 0, 0}; // Since exp_core guarantees x ≥ 0, x_negative is always false // (negative arguments are converted to x + k·ln2 inside exp_core) if (use_ps) { rf_exp_ps(result, x_rf, nw, prod_buf, scratch_buf, arena, wp_inner); } else { rf_exp_naive(result, x_rf, x_negative, nw, prod_buf, scratch_buf, arena, wp_inner); } // K squaring reconstructions: maintain full nw at every step // // The old implementation did gradual precision reduction (at step i, nw/2^(K-1-i) + guard), // but this rested on the mistaken assumption that "squaring doubles precision". // In reality rf_sqr truncates the output to needed_nw, so squaring only // maintains or reduces precision. Truncating to 13 limbs (832 bits) at an // early step caps the final accuracy at ~125 digits when K=97 // (causes BUG_FLOAT_EXP_HIGH_PRECISION). // // Correct schedule: maintain nw at every step (each sqr has an error of about // target_bits + log2(K) ULP, so the margin is already covered by wp_inner, // which includes guard_bits = K + log2(K) + 10). for (int i = 0; i < K; ++i) { // Truncate only when result exceeds nw (keep the top nw limbs) if (static_cast(result.nw) > static_cast(nw)) { size_t drop = result.nw - nw; std::memmove(result.d, result.d + drop, nw * sizeof(uint64_t)); result.nw = nw; result.exp += static_cast(drop) * 64; } rf_sqr(result, result, prod_buf, scratch_buf, nw); } Float out = rf_to_float(result, false, wp_inner); out.truncateToApprox(working_precision); return out; } // exp via Newton iteration: y_{n+1} = y_n · (1 + x - log(y_n)) // log is AGM-based O(M(n)·log n), so with precision doubling the total cost ≈ 2 × log(n). // The argument x is already reduced (0 <= x < log2). static Float exp_newton(const Float& x, int working_precision) { // Initial approximation: double-precision exp double x_d = x.toDouble(); Float y(std::exp(x_d)); int target_bits = Float::precisionToBits(working_precision); // Precision-doubling loop: correct_bits doubles 53→106→212→... via Newton's quadratic convergence // Each step's computation precision = 2*correct_bits + guard (to compute the correction accurately) int correct_bits = 53; // double-precision initial approximation int guard = 64; while (correct_bits < target_bits) { // Computation precision: 2*correct_bits is needed to compute the Newton correction int compute_bits = std::min(2 * correct_bits + guard, target_bits + guard); int step_prec = Float::bitsToPrecision(compute_bits); // Match effective_bits_ to this step's precision (see lessons-learned) y.setEffectiveBits(Float::precisionToBits(step_prec)); y.setResultPrecision(step_prec); // Compute log(y) at this step's precision Float log_y = log(y, step_prec); // delta = x - log(y) Float delta = Float(x); delta.truncateToApprox(step_prec); delta = delta - log_y; // y = y * (1 + delta) = y + y * delta Float one_plus_delta = Float::one(step_prec) + delta; y = y * one_plus_delta; y.truncateToApprox(step_prec); // Quadratic convergence: correct_bits doubles (capped at the computation precision) correct_bits = std::min(2 * correct_bits, compute_bits); } y.truncateToApprox(working_precision); return y; } // Body: takes x by value (assumes already moved) static Float exp_core(Float x, int eff_x, int precision) { Float::PrecisionScope _ps(precision + 64); // Compute exact÷exact inside the function at working precision int compute_prec = effectiveComputePrecision(eff_x, precision); int working_precision = compute_prec + 10; // Argument reduction: exp(x) = 2^k * exp(x - k*log2), x - k*log2 ∈ [0, log2) // double precision suffices for k (|k| < 1500, so exact in a 53-bit double) int k = 0; double x_d = x.toDouble(); if (x_d > 0.5 || x_d < -0.5) { k = static_cast(std::floor(x_d * 1.4426950408889634)); Float log2_val = Float::log2(working_precision); x = x - k * log2_val; // Guarantee x ∈ [0, log2) (correction when rounding error crosses the boundary) if (x.isNegative()) { x = x + log2_val; k--; } } else if (x.isNegative()) { // |x| <= 0.5 and x < 0: exp(x) = exp(x + log2) / 2 Float log2_val = Float::log2(working_precision); x = x + log2_val; k = -1; } x.truncateToApprox(working_precision); // High precision: Newton iteration (exp via log); low precision: Taylor + argument halving/squaring reconstruction // PS+squaring reconstruction is O(n^{1/3}·M(n)), exp_newton is O(log²n·M(n)) // n^{1/3} < log²n holds for n > ~10^7 bits, so // in the practical range PS+squaring reconstruction is almost always faster int wp_bits = Float::precisionToBits(working_precision); Float result; if (wp_bits >= 100000) { result = exp_newton(x, working_precision); } else { result = expDoubling(x, working_precision); } if (k > 0) result <<= k; else if (k < 0) result >>= -k; finalizeResult(result, eff_x, precision); return result; } Float exp(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) return x.isNegative() ? Float::zero() : Float::positiveInfinity(); if (x.isZero()) return Float::one(precision); if (x.isNegative() && x <= Float(-1000)) return Float::zero(); if (!x.isNegative() && x >= Float(1000)) return Float::positiveInfinity(); int precision_bits = Float::precisionToBits(precision); if (precision_bits <= 64) { return exp_small(x, precision); } return zivRound([](const Float& a, int e, int p) { return exp_core(Float(a), e, p); }, x, x.effectiveBits(), precision); } Float exp(Float&& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) return x.isNegative() ? Float::zero() : Float::positiveInfinity(); if (x.isZero()) return Float::one(precision); if (x.isNegative() && x <= Float(-1000)) return Float::zero(); if (!x.isNegative() && x >= Float(1000)) return Float::positiveInfinity(); int precision_bits = Float::precisionToBits(precision); if (precision_bits <= 64) { return exp_small(x, precision); } return zivRound([](const Float& a, int e, int p) { return exp_core(Float(a), e, p); }, x, x.effectiveBits(), precision); } //============================================================================= // Natural logarithm (log) //============================================================================= //------------------------------------------------------------------------- // Multi-prime argument reduction (Johansson 2022, PERF-LOG-D) // // log(x) = Σ c_i·log(p_i) + log(1+δ) // Find integer c_i via LLL lattice basis reduction, minimizing |δ|. // Reduces |δ| to about 2^{-50} → Halley's exp is fast for small arguments. // // Effect: at 2000-6000 bits (600-1800 digits), the Halley extension gives // a 20-30% speedup over AGM. Even more effective once the cache is warm. //------------------------------------------------------------------------- // Forward declaration (defined in Float.cpp) Float computeAtanhReciprocal(int n, int precision); // Forward declarations (defined later) static Float log_core(Float x, int eff_x, int precision); struct LogfResult { Float logf; int64_t k; bool f_is_one; }; static LogfResult logf_newton_core(Float x, int compute_prec); // Compute the log of small primes via the atanh BS formula // log(p) = k·log(2) + 2·atanh(1/n) (k, n depending on p) // atanh(1/n) = (1/n)·Σ (1/n²)^k/(2k+1) — fast computation via BS struct LogPrimeFormula { int prime; int log2_coeff; // coefficient of log(2) int log3_coeff; // coefficient of log(3) int log5_coeff; // coefficient of log(5) int log7_coeff; // coefficient of log(7) int log11_coeff; // coefficient of log(11) int atanh_n; // n in atanh(1/n) (0 = no atanh needed) int atanh_coeff; // coefficient of atanh(1/n) }; // Prime → atanh identity table // log(2) = 2·atanh(1/3) // log(3) = log(2) + 2·atanh(1/5) ∵ atanh(1/5)=(1/2)·log(3/2) // log(5) = 2·log(2) + 2·atanh(1/9) ∵ atanh(1/9)=(1/2)·log(5/4) // log(7) = 3·log(2) - 2·atanh(1/15) ∵ atanh(1/15)=(1/2)·log(8/7) // log(11)= log(2)+log(5) + 2·atanh(1/21) ∵ atanh(1/21)=(1/2)·log(11/10) // log(13)= 2·log(2)+log(3) + 2·atanh(1/25) ∵ atanh(1/25)=(1/2)·log(13/12) // log(17)= 4·log(2) + 2·atanh(1/33) ∵ atanh(1/33)=(1/2)·log(17/16) // log(19)= log(2)+2·log(3) + 2·atanh(1/37) ∵ atanh(1/37)=(1/2)·log(19/18) // log(23)= 3·log(2)+log(3) - 2·atanh(1/47) ∵ atanh(1/47)=(1/2)·log(24/23) // log(29)= 2·log(2)+log(7) + 2·atanh(1/57) ∵ atanh(1/57)=(1/2)·log(29/28) static constexpr int MP_NUM_PRIMES = 10; static constexpr int MP_PRIMES[MP_NUM_PRIMES] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29}; // thread_local cache: log(p_i) struct LogPrimeCacheEntry { Float value; int precision; }; static LogPrimeCacheEntry& logPrimeCacheAt(int index) { static thread_local LogPrimeCacheEntry cache[MP_NUM_PRIMES] = {}; return cache[index]; } // Compute log(prime) via atanh identities (reusing already-computed log(2), log(3), etc.) static Float computeLogPrime(int index, int precision) { int wp = precision + 15; switch (MP_PRIMES[index]) { case 2: return Float::log2(wp); case 3: { // log(3) = log(2) + 2·atanh(1/5) Float a5 = computeAtanhReciprocal(5, wp); return Float::log2(wp) + ldexp(a5, 1); } case 5: { // log(5) = 2·log(2) + 2·atanh(1/9) Float a9 = computeAtanhReciprocal(9, wp); return ldexp(Float::log2(wp), 1) + ldexp(a9, 1); } case 7: { // log(7) = 3·log(2) - 2·atanh(1/15) Float a15 = computeAtanhReciprocal(15, wp); return mulScalarF(Float::log2(wp), uint64_t(3)) - ldexp(a15, 1); } case 11: { // log(11) = log(10) + 2·atanh(1/21) // = log(2) + log(5) + 2·atanh(1/21) Float a21 = computeAtanhReciprocal(21, wp); Float log5 = computeLogPrime(2, wp); // index 2 = prime 5 return Float::log2(wp) + log5 + ldexp(a21, 1); } case 13: { // log(13) = 2·log(2) + log(3) + 2·atanh(1/25) Float a25 = computeAtanhReciprocal(25, wp); Float log3 = computeLogPrime(1, wp); return ldexp(Float::log2(wp), 1) + log3 + ldexp(a25, 1); } case 17: { // log(17) = 4·log(2) + 2·atanh(1/33) Float a33 = computeAtanhReciprocal(33, wp); return ldexp(Float::log2(wp), 2) + ldexp(a33, 1); } case 19: { // log(19) = log(2) + 2·log(3) + 2·atanh(1/37) Float a37 = computeAtanhReciprocal(37, wp); Float log3 = computeLogPrime(1, wp); return Float::log2(wp) + ldexp(log3, 1) + ldexp(a37, 1); } case 23: { // log(23) = 3·log(2) + log(3) - 2·atanh(1/47) Float a47 = computeAtanhReciprocal(47, wp); Float log3 = computeLogPrime(1, wp); return mulScalarF(Float::log2(wp), uint64_t(3)) + log3 - ldexp(a47, 1); } case 29: { // log(29) = 2·log(2) + log(7) + 2·atanh(1/57) Float a57 = computeAtanhReciprocal(57, wp); Float log7 = computeLogPrime(3, wp); return ldexp(Float::log2(wp), 1) + log7 + ldexp(a57, 1); } default: // fallback: AGM return log_core(Float(MP_PRIMES[index]), 64, wp); } } // Cached retrieval of log(p_i) static const Float& getLogPrime(int index, int precision) { auto& entry = logPrimeCacheAt(index); if (entry.precision > 0 && precision <= entry.precision) { return entry.value; } entry.value = computeLogPrime(index, precision); entry.value.setResultPrecision(precision); entry.precision = precision; return entry.value; } //------------------------------------------------------------------------- // LLL lattice basis reduction (low dimension, floating point) // Find coefficients c_i that approximate log(x) as an integer linear combination of log(p_i). // Lattice: b_i = (e_i, C·log(p_i)) ∈ R^{K+1}, i=0,...,K-1 // Target: t = (0,...,0, C·log(x)) // Find the nearest lattice point via Babai's nearest-plane method; components 0..K-1 = c_i //------------------------------------------------------------------------- static constexpr int MP_DIM = MP_NUM_PRIMES + 1; // 11 struct MultiPrimeLattice { int64_t basis[MP_NUM_PRIMES][MP_DIM]; double gs[MP_NUM_PRIMES][MP_DIM]; // Gram-Schmidt orthogonal basis double gs_norm2[MP_NUM_PRIMES]; // ||b*_i||² bool ready = false; void init() { if (ready) return; constexpr double SCALE = static_cast(1LL << 52); // Initial basis: b_i = (e_i, round(SCALE·log(p_i))) for (int i = 0; i < MP_NUM_PRIMES; i++) { for (int j = 0; j < MP_DIM; j++) basis[i][j] = 0; basis[i][i] = 1; basis[i][MP_NUM_PRIMES] = static_cast(std::llround( SCALE * std::log(static_cast(MP_PRIMES[i])))); } lll(); computeGS(); ready = true; } // Babai nearest-plane method: returns the coordinates of the lattice point nearest to log(x) void findCoeffs(double log_x, int64_t coeffs[MP_NUM_PRIMES]) const { constexpr double SCALE = static_cast(1LL << 52); double target[MP_DIM] = {}; target[MP_NUM_PRIMES] = SCALE * log_x; // Babai: orthogonal projection → rounding → subtraction double b[MP_DIM]; for (int j = 0; j < MP_DIM; j++) b[j] = target[j]; int64_t d[MP_NUM_PRIMES]; for (int i = MP_NUM_PRIMES - 1; i >= 0; i--) { double dot = 0; for (int j = 0; j < MP_DIM; j++) dot += b[j] * gs[i][j]; d[i] = std::llround(dot / gs_norm2[i]); for (int j = 0; j < MP_DIM; j++) b[j] -= d[i] * static_cast(basis[i][j]); } // Components 0..K-1 of the lattice point = Σ d_i · basis[i] are c_i for (int j = 0; j < MP_NUM_PRIMES; j++) { int64_t sum = 0; for (int i = 0; i < MP_NUM_PRIMES; i++) sum += d[i] * basis[i][j]; coeffs[j] = sum; } } private: void computeGS() { for (int i = 0; i < MP_NUM_PRIMES; i++) { for (int j = 0; j < MP_DIM; j++) gs[i][j] = static_cast(basis[i][j]); for (int k = 0; k < i; k++) { double dot = 0; for (int j = 0; j < MP_DIM; j++) dot += static_cast(basis[i][j]) * gs[k][j]; double mu = dot / gs_norm2[k]; for (int j = 0; j < MP_DIM; j++) gs[i][j] -= mu * gs[k][j]; } gs_norm2[i] = 0; for (int j = 0; j < MP_DIM; j++) gs_norm2[i] += gs[i][j] * gs[i][j]; } } void lll() { constexpr double DELTA = 0.75; double mu_mat[MP_NUM_PRIMES][MP_NUM_PRIMES] = {}; double Bstar[MP_NUM_PRIMES][MP_DIM]; double Bnorm[MP_NUM_PRIMES]; auto recomputeGS = [&]() { for (int i = 0; i < MP_NUM_PRIMES; i++) { for (int j = 0; j < MP_DIM; j++) Bstar[i][j] = static_cast(basis[i][j]); for (int k = 0; k < i; k++) { double dot = 0; for (int j = 0; j < MP_DIM; j++) dot += static_cast(basis[i][j]) * Bstar[k][j]; mu_mat[i][k] = dot / Bnorm[k]; for (int j = 0; j < MP_DIM; j++) Bstar[i][j] -= mu_mat[i][k] * Bstar[k][j]; } Bnorm[i] = 0; for (int j = 0; j < MP_DIM; j++) Bnorm[i] += Bstar[i][j] * Bstar[i][j]; } }; recomputeGS(); int k = 1; while (k < MP_NUM_PRIMES) { // Size reduction for (int j = k - 1; j >= 0; j--) { if (std::abs(mu_mat[k][j]) > 0.5) { int64_t r = std::llround(mu_mat[k][j]); for (int l = 0; l < MP_DIM; l++) basis[k][l] -= r * basis[j][l]; recomputeGS(); } } // Lovász condition double lhs = DELTA * Bnorm[k - 1]; double rhs = Bnorm[k] + mu_mat[k][k-1] * mu_mat[k][k-1] * Bnorm[k-1]; if (lhs > rhs) { for (int l = 0; l < MP_DIM; l++) std::swap(basis[k][l], basis[k - 1][l]); recomputeGS(); k = std::max(k - 1, 1); } else { k++; } } } }; static MultiPrimeLattice& getMultiPrimeLattice() { static MultiPrimeLattice lattice; if (!lattice.ready) lattice.init(); return lattice; } // log computation via multi-prime argument reduction // log(x) = Σ c_i·log(p_i) + log(r), r = x · Π p_i^{-c_i} ≈ 1 static Float log_multiprime(Float x, int eff_x, int precision) { auto& lattice = getMultiPrimeLattice(); int compute_prec = effectiveComputePrecision(eff_x, precision); int wp = compute_prec + 20; // 1. double approximation of log(x) (avoiding overflow) int64_t binary_exp = x.exponent() + static_cast(x.mantissa().bitLength()); double log_x; if (binary_exp > 500 || binary_exp < -500) { // Large exponent: log(mantissa) + exp·log(2) Float normalized = ldexp(x, static_cast(-binary_exp)); log_x = std::log(normalized.toDouble()) + static_cast(binary_exp) * std::log(2.0); } else { log_x = std::log(x.toDouble()); } // 2. Find coefficients c_i via LLL + Babai int64_t coeffs[MP_NUM_PRIMES]; lattice.findCoeffs(log_x, coeffs); // 3. Compute r = x · Π p_i^{-c_i} at high precision Float r(x); r.setResultPrecision(wp); for (int i = 0; i < MP_NUM_PRIMES; i++) { if (coeffs[i] == 0) continue; if (MP_PRIMES[i] == 2) { r = ldexp(std::move(r), static_cast(-coeffs[i])); } else { Int pk(1); for (int64_t j = 0; j < std::abs(coeffs[i]); j++) pk = pk * Int(MP_PRIMES[i]); Float pk_f(pk); pk_f.setResultPrecision(wp); if (coeffs[i] > 0) { r = r / pk_f; } else { r = r * pk_f; } } } // 4. Compute log(r) (r ≈ 1, |r-1| ≈ 2^{-50}) // Since r ≈ 1, Halley is efficient: // - exp(y) has a small argument, so expDoubling's K is automatically reduced // - Q256 initial approximation → converges in 2-3 Halley iterations Float log_r; if (r == Float::one()) { log_r = Float::zero(); } else { auto [logf, k_val, f_is_one] = logf_newton_core(std::move(r), wp); if (f_is_one) { if (k_val == 0) { log_r = Float::zero(); } else { log_r = Float::log2(wp) * k_val; } } else { log_r = std::move(logf); if (k_val != 0) { Float log2_val = Float::log2(wp); log_r = log_r + log2_val * k_val; } } log_r.setResultPrecision(wp); } // 5. log(x) = Σ c_i·log(p_i) + log(r) Float result = std::move(log_r); for (int i = 0; i < MP_NUM_PRIMES; i++) { if (coeffs[i] == 0) continue; const Float& log_p = getLogPrime(i, wp); Float contrib = log_p * coeffs[i]; contrib.setResultPrecision(wp); result = result + contrib; } finalizeResult(result, eff_x, precision); return result; } // log computation via the AGM method // log(x) = π / (2 · AGM(1, 4/s)) − m · log(2) // where s = x · 2^m > 2^{p/2} // Each AGM step is mul + sqrt = O(M(n)), over O(log n) steps. // Much lighter than Newton's method (each step has exp = O(M(n)·n^{1/3})). static Float log_core(Float x, int eff_x, int precision) { int compute_prec = effectiveComputePrecision(eff_x, precision); // Binary exponent of x: x ∈ [2^{ea-1}, 2^{ea}) int64_t ea = x.exponent() + static_cast(x.mantissa().bitLength()); // Estimate guard bits for cancellation // term1 = π/(2·AGM) ≈ log(x) + m·log(2), term2 = m·log(2) // cancel ≈ log2(|term1| / |log(x)|) double log_est = std::log(x.toDouble()); int target_bits = static_cast(std::ceil(compute_prec * 3.32192809488736)); int64_t m_est = (target_bits + 64) / 2 - ea; int cancel_bits = 0; if (std::abs(log_est) > 1e-300) { double term1_mag = std::abs(static_cast(m_est)) * 0.6931472 + std::abs(log_est); double ratio = term1_mag / std::abs(log_est); if (ratio > 1.0) { cancel_bits = static_cast(std::ceil(std::log2(ratio))); } } else { // x is extremely close to 1 → cancellation is huge. Add 200 guard bits. cancel_bits = 200; } // Guard bits: MPFR style + extra margin // Accounting for the second term 4/s² ≈ 4 ulp, rounding error ≈ 7 ulp, and uncertainty in the cancel estimate int q_bits = Float::precisionToBits(compute_prec); int log2_q = static_cast(std::ceil(std::log2(q_bits))); int p_bits = q_bits + 3 * log2_q + 20 + cancel_bits; int working_precision = Float::bitsToPrecision(p_bits); // m: s = x · 2^m, s > 2^{p/2} int64_t m = (p_bits + 3) / 2 - ea; // s = x · 2^m (exponent shift only, O(1)) Float s = ldexp(Float(x), static_cast(m)); s.setResultPrecision(working_precision); // 4/s Float four = Float::one(working_precision); four <<= 2; // Create 4 at working_precision Float four_over_s = four / s; four_over_s.setResultPrecision(working_precision); // AGM(1, 4/s) Float ag = agm(Float::one(working_precision), std::move(four_over_s), working_precision); // log(x) = π / (2 · AGM) − m · log(2) Float pi_val = Float::pi(working_precision); Float two_ag = ldexp(std::move(ag), 1); Float term1 = pi_val / two_ag; Float log2_val = Float::log2(working_precision); Float m_float = Float(m); m_float.setResultPrecision(working_precision); Float term2 = log2_val * m_float; Float result = term1 - term2; finalizeResult(result, eff_x, precision); return result; } // Q128 multiply: returns the high 128 bits of (a·b) >> 128 static void q128_mul(uint64_t a_hi, uint64_t a_lo, uint64_t b_hi, uint64_t b_lo, uint64_t& r_hi, uint64_t& r_lo) { uint64_t ll_hi, lh_hi, hl_hi, hh_hi; /*ll_lo*/ _umul128(a_lo, b_lo, &ll_hi); uint64_t lh_lo = _umul128(a_lo, b_hi, &lh_hi); uint64_t hl_lo = _umul128(a_hi, b_lo, &hl_hi); uint64_t hh_lo = _umul128(a_hi, b_hi, &hh_hi); // p1 = ll_hi + lh_lo + hl_lo (carry → p2) uint64_t p1 = ll_hi; unsigned char c1 = _addcarry_u64(0, p1, lh_lo, &p1); unsigned char c2 = _addcarry_u64(0, p1, hl_lo, &p1); uint64_t carry_p2 = static_cast(c1) + static_cast(c2); // p2 = hh_lo + lh_hi + hl_hi + carry_p2 uint64_t p2 = hh_lo; unsigned char c3 = _addcarry_u64(0, p2, lh_hi, &p2); unsigned char c4 = _addcarry_u64(0, p2, hl_hi, &p2); unsigned char c5 = _addcarry_u64(0, p2, carry_p2, &p2); uint64_t carry_p3 = static_cast(c3) + static_cast(c4) + static_cast(c5); r_hi = hh_hi + carry_p3; r_lo = p2; } // ========================================================================= // Q256 fixed-point arithmetic (256-bit, 4 × uint64_t, w[0]=LSW, w[3]=MSW) // ========================================================================= // Q256 multiply: returns the high 256 bits of (a·b) >> 256 // schoolbook 4×4 limb multiply, upper half static void q256_mul(const uint64_t a[4], const uint64_t b[4], uint64_t r[4]) { uint64_t w[8] = {}; for (int i = 0; i < 4; i++) { uint64_t carry = 0; for (int j = 0; j < 4; j++) { uint64_t hi; uint64_t lo = _umul128(a[i], b[j], &hi); unsigned char c1 = _addcarry_u64(0, w[i+j], lo, &w[i+j]); unsigned char c2 = _addcarry_u64(0, w[i+j], carry, &w[i+j]); carry = hi + static_cast(c1) + static_cast(c2); } if (i + 4 < 8) w[i + 4] += carry; } r[0] = w[4]; r[1] = w[5]; r[2] = w[6]; r[3] = w[7]; } // Q256 add: r = a + b, return carry static unsigned char q256_add(const uint64_t a[4], const uint64_t b[4], uint64_t r[4]) { unsigned char c = _addcarry_u64(0, a[0], b[0], &r[0]); c = _addcarry_u64(c, a[1], b[1], &r[1]); c = _addcarry_u64(c, a[2], b[2], &r[2]); c = _addcarry_u64(c, a[3], b[3], &r[3]); return c; } // Q256 subtract: r = a - b, return borrow static unsigned char q256_sub(const uint64_t a[4], const uint64_t b[4], uint64_t r[4]) { unsigned char c = _subborrow_u64(0, a[0], b[0], &r[0]); c = _subborrow_u64(c, a[1], b[1], &r[1]); c = _subborrow_u64(c, a[2], b[2], &r[2]); c = _subborrow_u64(c, a[3], b[3], &r[3]); return c; } // Q256 left shift by 1 bit static void q256_shl1(const uint64_t a[4], uint64_t r[4]) { r[0] = a[0] << 1; r[1] = (a[1] << 1) | (a[0] >> 63); r[2] = (a[2] << 1) | (a[1] >> 63); r[3] = (a[3] << 1) | (a[2] >> 63); } // Q256 scalar division: a / divisor (in-place) static void q256_div_scalar(uint64_t a[4], uint64_t divisor) { uint64_t rem = 0; a[3] = _udiv128(rem, a[3], divisor, &rem); a[2] = _udiv128(rem, a[2], divisor, &rem); a[1] = _udiv128(rem, a[1], divisor, &rem); a[0] = _udiv128(rem, a[0], divisor, &rem); } // Determine whether Q256 is zero static bool q256_is_zero(const uint64_t a[4]) { return a[0] == 0 && a[1] == 0 && a[2] == 0 && a[3] == 0; } // Q256 compare: a >= b static bool q256_ge(const uint64_t a[4], const uint64_t b[4]) { if (a[3] != b[3]) return a[3] > b[3]; if (a[2] != b[2]) return a[2] > b[2]; if (a[1] != b[1]) return a[1] > b[1]; return a[0] >= b[0]; } // Q256 → Float conversion static Float q256_to_float(const uint64_t q[4], int64_t exponent) { // Find the highest non-zero word int top = 3; while (top >= 0 && q[top] == 0) top--; if (top < 0) return Float::zero(); int nw = top + 1; auto val = Int::fromRawWordsPreNormalized( std::span(q, nw), 1); return Float(std::move(val), exponent, false); } // double → Q256 conversion [[maybe_unused]] static void double_to_q256(double val, uint64_t q[4]) { q[0] = q[1] = q[2] = q[3] = 0; if (val == 0.0) return; int exp; double m = std::frexp(val, &exp); uint64_t m_int = static_cast(m * static_cast(1ULL << 53)); // Q256: val * 2^256 = m_int * 2^(exp + 203) int bit_pos = exp + 203; int word_idx = bit_pos / 64; int bit_in_word = bit_pos % 64; if (word_idx >= 0 && word_idx < 4) { if (bit_in_word + 53 <= 64) { q[word_idx] = m_int << bit_in_word; } else { q[word_idx] = m_int << bit_in_word; if (word_idx + 1 < 4) q[word_idx + 1] = m_int >> (64 - bit_in_word); } } else if (word_idx == 4 && bit_in_word == 0) { // Overflow: val ≈ 1, shouldn't happen for atan args } } // Float → Q256 conversion (places the high 256 bits of the mantissa, x ∈ (0, 1)) static void float_to_q256(const Float& x, uint64_t q[4]) { const Int& mant = x.mantissa(); const uint64_t* mw = mant.data(); size_t mn = mant.size(); int64_t e = x.exponent(); uint64_t top = mw[mn - 1]; int top_bits = 64 - std::countl_zero(top); int64_t total_bl = static_cast((mn - 1) * 64) + top_bits; // Normalize the mantissa (place the MSB at bit 255) int clz = 64 - top_bits; uint64_t m[4] = {}; for (int i = 0; i < 4 && i < static_cast(mn); i++) { int src = static_cast(mn) - 1 - i; m[3 - i] = mw[src]; } // Left-shift by clz if (clz > 0) { for (int i = 3; i > 0; i--) m[i] = (m[i] << clz) | (m[i - 1] >> (64 - clz)); m[0] <<= clz; } // Q256 = x * 2^256 = m[3:0] * 2^(e + total_bl) int64_t sr = -(e + total_bl); if (sr <= 0) { q[0] = m[0]; q[1] = m[1]; q[2] = m[2]; q[3] = m[3]; } else if (sr < 64) { q[0] = (m[0] >> sr) | (m[1] << (64 - sr)); q[1] = (m[1] >> sr) | (m[2] << (64 - sr)); q[2] = (m[2] >> sr) | (m[3] << (64 - sr)); q[3] = m[3] >> sr; } else if (sr < 128) { int s = static_cast(sr - 64); q[0] = (m[1] >> s) | (m[2] << (64 - s)); q[1] = (m[2] >> s) | (m[3] << (64 - s)); q[2] = m[3] >> s; q[3] = 0; } else { q[0] = q[1] = q[2] = q[3] = 0; } } // Q256 division: compute t = u / (2^128 + u) in Q256 (Knuth Algorithm D, 3-word divisor) // u = (u_hi:u_lo), u_hi < 2^63 (MSB clear) // Result: returns t ∈ [0, 1/3) in Q256 static void q256_atanh_arg(const uint64_t u[4], uint64_t q[4]) { // t = u / (2^256 + u) = (f-1)/(f+1) in Q256 // u: 255 bits (MSB of mantissa cleared), u[3] < 2^63 // Divisor v = 2^256 + u (257 bits) = {u[0], u[1], u[2], u[3], 1} (LE 5 words) // Normalize: left-shift by 63 bits to set the MSB of d[4] uint64_t d[5]; d[4] = (1ULL << 63) | (u[3] >> 1); d[3] = (u[3] << 63) | (u[2] >> 1); d[2] = (u[2] << 63) | (u[1] >> 1); d[1] = (u[1] << 63) | (u[0] >> 1); d[0] = u[0] << 63; // Dividend: u × 2^256 × 2^63 (normalized) in 9 words uint64_t n[9] = {}; n[4] = u[0] << 63; n[5] = (u[1] << 63) | (u[0] >> 1); n[6] = (u[2] << 63) | (u[1] >> 1); n[7] = (u[3] << 63) | (u[2] >> 1); n[8] = u[3] >> 1; // Algorithm D: 9 word ÷ 5 word → 4-word quotient for (int i = 3; i >= 0; i--) { uint64_t q_hat, r_hat; bool skip_correct = false; // Knuth D3: when n[i+5] >= d[4], _udiv128 overflows if (n[i + 5] >= d[4]) { q_hat = UINT64_MAX; r_hat = n[i + 4] + d[4]; if (r_hat < d[4]) { skip_correct = true; } } else { q_hat = _udiv128(n[i + 5], n[i + 4], d[4], &r_hat); } // Knuth's 2-word correction if (!skip_correct) { for (;;) { uint64_t ph, pl; pl = _umul128(q_hat, d[3], &ph); if (ph < r_hat || (ph == r_hat && pl <= n[i + 3])) break; q_hat--; r_hat += d[4]; if (r_hat < d[4]) break; } } // Compute q_hat × d and subtract from n[i..i+5] uint64_t prod[6]; { uint64_t carry = 0; for (int j = 0; j < 5; j++) { uint64_t hi; uint64_t lo = _umul128(q_hat, d[j], &hi); unsigned char c = _addcarry_u64(0, lo, carry, &prod[j]); carry = hi + c; } prod[5] = carry; } unsigned char borrow = 0; for (int j = 0; j < 6; j++) { borrow = _subborrow_u64(borrow, n[i + j], prod[j], &n[i + j]); } if (borrow) { q_hat--; unsigned char c = 0; for (int j = 0; j < 5; j++) { c = _addcarry_u64(c, n[i + j], d[j], &n[i + j]); } n[i + 5] += c; } q[i] = q_hat; } } // Q128 → Float conversion helper static Float q128_to_float(uint64_t hi, uint64_t lo, int64_t exponent) { if (hi != 0) { uint64_t words[2] = { lo, hi }; auto val = Int::fromRawWordsPreNormalized( std::span(words, 2), 1); return Float(std::move(val), exponent, false); } else if (lo != 0) { return Float(Int(lo), exponent, false); } return Float::zero(); } // Compute log(f) via the Q128 fixed-point atanh series (1-word mantissa) // x = f·2^k, f ∈ [1, 2), log(f) = 2·atanh((f-1)/(f+1)) // Return values: logf_hi:logf_lo (Q128), k (binary exponent) // For f = 1.0, returns logf = 0 static void compute_logf_q128(const Float& x, uint64_t& logf_hi, uint64_t& logf_lo, int64_t& k) { const Int& mant = x.mantissa(); const uint64_t* mw = mant.data(); size_t mn = mant.size(); int64_t e = x.exponent(); // Get 64 bits from the most significant word of the mantissa (little-endian: MSW = mw[mn-1]) uint64_t top = mw[mn - 1]; int top_bits = 64 - std::countl_zero(top); int64_t total_bl = static_cast((mn - 1) * 64) + top_bits; k = e + total_bl - 1; // m_norm: normalize so the MSB lands at bit 63 int clz = 64 - top_bits; uint64_t m_norm; if (clz == 0) { m_norm = top; } else if (mn >= 2) { m_norm = (top << clz) | (mw[mn - 2] >> (64 - clz)); } else { m_norm = top << clz; } uint64_t u_int = m_norm & 0x7FFFFFFFFFFFFFFFULL; if (u_int == 0) { logf_hi = logf_lo = 0; return; } // v_half = floor(v_int / 2), v_int = m_norm + 2^63 (65 bit) uint64_t v_half = (m_norm >> 1) + (1ULL << 62); // t = u/v in Q128: t·2^128 ≈ u_int·2^127 / v_half uint64_t num_hi = u_int >> 1; uint64_t num_lo = u_int << 63; uint64_t rem1; uint64_t t_hi = _udiv128(num_hi, num_lo, v_half, &rem1); uint64_t rem2; uint64_t t_lo = _udiv128(rem1, 0, v_half, &rem2); // t² in Q128 uint64_t tsq_hi, tsq_lo; q128_mul(t_hi, t_lo, t_hi, t_lo, tsq_hi, tsq_lo); // Compute atanh(t) = t + t³/3 + t⁵/5 + ... in Q128 uint64_t sum_hi = t_hi, sum_lo = t_lo; uint64_t pow_hi = t_hi, pow_lo = t_lo; for (uint64_t d = 3; d < 200; d += 2) { q128_mul(pow_hi, pow_lo, tsq_hi, tsq_lo, pow_hi, pow_lo); uint64_t r; uint64_t term_hi = _udiv128(0, pow_hi, d, &r); uint64_t term_lo = _udiv128(r, pow_lo, d, &r); if (term_hi == 0 && term_lo == 0) break; unsigned char carry = _addcarry_u64(0, sum_lo, term_lo, &sum_lo); _addcarry_u64(carry, sum_hi, term_hi, &sum_hi); } // log(f) = 2·atanh(t) logf_hi = (sum_hi << 1) | (sum_lo >> 63); logf_lo = sum_lo << 1; } // Compute log(f) via the Q256 fixed-point atanh series (2-word mantissa) // x = f·2^k, f ∈ [1, 2), log(f) = 2·atanh((f-1)/(f+1)) // Return values: logf[4] (Q256), k (binary exponent) static void compute_logf_q256(const Float& x, uint64_t logf[4], int64_t& k) { const Int& mant = x.mantissa(); const uint64_t* mw = mant.data(); size_t mn = mant.size(); int64_t e = x.exponent(); uint64_t top = mw[mn - 1]; int top_bits = 64 - std::countl_zero(top); int64_t total_bl = static_cast((mn - 1) * 64) + top_bits; k = e + total_bl - 1; // Get the high 256 bits of the mantissa (MSB = bit 255) // m[3]:m[2]:m[1]:m[0] — MSB-aligned (left-shifted by clz) int clz = 64 - top_bits; uint64_t m[4] = {}; // Fill from the high words: left-shift mw[mn-1..mn-5] by clz bits and store into m[3..0] // m[k] = (mw[src+1] << clz) | (mw[src] >> (64-clz)), src = mn - 1 - (3-k) if (clz == 0) { for (int i = 0; i < 4 && i < static_cast(mn); i++) m[3 - i] = mw[mn - 1 - i]; } else { for (int k = 3; k >= 0; k--) { int hi_idx = static_cast(mn) - 1 - (3 - k); // mw index for high part int lo_idx = hi_idx - 1; // mw index for low part uint64_t hi_part = (hi_idx >= 0 && hi_idx < static_cast(mn)) ? mw[hi_idx] : 0; uint64_t lo_part = (lo_idx >= 0 && lo_idx < static_cast(mn)) ? mw[lo_idx] : 0; m[k] = (hi_part << clz) | (lo_part >> (64 - clz)); } } // u = m - 2^255 (MSB clear) uint64_t u[4] = { m[0], m[1], m[2], m[3] & 0x7FFFFFFFFFFFFFFFULL }; if (q256_is_zero(u)) { logf[0] = logf[1] = logf[2] = logf[3] = 0; return; } // t = u / (2^256 + u) = (f-1)/(f+1) in Q256 uint64_t t[4]; q256_atanh_arg(u, t); // t² in Q256 uint64_t tsq[4]; q256_mul(t, t, tsq); // atanh(t) = t + t³/3 + t⁵/5 + ... in Q256 uint64_t sum[4] = { t[0], t[1], t[2], t[3] }; uint64_t pow[4] = { t[0], t[1], t[2], t[3] }; for (uint64_t d = 3; d < 200; d += 2) { q256_mul(pow, tsq, pow); uint64_t term[4] = { pow[0], pow[1], pow[2], pow[3] }; q256_div_scalar(term, d); if (q256_is_zero(term)) break; q256_add(sum, term, sum); } // log(f) = 2 × atanh(t) q256_shl1(sum, logf); } // exp Q128 fast path (19 digits, precision_bits ≤ 64) // Argument reduction z = x - k·ln2 ∈ [0, ln2) → Q128 Taylor exp(z)-1 → (1+frac)·2^k static Float exp_small(const Float& x, int precision) { // Determine k = floor(x/ln2) in double double x_d = x.toDouble(); constexpr double inv_ln2_d = 1.4426950408889634; int k = static_cast(std::floor(x_d * inv_ln2_d)); // Q128 constant of ln(2) (ln2 = 0.6931471805599453... < 1, fits in Q128) constexpr uint64_t ln2_hi = 0xB17217F7D1CF79ABULL; constexpr uint64_t ln2_lo = 0xC9E3B39803F2F6AFULL; // Get the mantissa of x and compute z = x - k·ln2 exactly in Q128 // x = mantissa × 2^exponent, mantissa is a positive integer (max 64 bits) bool x_neg = x.isNegative(); const Int& mant = x.mantissa(); const uint64_t* mw = mant.data(); uint64_t m_val = mw[0]; // 1 word since precision ≤ 64 bits int64_t e = x.exponent(); int m_bits = 64 - std::countl_zero(m_val); // Absolute value of x = m_val × 2^e // x × 2^128 = m_val × 2^(e+128) // Q128 holds values in [0, 1). To handle the integer + fractional parts of x, // exploit that the integer parts of k·ln2 and x cancel, leaving z ∈ [0, ln2). // Compute k·ln2 as Q128 + integer part // k·ln2 = k × (0.B17217F7D1CF79AB_C9E3B39803F2F6AF × 2^0) // Integer part: floor(k·ln2) = floor(k × 0.6931...) // The low 128 bits of k·ln2 × 2^128 are the Q128 representation of the fractional part uint64_t abs_k = static_cast(k < 0 ? -k : k); uint64_t kln2_hi, kln2_lo; { uint64_t p0_hi; uint64_t p0_lo = _umul128(ln2_lo, abs_k, &p0_hi); uint64_t p1_hi; uint64_t p1_lo = _umul128(ln2_hi, abs_k, &p1_hi); unsigned char c = _addcarry_u64(0, p1_lo, p0_hi, &kln2_hi); (void)c; kln2_lo = p0_lo; // p1_hi is the integer part (integer part of k·ln2 × 2^0)... not needed } // Represent the fractional part of x in Q128 // x = m_val × 2^e → fractional part = frac(|x|) = frac(m_val × 2^e) // Integer part = floor(|x|) // The MSB is at position e+m_bits-1 = total_bits - 1 // Example: x = 3.5 → m_val=7, e=-1, m_bits=3 // |x| = 7×2^(-1) = 3.5, integer part=3, fractional part=0.5 int64_t total_bl = static_cast(m_bits) + e; // total_bl = the number of binary digits of x (2^(total_bl-1) ≤ |x| < 2^total_bl) // Place the fractional part into Q128: position m_val at 2^(128 - total_bl) and remove the integer-part bits uint64_t x_frac_hi = 0, x_frac_lo = 0; if (total_bl <= 0) { // |x| < 1 → no integer part, all fractional // m_val × 2^e × 2^128 = m_val × 2^(e+128) // Place the MSB of m_val at bit (128+e-1) int pos = static_cast(128 + e); // Position of the LSB of m_val (from bit 0) // Left-shift m_val by pos bits and place into Q128 if (pos >= 64) { x_frac_hi = m_val << (pos - 64); } else if (pos >= 0) { x_frac_hi = m_val >> (64 - pos); x_frac_lo = m_val << pos; } } else if (total_bl < 64) { // Has an integer part: mask off the integer-part bits and extract only the fractional part // The fractional part of m_val × 2^e = (m_val mod 2^(-e)) × 2^e (when e < 0) // = (m_val & ((1<<(-e))-1)) × 2^e if (e < 0) { uint64_t frac_mask = (1ULL << (-e)) - 1; uint64_t frac_val = m_val & frac_mask; if (frac_val != 0) { // frac_val × 2^e × 2^128 = frac_val × 2^(128+e) int pos = static_cast(128 + e); if (pos >= 64) { x_frac_hi = frac_val << (pos - 64); } else if (pos > 0) { x_frac_hi = frac_val >> (64 - pos); x_frac_lo = frac_val << pos; } } } // e >= 0 → no fractional part (x_frac = 0) } // z = frac(|x|) - frac(|k|·ln2) (difference mod 2^128) // The Q128 subtraction result correctly represents z mod 1 (the integer-part difference cancels via wraparound). // The fine adjustment of k is decided by a double approximation (the Q128 MSB/borrow misjudges 0.5 and integer arguments). uint64_t z_hi, z_lo; if (!x_neg) { unsigned char borrow = _subborrow_u64(0, x_frac_lo, kln2_lo, &z_lo); _subborrow_u64(borrow, x_frac_hi, kln2_hi, &z_hi); } else { unsigned char borrow = _subborrow_u64(0, kln2_lo, x_frac_lo, &z_lo); _subborrow_u64(borrow, kln2_hi, x_frac_hi, &z_hi); } // Fine adjustment of k: decided by the double approximation z (z < 0 → k--, z >= ln2 → k++) constexpr double ln2_d = 0.6931471805599453; double z_approx = std::fabs(x_d) - static_cast(abs_k) * ln2_d; if (x_neg) z_approx = static_cast(abs_k) * ln2_d - std::fabs(x_d); if (z_approx < 0) { // z < 0: decrement k by 1 and z += ln2 k--; unsigned char c = _addcarry_u64(0, z_lo, ln2_lo, &z_lo); _addcarry_u64(c, z_hi, ln2_hi, &z_hi); } else if (z_approx >= ln2_d) { // z >= ln2: increment k by 1 and z -= ln2 k++; unsigned char b = _subborrow_u64(0, z_lo, ln2_lo, &z_lo); _subborrow_u64(b, z_hi, ln2_hi, &z_hi); } if (z_hi == 0 && z_lo == 0) { Float result = Float::one(precision); if (k > 0) result <<= k; else if (k < 0) result >>= -k; result.setResultPrecision(precision); return result; } // Q128 Taylor: exp(z) - 1 = z + z²/2! + z³/3! + ... uint64_t sum_hi = z_hi, sum_lo = z_lo; uint64_t term_hi = z_hi, term_lo = z_lo; for (int n = 2; n <= 30; n++) { q128_mul(term_hi, term_lo, z_hi, z_lo, term_hi, term_lo); uint64_t rem; term_hi = _udiv128(0, term_hi, static_cast(n), &rem); term_lo = _udiv128(rem, term_lo, static_cast(n), &rem); if (term_hi == 0 && term_lo == 0) break; unsigned char c = _addcarry_u64(0, sum_lo, term_lo, &sum_lo); _addcarry_u64(c, sum_hi, term_hi, &sum_hi); } // exp(z) = 1 + sum → mantissa (2^128 + sum) × 2^{-128} uint64_t words[3] = { sum_lo, sum_hi, 1 }; auto mant2 = Int::fromRawWordsPreNormalized( std::span(words, 3), 1); Float result(std::move(mant2), -128, false); if (k > 0) result <<= k; else if (k < 0) result >>= -k; result.setResultPrecision(precision); return result; } // exp Q256 fast path (38 digits, precision_bits ≤ 128) // Argument reduction in Float → convert z to Q256 → Q256 Taylor → reconstruct as Float static Float exp_medium(const Float& x, int precision) { // Compute z = x - k·ln2 ∈ [0, ln2) exactly in Float double x_d = x.toDouble(); constexpr double inv_ln2_d = 1.4426950408889634; int k = static_cast(std::floor(x_d * inv_ln2_d)); Float ln2_val = Float::log2(precision + 10); Float z_float = x - k * ln2_val; if (z_float.isNegative()) { z_float = z_float + ln2_val; k--; } if (z_float.isZero()) { Float result = Float::one(precision); if (k > 0) result <<= k; else if (k < 0) result >>= -k; result.setResultPrecision(precision); return result; } // z ∈ (0, ln2) ⊂ (0, 1) → convert to Q256 uint64_t z[4]; float_to_q256(z_float, z); // Q256 Taylor: exp(z) - 1 = z + z²/2! + z³/3! + ... uint64_t sum[4] = { z[0], z[1], z[2], z[3] }; uint64_t term[4] = { z[0], z[1], z[2], z[3] }; for (int n = 2; n <= 60; n++) { q256_mul(term, z, term); q256_div_scalar(term, static_cast(n)); if (q256_is_zero(term)) break; q256_add(sum, term, sum); } // exp(z) = 1 + sum → mantissa (2^256 + sum) × 2^{-256} uint64_t words[5] = { sum[0], sum[1], sum[2], sum[3], 1 }; auto mant2 = Int::fromRawWordsPreNormalized( std::span(words, 5), 1); Float result(std::move(mant2), -256, false); if (k > 0) result <<= k; else if (k < 0) result >>= -k; result.setResultPrecision(precision); return result; } // log fast path (≤ 64-bit precision, Q128 atanh computation using the high 64 bits of the mantissa) static Float log_small(const Float& x, int precision) { uint64_t logf_hi, logf_lo; int64_t k; compute_logf_q128(x, logf_hi, logf_lo, k); if (logf_hi == 0 && logf_lo == 0) { if (k == 0) return Float::zero(); Float result = Float::log2(precision) * k; result.setResultPrecision(precision); return result; } Float result = q128_to_float(logf_hi, logf_lo, -128); if (k != 0) { Float k_log2 = Float::log2(precision) * k; k_log2.setResultPrecision(precision); result = result + k_log2; } result.setResultPrecision(precision); return result; } // log medium path (≤ 128-bit precision, Q256 atanh computation using the high 128 bits of the mantissa) static Float log_medium(const Float& x, int precision) { uint64_t logf[4]; int64_t k; compute_logf_q256(x, logf, k); if (q256_is_zero(logf)) { if (k == 0) return Float::zero(); Float result = Float::log2(precision) * k; result.setResultPrecision(precision); return result; } Float result = q256_to_float(logf, -256); if (k != 0) { Float k_log2 = Float::log2(precision) * k; k_log2.setResultPrecision(precision); result = result + k_log2; } result.setResultPrecision(precision); return result; } // log2 Q256 fast path: log2(x) = log(f)·log2(e) + k // log2(e) = 1 + frac → log2(f) = logf + logf·frac(log2(e)) static Float log2_medium(const Float& x, int precision) { uint64_t logf[4]; int64_t k; compute_logf_q256(x, logf, k); if (q256_is_zero(logf)) { return Float(Int(k), 0, k < 0); } // Fractional part of log2(e) = 0.44269504088896340735992468100189... // Q256: frac(log2(e)) × 2^256 constexpr uint64_t inv_ln2_frac[4] = { 0x164A2CD9A342648FULL, 0xD6AEF551BAD2B4B1ULL, 0x7D0FFDA0D23A7D11ULL, 0x71547652B82FE177ULL }; // log2(f) = logf + logf · frac(log2(e)) uint64_t prod[4]; q256_mul(logf, inv_ln2_frac, prod); uint64_t log2f[4]; q256_add(logf, prod, log2f); // result = log2(f) + k Float result = q256_to_float(log2f, -256); if (k != 0) { result = result + Float(Int(k), 0, k < 0); } result.setResultPrecision(precision); return result; } // log10 Q256 fast path: log10(x) = log(f)·log10(e) + k·log10(2) static Float log10_medium(const Float& x, int precision) { uint64_t logf[4]; int64_t k; compute_logf_q256(x, logf, k); // log10(e) = 1/ln(10) = 0.43429448190325182765112891891660508229... constexpr uint64_t log10e[4] = { 0x1D1F96A27BC7529EULL, 0x1F71A30122E4D101ULL, 0x9AADD557D699EE19ULL, 0x6F2DEC549B9438CAULL }; // log10(2) = 0.30102999566398119521373889472449302676... constexpr uint64_t log10_2[4] = { 0xC52F37935BE631E5ULL, 0x13569862A1E8F9A4ULL, 0x47C4ACD605BE48BCULL, 0x4D104D427DE7FBCCULL }; if (q256_is_zero(logf)) { if (k == 0) return Float::zero(); // Compute log10(2^k) = k·log10(2) in Q256 uint64_t abs_k = static_cast(k < 0 ? -k : k); // Q256 × uint64 → Q256 (no high overflow since k is small) uint64_t kl2[4]; uint64_t carry = 0; for (int i = 0; i < 4; i++) { uint64_t hi; uint64_t lo = _umul128(log10_2[i], abs_k, &hi); unsigned char c = _addcarry_u64(0, lo, carry, &kl2[i]); carry = hi + c; } Float result = q256_to_float(kl2, -256); if (k < 0) result = -result; result.setResultPrecision(precision); return result; } // log10(f) = logf · log10(e) (Q256 × Q256 → Q256) uint64_t log10f[4]; q256_mul(logf, log10e, log10f); // Compute k·log10(2) in Q256 and add if (k != 0) { uint64_t abs_k = static_cast(k < 0 ? -k : k); uint64_t kl2[4]; uint64_t carry = 0; for (int i = 0; i < 4; i++) { uint64_t hi; uint64_t lo = _umul128(log10_2[i], abs_k, &hi); unsigned char c = _addcarry_u64(0, lo, carry, &kl2[i]); carry = hi + c; } if (k > 0) { q256_add(log10f, kl2, log10f); } else { // log10(x) = log10(f) - |k|·log10(2) // log10(f) ∈ [0,1), |k|·log10(2) > 0 // Compare to determine the sign bool neg = false; for (int i = 3; i >= 0; i--) { if (log10f[i] > kl2[i]) break; if (log10f[i] < kl2[i]) { neg = true; break; } } if (neg) { uint64_t tmp[4]; q256_sub(kl2, log10f, tmp); Float result = q256_to_float(tmp, -256); result = -result; result.setResultPrecision(precision); return result; } else { q256_sub(log10f, kl2, log10f); } } } Float result = q256_to_float(log10f, -256); result.setResultPrecision(precision); return result; } // log2 fast path: log2(x) = log(f)/ln(2) + k = log(f)·log2(e) + k // log2(e) = 1 + 0.44269504... → log2(f) = logf + logf·frac(log2(e)) // log2(f) ∈ [0, 1), so it fits in Q128 static Float log2_small(const Float& x, int precision) { uint64_t logf_hi, logf_lo; int64_t k; compute_logf_q128(x, logf_hi, logf_lo, k); if (logf_hi == 0 && logf_lo == 0) { return Float(Int(k), 0, k < 0); } // Fractional part of log2(e) = 0.44269504088896340735992468100189... // Q128: the hi:lo words of 0.44269504... × 2^128 // Python: int(0.44269504088896340735992468100189213742... * 2**128) // = 150675655792891826045981552050498960470 // hi = 150675655792891826045981552050498960470 >> 64 // = 8168882295414032698 = 0x71547652B82FE177 // lo = 150675655792891826045981552050498960470 & (2^64-1) // = 8927088712929562294 = 0x7BF3F01ADBC79AB6 constexpr uint64_t inv_ln2_frac_hi = 0x71547652B82FE177ULL; constexpr uint64_t inv_ln2_frac_lo = 0x7D0FFDA0D23A7D11ULL; // log2(f) = logf + logf * frac(log2(e)) uint64_t prod_hi, prod_lo; q128_mul(logf_hi, logf_lo, inv_ln2_frac_hi, inv_ln2_frac_lo, prod_hi, prod_lo); // log2f = logf + prod uint64_t log2f_lo, log2f_hi; unsigned char carry = _addcarry_u64(0, logf_lo, prod_lo, &log2f_lo); _addcarry_u64(carry, logf_hi, prod_hi, &log2f_hi); // result = log2(f) + k Float result = q128_to_float(log2f_hi, log2f_lo, -128); if (k != 0) { result = result + Float(Int(k), 0, k < 0); } result.setResultPrecision(precision); return result; } // log10 fast path: log10(x) = log(f)·log10(e) + k·log10(2) // log10(2) is also kept in Q128, computing k·log10(2) without Float constant computation static Float log10_small(const Float& x, int precision) { uint64_t logf_hi, logf_lo; int64_t k; compute_logf_q128(x, logf_hi, logf_lo, k); // log10(e) = 1/ln(10) = 0.43429448190325182765112891891660508229... // Q128: int(0.43429448190325182765112891891660508229... * 2**128) constexpr uint64_t log10e_hi = 0x6F2DEC549B9438CAULL; constexpr uint64_t log10e_lo = 0x9AADD557D699EE19ULL; // log10(2) = ln(2)/ln(10) = 0.30102999566398119521373889472449302676... // Q128: int(0.30102999566398119521373889472449302676... * 2**128) constexpr uint64_t log10_2_hi = 0x4D104D427DE7FBCCULL; constexpr uint64_t log10_2_lo = 0x47C4ACD605BE48BCULL; if (logf_hi == 0 && logf_lo == 0) { if (k == 0) return Float::zero(); // log10(2^k) = k·log10(2); the integer part of k·log10(2) is at most about 19 // Compute k*log10_2 in Q128: simple multiplication since k is small uint64_t abs_k = static_cast(k < 0 ? -k : k); uint64_t kl2_hi, kl2_lo; // Q128 × uint64 → Q128 (high-word overflow ignored; safe since k ≤ 64) uint64_t mul_lo_hi; kl2_lo = _umul128(log10_2_lo, abs_k, &mul_lo_hi); uint64_t mul_hi_hi; uint64_t mul_hi_lo = _umul128(log10_2_hi, abs_k, &mul_hi_hi); unsigned char c = _addcarry_u64(0, mul_hi_lo, mul_lo_hi, &kl2_hi); (void)c; // The carry into mul_hi_hi is the integer part (ignorable) Float result = q128_to_float(kl2_hi, kl2_lo, -128); if (k < 0) result = -result; result.setResultPrecision(precision); return result; } // log10(f) = logf · log10(e) (Q128 × Q128 → Q128) uint64_t log10f_hi, log10f_lo; q128_mul(logf_hi, logf_lo, log10e_hi, log10e_lo, log10f_hi, log10f_lo); // Compute k·log10(2) in Q128 as well and add if (k != 0) { uint64_t abs_k = static_cast(k < 0 ? -k : k); uint64_t kl2_hi, kl2_lo; uint64_t mul_lo_hi; kl2_lo = _umul128(log10_2_lo, abs_k, &mul_lo_hi); uint64_t mul_hi_hi; uint64_t mul_hi_lo = _umul128(log10_2_hi, abs_k, &mul_hi_hi); unsigned char c = _addcarry_u64(0, mul_hi_lo, mul_lo_hi, &kl2_hi); (void)c; if (k > 0) { unsigned char carry = _addcarry_u64(0, log10f_lo, kl2_lo, &log10f_lo); _addcarry_u64(carry, log10f_hi, kl2_hi, &log10f_hi); } else { // log10f - kl2: log10(f) is positive, k < 0 → log10(2^k) is negative // log10(x) = log10(f) + k·log10(2) = log10(f) - |k|·log10(2) // When x < 1, the result may be negative if (log10f_hi > kl2_hi || (log10f_hi == kl2_hi && log10f_lo >= kl2_lo)) { unsigned char borrow = 0; borrow = _subborrow_u64(0, log10f_lo, kl2_lo, &log10f_lo); _subborrow_u64(borrow, log10f_hi, kl2_hi, &log10f_hi); } else { // The result is negative unsigned char borrow = 0; uint64_t neg_lo, neg_hi; borrow = _subborrow_u64(0, kl2_lo, log10f_lo, &neg_lo); _subborrow_u64(borrow, kl2_hi, log10f_hi, &neg_hi); Float result = q128_to_float(neg_hi, neg_lo, -128); result = -result; result.setResultPrecision(precision); return result; } } } Float result = q128_to_float(log10f_hi, log10f_lo, -128); result.setResultPrecision(precision); return result; } // Helper for log(f) computation via Newton iteration (f ∈ [1,2)) // Return values: log(f) (natural log), k (binary exponent x = f·2^k) // Initial approximation: std::log(double) (~53 bit) + Newton precision doubling // Step 1: exp_small (Q128 Taylor) — called with the mantissa still 1 word // Important: setResultPrecision left-shift-extends the mantissa, so // do not call setPrecision before the exp_small call. // From step 2 onward: expDoubling (RawFloat Taylor + squaring reconstruction) // Target: ≤ 2000 bits (~600 digits). Above 600 digits, AGM wins. // (LogfResult is forward-declared) LogfResult logf_newton_core(Float x, int compute_prec) { // x = f · 2^k, f ∈ [1, 2) int64_t ea = x.exponent() + static_cast(x.mantissa().bitLength()); int64_t k = ea - 1; // f = x · 2^(-k) Float f = ldexp(std::move(x), static_cast(-k)); // f = 1.0 exactly ⟺ mantissa = 1 (LSB=1 under odd normalization) if (f.mantissa().bitLength() == 1) { return { Float::zero(), k, true }; } int target_bits = Float::precisionToBits(compute_prec); int guard = 32; // Choice of initial approximation Float y; int correct_bits; if (target_bits <= 230) { // Within direct Q256 range: double initial approximation (completes in 1-2 iterations) double fd = f.toDouble(); y = Float(std::log(fd)); correct_bits = 50; } else { // High precision: ~240-bit initial approximation via Q256 atanh → fewer iterations // 240→720→2160 vs 50→150→450→1350→4050 // 250d (830b): 3 iters→2 iters, 500d (1660b): 4 iters→2 iters uint64_t logf_q256[4]; int64_t k_q256; compute_logf_q256(f, logf_q256, k_q256); if (q256_is_zero(logf_q256)) { // f-1 is below Q256 resolution (~2^-255). f==1 (mantissa bitLength==1) is // already handled above, so this is the case f≠1 but extremely close to 1. // BUGFIX (2026-05-31): previously returned log=0, which is the error log(1+u)→0 // (surfaced when the twin_prime constant's I(n)~2^n amplifies a tiny lnλ(n)~3^{-n}). // Seed with log(1+u)≈u (relative error ~u/2) and bring it to full precision via Halley iteration. Float u = f - Float::one(); if (u.isZero()) return { Float::zero(), k, true }; int64_t umsb = u.exponent() + static_cast(u.mantissa().bitLength()); y = u; correct_bits = std::max(1, static_cast(-umsb) - 4); } else { y = q256_to_float(logf_q256, -256); correct_bits = 230; } } // Halley precision-tripling loop: 50 → 150 → 450 → 1350 → ... // Halley: y_{n+1} = y_n + 2·(f - exp(y_n)) / (f + exp(y_n)) // Cubic convergence: the correct bit count triples each step (Newton doubles) while (correct_bits < target_bits) { int compute_bits = std::min(3 * correct_bits + guard, target_bits + guard); int step_prec = Float::bitsToPrecision(compute_bits); // Compute exp(y_n): no argument reduction needed since y ∈ [0, ln2) // exp_small: Q128 Taylor, only for 1-word mantissa & step_prec ≤ 19 // Note: setResultPrecision left-shift-extends the mantissa, making it // multiple words, so do not call it before the exp_small decision. // exp_medium: Q256 Taylor, step_prec ≤ 38 // expDoubling: RawFloat Taylor + squaring reconstruction (general) Float e_y; int step_bits = Float::precisionToBits(step_prec); if (y.mantissa().size() <= 1 && step_bits <= 120) { // Q128 Taylor fast path: 1-word input, ~128-bit output // Used for step_bits ≤ 120 (guard margin 8 bit) e_y = exp_small(y, step_prec); } else if (step_bits <= 240) { // Q256 Taylor: ~256-bit output y.setEffectiveBits(step_bits); y.setResultPrecision(step_prec); e_y = exp_medium(y, step_prec); } else { y.setEffectiveBits(step_bits); y.setResultPrecision(step_prec); e_y = expDoubling(y, step_prec); } // Halley correction: 2·(f - t) / (f + t) where t = exp(y) // Since f, t ∈ [1, 2), f+t > 0, numerically stable Float f_prec(f); f_prec.truncateToApprox(step_prec); Float num = f_prec - e_y; // f - t (approximately near 0) Float den = f_prec + e_y; // f + t (≈ 2f) Float correction = (num + num) / den; // 2·(f-t)/(f+t) y = y + correction; y.truncateToApprox(step_prec); correct_bits = std::min(3 * correct_bits, compute_bits); } return { std::move(y), k, false }; } // log(x) = log(f) + k · log(2) static Float log_newton(Float x, int eff_x, int precision) { int compute_prec = effectiveComputePrecision(eff_x, precision); auto [logf, k, f_is_one] = logf_newton_core(std::move(x), compute_prec); if (f_is_one) { if (k == 0) return Float::zero(); Float result = Float::log2(compute_prec) * k; finalizeResult(result, eff_x, precision); return result; } if (k != 0) { logf = logf + Float::log2(compute_prec) * k; } finalizeResult(logf, eff_x, precision); return logf; } // log2(x) = log(f) * log2(e) + k (replacing division with multiplication) static Float log2_newton(Float x, int eff_x, int precision) { int compute_prec = effectiveComputePrecision(eff_x, precision); auto [logf, k, f_is_one] = logf_newton_core(std::move(x), compute_prec); if (f_is_one) { if (k == 0) return Float::zero(); Float result(k); finalizeResult(result, eff_x, precision); return result; } // log2(e) = 1/log(2) — multiplication is about 2-3× faster than division Float log2e = Float::one(compute_prec) / Float::log2(compute_prec); Float result = logf * log2e; if (k != 0) { result = result + Float(k); } finalizeResult(result, eff_x, precision); return result; } // log10(x) = log(f) * log10(e) + k · log10(2) (replacing division with multiplication) static Float log10_newton(Float x, int eff_x, int precision) { int compute_prec = effectiveComputePrecision(eff_x, precision); auto [logf, k, f_is_one] = logf_newton_core(std::move(x), compute_prec); if (f_is_one) { if (k == 0) return Float::zero(); // log10(2^k) = k · log10(2) = k · log(2)/log(10) Float result = Float::log2(compute_prec) * k / Float::log10(compute_prec); finalizeResult(result, eff_x, precision); return result; } // log10(e) = 1/log(10) — multiplication is about 2-3× faster than division Float log10e = Float::one(compute_prec) / Float::log10(compute_prec); Float result = logf * log10e; if (k != 0) { // log10(2) = log(2)/log(10) = log(2) · log10(e) Float log10_2 = Float::log2(compute_prec) * log10e; result = result + log10_2 * k; } finalizeResult(result, eff_x, precision); return result; } // Internal log dispatch (selects the optimal algorithm based on precision) static Float log_dispatch(const Float& x, int eff_x, int precision) { int precision_bits = Float::precisionToBits(precision); if (precision_bits <= 64) { return log_newton(Float(x), eff_x, precision); } if (precision_bits <= 230) { return log_medium(x, precision); } if (precision_bits <= 3000) { return log_newton(Float(x), eff_x, precision); } if (precision_bits <= 6000) { return log_multiprime(Float(x), eff_x, precision); } return log_core(Float(x), eff_x, precision); } Float log(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity() && !x.isNegative()) return Float::positiveInfinity(); if (x.isZero()) return Float::negativeInfinity(); if (x.isNegative()) return Float::nan(); if (x == Float::one()) return Float::zero(); return zivRound(log_dispatch, x, x.effectiveBits(), precision); } Float log(Float&& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity() && !x.isNegative()) return Float::positiveInfinity(); if (x.isZero()) return Float::negativeInfinity(); if (x.isNegative()) return Float::nan(); if (x == Float::one()) return Float::zero(); return zivRound(log_dispatch, x, x.effectiveBits(), precision); } //============================================================================= // log_ui — natural logarithm of a positive integer //============================================================================= Float logUi(unsigned long long n, int precision) { if (n == 0) return Float::negativeInfinity(); if (n == 1) return Float::zero(precision); int precision_bits = Float::precisionToBits(precision); int wp = precision_bits + 10; // Trial-divide by small primes and decompose as log(n) = Σ e_i·log(p_i) + log(remainder) unsigned long long rem = n; int exponents[MP_NUM_PRIMES] = {}; for (int i = 0; i < MP_NUM_PRIMES && rem > 1; ++i) { unsigned long long p = static_cast(MP_PRIMES[i]); while (rem % p == 0) { exponents[i]++; rem /= p; } } // If fully decomposed: Σ e_i·log(p_i) // If there is a remainder: Σ e_i·log(p_i) + log(Float(rem)) bool has_prime_part = false; for (int i = 0; i < MP_NUM_PRIMES; ++i) { if (exponents[i] != 0) { has_prime_part = true; break; } } if (!has_prime_part) { // Could not decompose into small primes → log directly return log(Float(static_cast(n)), precision); } // Sum of the prime-factorization part Float result = Float::zero(wp); for (int i = 0; i < MP_NUM_PRIMES; ++i) { if (exponents[i] == 0) continue; const Float& lp = getLogPrime(i, wp); if (exponents[i] == 1) result = result + lp; else result = result + lp * exponents[i]; } // If there is a remainder if (rem > 1) { result = result + log(Float(static_cast(rem)), wp); } result.setResultPrecision(precision); return result; } //============================================================================= // Square root (sqrt) — correct rounding based on integer sqrtRem //============================================================================= static Float sqrt_isqrt(Float x, int precision) { // Approach: reduce sqrt(mantissa * 2^exp) to an integer square root. // Scale the mantissa to a 2*(p+1)-bit integer and // obtain the (p+1)-bit root via isqrt. // Determine the sticky bit by directly inspecting the dc_sqrtrem remainder (skips the M(n) squaring). int precision_bits = Float::precisionToBits(precision); int target_root_bits = precision_bits + 1; // Get the mantissa and exponent const Int& m = x.mantissa(); int64_t e = x.exponent(); size_t mn = m.size(); int bl = static_cast(m.bitLength()); // Compute the scaling amount int desired_bits = 2 * target_root_bits; int base_shift = desired_bits - bl; if (base_shift < 0) base_shift = 0; int64_t adjusted_exp = e - base_shift; if (adjusted_exp % 2 != 0) { base_shift += 1; adjusted_exp -= 1; } int64_t result_exponent = adjusted_exp / 2; // Scale + sqrtrem at the mpn level (bypasses IntSqrt::sqrtRem) ScratchScope scope; auto& arena = getThreadArena(); // Scaling: scaled = m << base_shift (mpn level) size_t word_shift = static_cast(base_shift) / 64; unsigned bit_shift = static_cast(base_shift) % 64; size_t an = mn + word_shift + (bit_shift > 0 ? 1 : 0); uint64_t* ap = arena.alloc_limbs(an + 1); std::memset(ap, 0, (an + 1) * sizeof(uint64_t)); const uint64_t* mw = m.data(); for (size_t i = 0; i < mn; i++) ap[i + word_shift] = mw[i]; if (bit_shift > 0) { ap[mn + word_shift] = mpn::lshift(ap + word_shift, ap + word_shift, mn, bit_shift); } // Normalized size while (an > 0 && ap[an - 1] == 0) an--; if (an == 0) { return Float::zero(); } // sqrtrem_check_exact: sqrt with zero-remainder detection (skips the M(n) squaring) size_t sn = (an + 1) / 2; uint64_t* sp = arena.alloc_limbs(sn); size_t scratch_sz = mpn::sqrtrem_scratch_size(an); uint64_t* scratch = arena.alloc_limbs(scratch_sz); auto [sp_n, exact] = mpn::sqrtrem_check_exact(sp, ap, an, scratch); // Sticky bit: if not exact, set the LSB to 1 if (!exact && sp_n > 0) { sp[0] |= 1; } // Construct the Int (SBO: no heap allocation for small sizes) Int s = (sp_n > 0) ? Int::fromRawWords(std::span(sp, sp_n), 1) : Int::Zero(); Float result(std::move(s), result_exponent, false); result.setResultPrecision(precision); return result; } //============================================================================= // Square root (sqrt) — Newton reciprocal square root (YC-7) // r_{n+1} = r_n * (3 - x * r_n²) / 2 (no division, multiplication only) // sqrt(x) = x * r_n //============================================================================= // Newton invsqrt vs isqrt benchmark results (2026-03-16, Zen3, AVX2 NTT): // 2M digits: isqrt 318ms, Newton 528ms (isqrt 40% faster) // 10M digits: isqrt 3208ms, Newton 4021ms (isqrt 20% faster) // → isqrt (integer sqrtRem) is best across all sizes. The Newton path is a deletion candidate. static Float sqrt_core(Float x, int /*eff_x*/, int precision) { return sqrt_isqrt(std::move(x), precision); } // sqrt ultra-fast path: 1-word mantissa + double sqrt + 1 Newton step // double sqrt (~53 bit) → _udiv128 Newton → _umul128 correction // Completely bypasses the Float abstraction layer static Float sqrt_newton_1(const Float& x, int precision) { const Int& mant = x.mantissa(); const uint64_t* mw = mant.data(); int64_t e = x.exponent(); int bl = static_cast(64 - std::countl_zero(mw[0])); // Scale to 128 bits (desired_bits=126: even with parity adjustment +1, 127 ≤ 128) int base_shift = 126 - bl; if (base_shift < 0) base_shift = 0; int64_t adjusted_exp = e - base_shift; if (adjusted_exp % 2 != 0) { base_shift += 1; adjusted_exp -= 1; } int64_t result_exponent = adjusted_exp / 2; // M = mw[0] << base_shift (128 bits: M_hi:M_lo) uint64_t m = mw[0]; uint64_t M_hi, M_lo; if (base_shift == 0) { M_hi = 0; M_lo = m; } else if (base_shift < 64) { M_hi = m >> (64 - base_shift); M_lo = m << base_shift; } else if (base_shift == 64) { M_hi = m; M_lo = 0; } else { M_hi = m << (base_shift - 64); M_lo = 0; } // Initial value: double sqrt (~53-bit precision) double M_dbl = static_cast(M_hi) * 0x1p64 + static_cast(M_lo); uint64_t s0 = static_cast(std::sqrt(M_dbl)); if (s0 == 0) s0 = 1; // Newton iteration: s1 = (s0 + M/s0) / 2 // _udiv128 requires M_hi < s0 (the condition that the quotient fits in 64 bits) while (M_hi >= s0) s0++; uint64_t rem; uint64_t q = _udiv128(M_hi, M_lo, s0, &rem); // Overflow-safe (s0 + q) / 2 uint64_t s1 = (s0 >> 1) + (q >> 1) + ((s0 & q) & 1); // Correction: guarantee s1² ≤ M < (s1+1)² (adjustment of at most ±1) uint64_t sq_hi, sq_lo; sq_lo = _umul128(s1, s1, &sq_hi); // If s1² > M, decrement s1 if (sq_hi > M_hi || (sq_hi == M_hi && sq_lo > M_lo)) { s1--; sq_lo = _umul128(s1, s1, &sq_hi); } // If (s1+1)² ≤ M, increment s1 uint64_t sq1_hi, sq1_lo; sq1_lo = _umul128(s1 + 1, s1 + 1, &sq1_hi); if (sq1_hi < M_hi || (sq1_hi == M_hi && sq1_lo <= M_lo)) { s1++; sq_hi = sq1_hi; sq_lo = sq1_lo; } // Remainder check (sticky bit) bool has_remainder = (sq_hi != M_hi || sq_lo != M_lo); if (has_remainder) { s1 |= 1; } Int s_int(s1); Float result(std::move(s_int), result_exponent, false); result.setResultPrecision(precision); return result; } // sqrt fast path: 1-2 word mantissa, dc_sqrtrem(n=2) avoids the Int overhead // precision_bits <= 128 (result is at most 2 words) static Float sqrt_small_2(const Float& x, int precision) { int precision_bits = Float::precisionToBits(precision); int target_root_bits = precision_bits + 1; const Int& mant = x.mantissa(); const uint64_t* mw = mant.data(); size_t mn = mant.size(); int64_t e = x.exponent(); int bl = static_cast((mn - 1) * 64 + (64 - std::countl_zero(mw[mn - 1]))); // Limit desired_bits to 255 or below so it fits in the 4-word (256-bit) buffer // Even with parity adjustment +1, stays within 256 bits int desired_bits = std::min(2 * target_root_bits, 255); int base_shift = desired_bits - bl; if (base_shift < 0) base_shift = 0; int64_t adjusted_exp = e - base_shift; if (adjusted_exp % 2 != 0) { base_shift += 1; adjusted_exp -= 1; } int64_t result_exponent = adjusted_exp / 2; // Scale into the 4-word buffer uint64_t scaled[6] = {0, 0, 0, 0, 0, 0}; // +2 because dc_sqrtrem uses np[n..n+2l-1] for (size_t i = 0; i < mn; i++) scaled[i] = mw[i]; if (base_shift > 0) { size_t word_shift = static_cast(base_shift) / 64; unsigned bit_shift = static_cast(base_shift) % 64; if (word_shift > 0) { for (int i = 3; i >= static_cast(word_shift); i--) scaled[i] = scaled[i - word_shift]; for (size_t i = 0; i < word_shift; i++) scaled[i] = 0; } if (bit_shift > 0) { for (int i = 3; i > 0; i--) scaled[i] = (scaled[i] << bit_shift) | (scaled[i - 1] >> (64 - bit_shift)); scaled[0] <<= bit_shift; } } // Normalization for dc_sqrtrem: scaled[3] >= 2^62 is required // If scaled[3] is 0, a word-level shift is also needed unsigned total_norm = 0; if (scaled[3] == 0) { // scaled[3] is 0 → the data is in scaled[2] unsigned clz2 = static_cast(std::countl_zero(scaled[2])); total_norm = 64 + (clz2 & ~1u); } else { unsigned clz = static_cast(std::countl_zero(scaled[3])); total_norm = clz & ~1u; } if (total_norm > 0) { size_t word_norm = total_norm / 64; unsigned bit_norm = total_norm % 64; if (word_norm > 0) { for (int i = 3; i >= static_cast(word_norm); i--) scaled[i] = scaled[i - word_norm]; for (size_t i = 0; i < word_norm; i++) scaled[i] = 0; } if (bit_norm > 0) { for (int i = 3; i > 0; i--) scaled[i] = (scaled[i] << bit_norm) | (scaled[i - 1] >> (64 - bit_norm)); scaled[0] <<= bit_norm; } } // dc_sqrtrem(sp, np, n=2, scratch) // sp[0..1] = floor(sqrt(scaled[0..3])), remainder stored in scaled[0..1] uint64_t sp[2] = {0, 0}; uint64_t scratch[16]; // dc_sqrtrem_scratch_size(2) ≈ 9 int carry = mpn::dc_sqrtrem(sp, scaled, 2, scratch); // Remainder check (against the normalized input) bool has_remainder = (carry != 0 || scaled[0] != 0 || scaled[1] != 0); // Denormalize: right-shift the root by total_norm/2 bits unsigned root_shift = total_norm / 2; if (root_shift > 0) { if (root_shift >= 64) { // Word crossing: sp[0] = sp[1] >> (root_shift - 64) sp[0] = sp[1] >> (root_shift - 64); sp[1] = 0; } else { sp[0] = (sp[0] >> root_shift) | (sp[1] << (64 - root_shift)); sp[1] >>= root_shift; } } if (has_remainder) { sp[0] |= 1; } // Construct the Int from 2 words (SBO: no heap allocation) size_t sp_n = (sp[1] != 0) ? 2 : 1; auto s_int = Int::fromRawWordsPreNormalized( std::span(sp, sp_n), 1); Float result(std::move(s_int), result_exponent, false); result.setResultPrecision(precision); return result; } //============================================================================= // Square (sqr) — dedicated squaring via IntOps::square //============================================================================= Float sqr(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) return Float::positiveInfinity(); if (x.isZero()) { Float z; z.effective_bits_ = x.effective_bits_; z.requested_bits_ = x.requested_bits_; return z; } int eff = x.effectiveBits(); // Operand truncation const Int* xp = &x.mantissa_; Int x_trunc; int64_t x_exp = x.exponent_; if (eff < INT_MAX) { int compute_bits = eff + 32; int bl = static_cast(xp->bitLength()); if (bl > compute_bits) { int words_to_drop = (bl - compute_bits) / 64; if (words_to_drop > 0) { int shift = words_to_drop * 64; x_trunc = *xp; x_trunc >>= shift; x_exp += shift; xp = &x_trunc; } } } // IntOps::squareUnchecked: NaN/Inf/Zero already rejected at entry → mantissa is Normal Int result_mantissa; IntOps::squareUnchecked(*xp, result_mantissa); int64_t result_exponent = x_exp * 2; Float result(std::move(result_mantissa), result_exponent, false); // x² is always positive result.effective_bits_ = eff; result.requested_bits_ = x.requested_bits_; finalizeResult(result, eff, precision); return result; } Float sqr(Float&& x, int precision) { return sqr(static_cast(x), precision); } Float sqrt(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isNegative()) return Float::nan(); if (x.isZero()) return Float::zero(); if (x.isInfinity()) return Float::positiveInfinity(); // Fast paths size_t mn = x.mantissa().size(); int precision_bits = Float::precisionToBits(precision); if (mn == 1 && precision_bits <= 64) { return sqrt_newton_1(x, precision); } if (mn <= 2 && precision_bits <= 128) { return sqrt_small_2(x, precision); } // isqrt is faster than Newton at all sizes → always use isqrt return sqrt_core(Float(x), x.effectiveBits(), precision); } Float sqrt(Float&& x, int precision) { // The rvalue version delegates to the const-ref version (no difference, as the Newton path copies internally) return sqrt(static_cast(x), precision); } //============================================================================= // Sine (sin) / cosine (cos) common code //============================================================================= static Float sinTaylor(const Float& x, int working_precision) { // sin(x) = x - x³/3! + x⁵/5! - ... // Computed directly with RawFloat (zero allocation inside the loop) int wp_bits = Float::precisionToBits(working_precision); int nw = (wp_bits + 63) / 64; ScratchScope scope; auto& arena = getThreadArena(); // Pre-allocate buffers size_t prod_alloc = 2 * static_cast(nw) + 4; uint64_t* prod_buf = arena.alloc_limbs(prod_alloc); size_t mul_scratch = mpn::multiply_scratch_size(nw + 1, nw + 1); size_t sqr_scratch = mpn::square_scratch_size(nw + 1); size_t scratch_sz = std::max(mul_scratch, sqr_scratch); uint64_t* scratch = arena.alloc_limbs(scratch_sz > 0 ? scratch_sz : 1); // Extract |x| RawFloat x_rf = rf_extract(x, nw, arena); bool x_negative = x.isNegative(); // Precompute x² (squaring is accelerated via square) uint64_t* x2_d = arena.alloc_limbs(nw + 2); RawFloat x2{x2_d, 0, 0}; rf_sqr(x2, x_rf, prod_buf, scratch, nw); uint64_t* result_d = arena.alloc_limbs(nw + 2); std::memset(result_d, 0, (nw + 2) * sizeof(uint64_t)); if (wp_bits >= 10000) { // Paterson-Stockmeyer: O(sqrt(l)) full-size multiplications RawFloat result{result_d, 0, 0}; rf_sin_ps(result, x_rf, x2, nw, prod_buf, scratch, arena, working_precision); return rf_to_float(result, x_negative, working_precision); } // Naive Taylor series uint64_t* term_d = arena.alloc_limbs(nw + 2); std::memset(term_d, 0, (nw + 2) * sizeof(uint64_t)); std::memcpy(term_d, x_rf.d, x_rf.nw * sizeof(uint64_t)); RawFloat term{term_d, x_rf.nw, x_rf.exp}; std::memcpy(result_d, x_rf.d, x_rf.nw * sizeof(uint64_t)); RawFloat result{result_d, x_rf.nw, x_rf.exp}; int max_terms = static_cast(working_precision * 1.2) + 10; for (int k = 1; k <= max_terms; ++k) { rf_mul(term, term, x2, prod_buf, scratch, nw); uint64_t divisor = static_cast(2*k) * static_cast(2*k + 1); rf_divmod_1(term, divisor); if (term.nw == 0) break; if (k & 1) { if (!rf_sub(result, term, nw + 1)) break; } else { if (!rf_add(result, term, nw + 1)) break; } } return rf_to_float(result, x_negative, working_precision); } // Internal computation of cos(x) via Taylor series (assumes |x| <= π/4) static Float cosTaylor(const Float& x, int working_precision) { // cos(x) = 1 - x²/2! + x⁴/4! - ... // Computed directly with RawFloat (zero allocation inside the loop) int wp_bits = Float::precisionToBits(working_precision); int nw = (wp_bits + 63) / 64; ScratchScope scope; auto& arena = getThreadArena(); // Pre-allocate buffers size_t prod_alloc = 2 * static_cast(nw) + 4; uint64_t* prod_buf = arena.alloc_limbs(prod_alloc); size_t mul_scratch = mpn::multiply_scratch_size(nw + 1, nw + 1); size_t sqr_scratch = mpn::square_scratch_size(nw + 1); size_t scratch_sz = std::max(mul_scratch, sqr_scratch); uint64_t* scratch = arena.alloc_limbs(scratch_sz > 0 ? scratch_sz : 1); // Extract |x| RawFloat x_rf = rf_extract(x, nw, arena); // Precompute x² (squaring is accelerated via square) uint64_t* x2_d = arena.alloc_limbs(nw + 2); RawFloat x2{x2_d, 0, 0}; rf_sqr(x2, x_rf, prod_buf, scratch, nw); uint64_t* result_d = arena.alloc_limbs(nw + 2); std::memset(result_d, 0, (nw + 2) * sizeof(uint64_t)); if (wp_bits >= 10000) { // Paterson-Stockmeyer: O(sqrt(l)) full-size multiplications RawFloat result{result_d, 0, 0}; rf_cos_ps(result, x2, nw, prod_buf, scratch, arena, working_precision); return rf_to_float(result, false, working_precision); } // Naive Taylor series uint64_t* term_d = arena.alloc_limbs(nw + 2); std::memset(term_d, 0, (nw + 2) * sizeof(uint64_t)); term_d[nw - 1] = uint64_t(1) << 63; RawFloat term{term_d, static_cast(nw), -static_cast(nw * 64 - 1)}; result_d[nw - 1] = uint64_t(1) << 63; RawFloat result{result_d, static_cast(nw), -static_cast(nw * 64 - 1)}; int max_terms = static_cast(working_precision * 1.2) + 10; for (int k = 1; k <= max_terms; ++k) { rf_mul(term, term, x2, prod_buf, scratch, nw); uint64_t divisor = static_cast(2*k - 1) * static_cast(2*k); rf_divmod_1(term, divisor); if (term.nw == 0) break; if (k & 1) { if (!rf_sub(result, term, nw + 1)) break; } else { if (!rf_add(result, term, nw + 1)) break; } } // cos(x) = cos(-x) ≥ 0 (after argument reduction |x| ≤ π/4, so cos > 0) return rf_to_float(result, false, working_precision); } // cos computation via the double-angle formula: halve K times → cosTaylor → reconstruct via double-angle K times // cos(2θ) = 2cos²(θ) - 1 // x is assumed to be in the range [0, π/2] static Float cosDoubling(const Float& x, int working_precision) { int wp_bits = Float::precisionToBits(working_precision); int K; if (wp_bits >= 10000) { // With P-S, Taylor is O(√N) → reduce K to lower the reconstruction cost // Optimal K: min(K + 3√(p/(2K))) → K = cbrt(wp_bits) K = static_cast(std::cbrt(static_cast(wp_bits))); } else { K = static_cast(std::sqrt(wp_bits / 2.0)); } if (K < 1) return cosTaylor(x, working_precision); // Guard bits: add 2K+α bits since the double-angle reconstruction amplifies error by up to 4^K int guard_bits = 2 * K + static_cast(std::ceil(std::log2(K + 1))) + 10; int wp_inner = working_precision + Float::bitsToPrecision(guard_bits); Float x_red = ldexp(x, -K); // x / 2^K — O(1) exponent operation x_red.truncateToApprox(wp_inner); // Even if the input is a double (eff=53), have Taylor computed at wp_inner precision. // Accurate because truncateToApprox has padded the mantissa to wp_inner bits. // Note: set wp_inner bits, not INT_MAX. With INT_MAX, division judges // "both exact" and falls back to defaultPrecision. int wp_inner_bits = Float::precisionToBits(wp_inner); x_red.setEffectiveBits(wp_inner_bits); Float c = cosTaylor(x_red, wp_inner); // K double-angle reconstructions (RawFloat — avoids per-op Float overhead) // cos(2θ) = 2cos²(θ) - 1 int nw = (wp_inner_bits + 63) / 64; ScratchScope scope; auto& arena = getThreadArena(); uint64_t* c_d = arena.alloc_limbs(nw + 2); RawFloat c_rf = rf_extract(c, nw, arena); // Copy c_rf's data into c_d (because rf_extract allocates in a separate buffer) std::memcpy(c_d, c_rf.d, c_rf.nw * sizeof(uint64_t)); c_rf.d = c_d; size_t prod_alloc = 2 * static_cast(nw) + 4; uint64_t* prod_buf = arena.alloc_limbs(prod_alloc); size_t sqr_scratch = mpn::square_scratch_size(nw + 1); size_t scratch_sz = std::max(mpn::multiply_scratch_size(nw + 1, nw + 1), sqr_scratch); uint64_t* scratch_buf = arena.alloc_limbs(scratch_sz > 0 ? scratch_sz : 1); bool c_negative = false; // K double-angle reconstructions: maintain full nw at every step // // The old implementation did gradual precision reduction (at step i, nw/2^(K-1-i) + guard), // but like expDoubling this rested on the mistaken assumption that // "squaring doubles precision". In reality rf_sqr truncates the output to needed_nw, // so squaring only maintains or reduces precision. Truncating to // cos_guard_limbs+2 limbs (~150 digits) at an early step caps the final accuracy // at ~150 digits regardless of prec. // // Correct schedule: maintain nw at every step (each double-angle reconstruction has // an error of about target_bits + 2K ULP, so the margin is already covered by // wp_inner, which includes guard_bits = 2K + log2(K) + 10). for (int i = 0; i < K; ++i) { // Truncate only when result exceeds nw (keep the top nw limbs) if (static_cast(c_rf.nw) > static_cast(nw)) { size_t drop = c_rf.nw - nw; std::memmove(c_rf.d, c_rf.d + drop, nw * sizeof(uint64_t)); c_rf.nw = nw; c_rf.exp += static_cast(drop) * 64; } // c = c² (truncated) — the sign vanishes (accelerated via squaring) rf_sqr(c_rf, c_rf, prod_buf, scratch_buf, nw); c_negative = false; // c *= 2 (exponent shift) c_rf.exp += 1; // c -= 1.0: subtract 2^(-exp) from the mantissa V // Bit position of 1.0 = bit (-exp). word_idx = (-exp)/64, bit_idx = (-exp)%64 // If word_idx >= nw: the bit of 1.0 is outside the mantissa range (above the MSB) // → 2c² < 1, so the result is 1 - 2c² (negative) if (c_rf.nw > 0 && c_rf.exp < 0) { int64_t neg_exp = -c_rf.exp; size_t word_idx = static_cast(neg_exp / 64); unsigned bit_idx = static_cast(neg_exp % 64); if (word_idx < c_rf.nw) { uint64_t borrow = mpn::sub_1( c_rf.d + word_idx, c_rf.nw - word_idx, 1ULL << bit_idx); if (borrow) { // 2c² < 1: obtain |1 - 2c²| via two's complement inversion for (size_t j = 0; j < c_rf.nw; ++j) c_rf.d[j] = ~c_rf.d[j]; mpn::add_1(c_rf.d, c_rf.nw, 1); c_negative = true; } } else { // word_idx >= nw: 1.0 > 2c² → c = 1 - 2c² (sign flip) // Build the mantissa of 1.0 and subtract: one - V // one = 2^(-exp) = 2^(word_idx*64 + bit_idx) // V = mantissa (nw words) // |1 - 2c²| = 1 - V*2^exp // This would need nw+1 or more words, but word_idx==nw and bit_idx==0 is typical // In that case 1.0 = 2^(nw*64) → just invert V if (word_idx == static_cast(nw) && bit_idx == 0) { // |1 - V*2^exp| = (2^(nw*64) - V) * 2^exp // = two's complement of V for (size_t j = 0; j < c_rf.nw; ++j) c_rf.d[j] = ~c_rf.d[j]; mpn::add_1(c_rf.d, c_rf.nw, 1); c_negative = true; // 2c² < 1 → 2c²-1 < 0 } else { // General case: fall back to Float Float c_tmp = rf_to_float(c_rf, false, wp_inner); c_tmp = c_tmp - Float::one(wp_inner); if (c_tmp.isNegative()) { c_tmp = -c_tmp; c_negative = true; } c_rf = rf_extract(c_tmp, nw, arena); std::memcpy(c_d, c_rf.d, c_rf.nw * sizeof(uint64_t)); c_rf.d = c_d; } } } c_rf.nw = mpn::normalized_size(c_rf.d, c_rf.nw); rf_normalize(c_rf); } Float out = rf_to_float(c_rf, c_negative, wp_inner); out.truncateToApprox(working_precision); return out; } // Simultaneous (sin, cos) computation via the double-angle formula // sin(2θ) = 2sinθcosθ, cos(2θ) = 2cos²θ - 1 // x is assumed to be in the range [0, π/2] static std::pair sinCosDoubling(const Float& x, int working_precision) { int wp_bits = Float::precisionToBits(working_precision); int K; if (wp_bits >= 10000) { // With P-S, Taylor is O(√N) → reconstruction 2K + Taylor 6√(p/(2K)) // Optimal K = cbrt(2·wp_bits) (since reconstruction is 2M(n)/step) K = static_cast(std::cbrt(2.0 * wp_bits)); } else { K = static_cast(std::sqrt(wp_bits / 2.0)); } if (K < 1) return { sinTaylor(x, working_precision), cosTaylor(x, working_precision) }; int guard_bits = 2 * K + static_cast(std::ceil(std::log2(K + 1))) + 10; int wp_inner = working_precision + Float::bitsToPrecision(guard_bits); Float x_red = ldexp(x, -K); x_red.truncateToApprox(wp_inner); // Even if the input is a double (eff=53), have Taylor computed at wp_inner precision. // Set wp_inner bits, not INT_MAX (avoids the division defaultPrecision problem). int wp_inner_bits = Float::precisionToBits(wp_inner); x_red.setEffectiveBits(wp_inner_bits); Float s_float = sinTaylor(x_red, wp_inner); Float c_float = cosTaylor(x_red, wp_inner); // K simultaneous reconstructions (RawFloat — avoids per-op Float overhead) // sin(2θ) = 2sinθcosθ, cos(2θ) = 2cos²θ - 1 int nw = (wp_inner_bits + 63) / 64; ScratchScope scope; auto& arena = getThreadArena(); uint64_t* s_d = arena.alloc_limbs(nw + 2); RawFloat s_rf = rf_extract(s_float, nw, arena); std::memcpy(s_d, s_rf.d, s_rf.nw * sizeof(uint64_t)); s_rf.d = s_d; uint64_t* c_d = arena.alloc_limbs(nw + 2); RawFloat c_rf = rf_extract(c_float, nw, arena); std::memcpy(c_d, c_rf.d, c_rf.nw * sizeof(uint64_t)); c_rf.d = c_d; size_t prod_alloc = 2 * static_cast(nw) + 4; uint64_t* prod_buf = arena.alloc_limbs(prod_alloc); size_t sqr_scratch = mpn::square_scratch_size(nw + 1); size_t scratch_sz = std::max(mpn::multiply_scratch_size(nw + 1, nw + 1), sqr_scratch); uint64_t* scratch_buf = arena.alloc_limbs(scratch_sz > 0 ? scratch_sz : 1); // Temporary buffer for new_s uint64_t* tmp_d = arena.alloc_limbs(nw + 2); bool c_negative = false; bool s_negative = false; // Maintain full nw at every step. // The old implementation did gradual precision reduction (at step i, nw>>remaining + guard limbs), // but this rested on the mistaken assumption that "the double-angle reconstruction // sin(2θ)=2sc, cos(2θ)=2c²−1 doubles precision" (same kind as expDoubling's // BUG_FLOAT_EXP_HIGH_PRECISION). In reality rf_mul/rf_sqr truncate the output to needed_nw, // so reconstruction only maintains or reduces precision, and truncating at an early step // capped tan(1, 200)'s final accuracy at ~152 digits. // guard_bits = 2K + log2(K) + 10 includes the K-fold error amplification, so full nw is correct. for (int i = 0; i < K; ++i) { int needed_nw = nw; // truncate s_rf and c_rf if (static_cast(s_rf.nw) > needed_nw) { size_t drop = s_rf.nw - needed_nw; std::memmove(s_rf.d, s_rf.d + drop, needed_nw * sizeof(uint64_t)); s_rf.nw = needed_nw; s_rf.exp += static_cast(drop) * 64; } if (static_cast(c_rf.nw) > needed_nw) { size_t drop = c_rf.nw - needed_nw; std::memmove(c_rf.d, c_rf.d + drop, needed_nw * sizeof(uint64_t)); c_rf.nw = needed_nw; c_rf.exp += static_cast(drop) * 64; } // new_s = 2 * s * c (stored in a temporary buffer) // Sign: s_neg XOR c_neg (the sign rule for multiplication) RawFloat tmp_rf{tmp_d, 0, 0}; rf_mul(tmp_rf, s_rf, c_rf, prod_buf, scratch_buf, needed_nw); tmp_rf.exp += 1; // ×2 bool new_s_neg = (s_negative != c_negative); // c = 2 * c² - 1 (the sign vanishes via c², accelerated via squaring) rf_sqr(c_rf, c_rf, prod_buf, scratch_buf, needed_nw); c_negative = false; c_rf.exp += 1; // ×2 // c -= 1.0: subtract 2^(-exp) from the mantissa if (c_rf.nw > 0 && c_rf.exp < 0) { int64_t neg_exp = -c_rf.exp; size_t word_idx = static_cast(neg_exp / 64); unsigned bit_idx = static_cast(neg_exp % 64); if (word_idx < c_rf.nw) { uint64_t borrow = mpn::sub_1( c_rf.d + word_idx, c_rf.nw - word_idx, 1ULL << bit_idx); if (borrow) { for (size_t j = 0; j < c_rf.nw; ++j) c_rf.d[j] = ~c_rf.d[j]; mpn::add_1(c_rf.d, c_rf.nw, 1); c_negative = true; } } else { // word_idx >= nw: 1.0 > 2c² → c = 1 - 2c² (sign flip) if (word_idx == static_cast(nw) && bit_idx == 0) { for (size_t j = 0; j < c_rf.nw; ++j) c_rf.d[j] = ~c_rf.d[j]; mpn::add_1(c_rf.d, c_rf.nw, 1); c_negative = true; } else { Float c_tmp = rf_to_float(c_rf, false, wp_inner); c_tmp = c_tmp - Float::one(wp_inner); if (c_tmp.isNegative()) { c_tmp = -c_tmp; c_negative = true; } c_rf = rf_extract(c_tmp, nw, arena); std::memcpy(c_d, c_rf.d, c_rf.nw * sizeof(uint64_t)); c_rf.d = c_d; } } } c_rf.nw = mpn::normalized_size(c_rf.d, c_rf.nw); rf_normalize(c_rf); // s = new_s std::memcpy(s_rf.d, tmp_rf.d, tmp_rf.nw * sizeof(uint64_t)); s_rf.nw = tmp_rf.nw; s_rf.exp = tmp_rf.exp; s_negative = new_s_neg; } Float s_out = rf_to_float(s_rf, s_negative, wp_inner); Float c_out = rf_to_float(c_rf, c_negative, wp_inner); s_out.truncateToApprox(working_precision); c_out.truncateToApprox(working_precision); return { std::move(s_out), std::move(c_out) }; } //============================================================================= // Bit-burst simultaneous sin/cos computation (Smith 2001) //============================================================================= // Split x into B-bit chunks and combine via the addition theorem sin(a+b) = sin(a)cos(b)+cos(a)sin(b). // Each chunk is tiny → Taylor converges in few terms. // Complexity: O(M(n) · log²n) (when combined with fixed-point BS; currently PS Taylor) // MPFR switches to this method above 30000 bits. // x ∈ [0, π/2] (already reduceToFirstQuadrant'd), working_precision is in decimal digits static std::pair sincos_bitburst(const Float& x, int working_precision) { int wp_bits = Float::precisionToBits(working_precision); // Halving count K: optimal value for PS Taylor int K; if (wp_bits >= 10000) { K = static_cast(std::cbrt(static_cast(wp_bits))); } else { K = static_cast(std::sqrt(wp_bits / 2.0)); } if (K < 1) K = 1; // Chunk count L: keeps the combine cost (4L·M(p)) down while lightening the Taylor of later chunks // A large L increases combine cost; a small L makes chunk 0 heavy (= equivalent to the normal path) // Empirically L=8-16 balances combine and Taylor int L = 12; if (wp_bits < 40000) L = 8; if (wp_bits > 200000) L = 16; // Guard bits: double-angle reconstruction 4^K amplification + rounding error of L combines int guard_bits = 2 * K + static_cast(std::ceil(std::log2(K + L + 1))) + 30; int wp_inner = working_precision + Float::bitsToPrecision(guard_bits); int wp_inner_bits = Float::precisionToBits(wp_inner); // Argument halving: x_red = x / 2^K Float x_red = ldexp(x, -K); x_red.truncateToApprox(wp_inner); x_red.setEffectiveBits(wp_inner_bits); if (x_red.isZero()) { return { Float::zero(), Float::one(working_precision) }; } // Split the mantissa into L chunks const Int& mant = x_red.mantissa(); int64_t base_exp = x_red.exponent(); int mant_bl = static_cast(mant.bitLength()); int B = (mant_bl + L - 1) / L; // Chunk size (bits) if (B < 1) B = 1; int actual_L = (mant_bl + B - 1) / B; // Mask: (1 << B) - 1 Int mask = (Int(1) << B) - 1; // Initialize the (sin, cos) accumulators as Float Float S = Float::zero(); Float C = Float::one(wp_inner); bool first_chunk = true; for (int j = 0; j < actual_L; ++j) { // Chunk j: extract bits [mant_bl - (j+1)*B, mant_bl - j*B) int bit_start = mant_bl - (j + 1) * B; Int chunk_val; if (bit_start >= 0) { chunk_val = (mant >> bit_start) & mask; } else { // Last fractional chunk int remaining_bits = mant_bl - j * B; Int last_mask = (Int(1) << remaining_bits) - 1; chunk_val = mant & last_mask; bit_start = 0; } if (chunk_val.isZero()) continue; // δ_j = chunk_val · 2^{base_exp + bit_start} int64_t chunk_exp = base_exp + bit_start; Float delta(std::move(chunk_val)); delta = ldexp(std::move(delta), static_cast(chunk_exp)); delta.truncateToApprox(wp_inner); delta.setEffectiveBits(wp_inner_bits); // Estimate the magnitude of δ_j: MSB position int64_t delta_msb = delta.exponent() + static_cast(delta.mantissa().bitLength()); Float sd, cd; if (2 * (-delta_msb) > wp_inner_bits) { // |δ_j²| < 2^{-wp_inner_bits} → sin(δ) = δ, cos(δ) = 1 (within precision) sd = std::move(delta); cd = Float::one(wp_inner); } else { // sinTaylor/cosTaylor: automatically use PS internally (≥10000bit) sd = sinTaylor(delta, wp_inner); cd = cosTaylor(delta, wp_inner); } if (first_chunk) { S = std::move(sd); C = std::move(cd); first_chunk = false; } else { // Addition theorem: sin(a+δ) = sin(a)cos(δ) + cos(a)sin(δ) // cos(a+δ) = cos(a)cos(δ) - sin(a)sin(δ) // The 3-argument FloatOps version reduces temporary Float construction in multiplication Float t1, t2; FloatOps::mul(S, cd, t1); // t1 = S*cos(δ) FloatOps::mul(C, sd, t2); // t2 = C*sin(δ) Float new_S = t1 + t2; FloatOps::mul(C, cd, t1); // t1 = C*cos(δ) FloatOps::mul(S, sd, t2); // t2 = S*sin(δ) S = std::move(new_S); C = t1 - t2; } S.truncateToApprox(wp_inner); C.truncateToApprox(wp_inner); } if (first_chunk) { // All chunks are 0 → x_red = 0 return { Float::zero(), Float::one(working_precision) }; } // K double-angle reconstructions (accelerated with RawFloat) // sin(2θ) = 2sinθcosθ, cos(2θ) = 2cos²θ - 1 int nw = (wp_inner_bits + 63) / 64; ScratchScope scope; auto& arena = getThreadArena(); uint64_t* s_d = arena.alloc_limbs(nw + 2); RawFloat s_rf = rf_extract(S, nw, arena); std::memcpy(s_d, s_rf.d, s_rf.nw * sizeof(uint64_t)); s_rf.d = s_d; uint64_t* c_d = arena.alloc_limbs(nw + 2); RawFloat c_rf = rf_extract(C, nw, arena); std::memcpy(c_d, c_rf.d, c_rf.nw * sizeof(uint64_t)); c_rf.d = c_d; size_t prod_alloc = 2 * static_cast(nw) + 4; uint64_t* prod_buf = arena.alloc_limbs(prod_alloc); size_t sqr_scratch = mpn::square_scratch_size(nw + 1); size_t scratch_sz = std::max(mpn::multiply_scratch_size(nw + 1, nw + 1), sqr_scratch); uint64_t* scratch_buf = arena.alloc_limbs(scratch_sz > 0 ? scratch_sz : 1); uint64_t* tmp_d = arena.alloc_limbs(nw + 2); bool c_negative = false; bool s_negative = false; // Gradual precision reduction (P2-3 technique) int bb_guard_limbs = (K + 63) / 64 + 6; for (int i = 0; i < K; ++i) { int remaining = K - 1 - i; int needed_nw = nw; if (remaining > 0 && remaining < 20) { needed_nw = (nw >> remaining) + bb_guard_limbs; if (needed_nw < bb_guard_limbs + 2) needed_nw = bb_guard_limbs + 2; if (needed_nw > nw) needed_nw = nw; } else if (remaining >= 20) { needed_nw = bb_guard_limbs + 2; } // truncate s_rf and c_rf if (static_cast(s_rf.nw) > needed_nw) { size_t drop = s_rf.nw - needed_nw; std::memmove(s_rf.d, s_rf.d + drop, needed_nw * sizeof(uint64_t)); s_rf.nw = needed_nw; s_rf.exp += static_cast(drop) * 64; } if (static_cast(c_rf.nw) > needed_nw) { size_t drop = c_rf.nw - needed_nw; std::memmove(c_rf.d, c_rf.d + drop, needed_nw * sizeof(uint64_t)); c_rf.nw = needed_nw; c_rf.exp += static_cast(drop) * 64; } // new_s = 2 * s * c RawFloat tmp_rf{tmp_d, 0, 0}; rf_mul(tmp_rf, s_rf, c_rf, prod_buf, scratch_buf, needed_nw); tmp_rf.exp += 1; // ×2 bool new_s_neg = (s_negative != c_negative); // c = 2 * c² - 1 rf_sqr(c_rf, c_rf, prod_buf, scratch_buf, needed_nw); c_negative = false; c_rf.exp += 1; // ×2 // c -= 1.0 if (c_rf.nw > 0 && c_rf.exp < 0) { int64_t neg_exp = -c_rf.exp; size_t word_idx = static_cast(neg_exp / 64); unsigned bit_idx = static_cast(neg_exp % 64); if (word_idx < c_rf.nw) { uint64_t borrow = mpn::sub_1( c_rf.d + word_idx, c_rf.nw - word_idx, 1ULL << bit_idx); if (borrow) { for (size_t jj = 0; jj < c_rf.nw; ++jj) c_rf.d[jj] = ~c_rf.d[jj]; mpn::add_1(c_rf.d, c_rf.nw, 1); c_negative = true; } } else { if (word_idx == static_cast(nw) && bit_idx == 0) { for (size_t jj = 0; jj < c_rf.nw; ++jj) c_rf.d[jj] = ~c_rf.d[jj]; mpn::add_1(c_rf.d, c_rf.nw, 1); c_negative = true; } else { Float c_tmp = rf_to_float(c_rf, false, wp_inner); c_tmp = c_tmp - Float::one(wp_inner); if (c_tmp.isNegative()) { c_tmp = -c_tmp; c_negative = true; } c_rf = rf_extract(c_tmp, nw, arena); std::memcpy(c_d, c_rf.d, c_rf.nw * sizeof(uint64_t)); c_rf.d = c_d; } } } c_rf.nw = mpn::normalized_size(c_rf.d, c_rf.nw); rf_normalize(c_rf); // s = new_s std::memcpy(s_rf.d, tmp_rf.d, tmp_rf.nw * sizeof(uint64_t)); s_rf.nw = tmp_rf.nw; s_rf.exp = tmp_rf.exp; s_negative = new_s_neg; } Float s_out = rf_to_float(s_rf, s_negative, wp_inner); Float c_out = rf_to_float(c_rf, c_negative, wp_inner); s_out.truncateToApprox(working_precision); c_out.truncateToApprox(working_precision); return { std::move(s_out), std::move(c_out) }; } //============================================================================= // Trigonometric common range reduction //============================================================================= // Reduce x > 0 to [0, π/2] and return the sin/cos sign flags struct TrigReduced { Float x; // Value reduced to [0, π/2] bool sin_negative; // sin sign-flip flag bool cos_negative; // cos sign-flip flag }; static TrigReduced reduceToFirstQuadrant(Float x, int working_precision) { x.truncateToApprox(working_precision); // Dynamically extend π according to the magnitude of |x| (MPFR-style argument reduction). // x mod 2π is essentially x - k·2π, and the larger x is, the more of the leading log₂|x| bits // are lost to catastrophic cancellation. To prevent this, k·2π must be representable exactly // at the same bit length as x, so extend π's precision to // pi_bits = max(0, msb_bits(x)) + bits(working_precision) + GUARD // This keeps x mod 2π at working_precision bits. // If |x| < 2π then msb_bits(x) ≤ 3 and the contribution is negligible (the extension does not run). int reduce_precision = working_precision; if (!x.isZero()) { int64_t x_msb_bits = x.exponent() + static_cast(x.mantissa().bitLength()); if (x_msb_bits > 0) { int wp_bits = Float::precisionToBits(working_precision); int64_t pi_bits = x_msb_bits + static_cast(wp_bits) + 16; // Clip to the INT_MAX upper bound (a safeguard if |x| is an unrealistically huge value) if (pi_bits > static_cast(INT_MAX) - 64) { pi_bits = static_cast(INT_MAX) - 64; } int extended = Float::bitsToPrecision(static_cast(pi_bits)); if (extended > reduce_precision) reduce_precision = extended; } } Float pi = Float::pi(reduce_precision); Float pi2 = ldexp(pi, 1); // x mod 2π — extending π prevents precision loss and keeps working_precision if (x > pi2) { Int k = (x / pi2).toInt(); x = x - Float(k) * pi2; } // Reduce to [0, π/2] — four-quadrant sign table: // Q1 [0, π/2] : sin+, cos+ // Q2 (π/2, π] : sin+, cos- // Q3 (π, 3π/2] : sin-, cos- // Q4 (3π/2, 2π) : sin-, cos+ bool sin_neg = false; bool cos_neg = false; Float pi_half = ldexp(pi, -1); if (x > pi_half && x <= pi) { x = pi - x; cos_neg = true; } else if (x > pi && x <= pi + pi_half) { x = x - pi; sin_neg = true; cos_neg = true; } else if (x > pi + pi_half) { x = pi2 - x; sin_neg = true; } x.truncateToApprox(working_precision); return { std::move(x), sin_neg, cos_neg }; } //============================================================================= // Sine (sin) //============================================================================= // Body: takes x by value (assumes already moved, x > 0) // TRIG-2: cosDoubling + sqrt(1 - cos²) saves one Taylor pass // Bit-burst switchover threshold // The current implementation uses the existing PS Taylor + Float combination, which is slower than cosDoubling+sqrt. // As it stands, bit-burst uses 2 Float-level Taylor passes + K doublings, // which is slower than the cosDoubling + sqrt(1-cos²) path. // It will be enabled after an integer-level BS is implemented in the future. // Until then, set the threshold to infinity and use the cosDoubling + sqrt path. static constexpr int BITBURST_THRESHOLD = INT_MAX; static Float sin_core(Float x, int eff_x, int precision) { int compute_prec = effectiveComputePrecision(eff_x, precision); // (a) Properly guard the Taylor series roundoff accumulation with log10(N_terms). // (b) Inside argument reduction, extend π according to the magnitude of |x| (see reduceToFirstQuadrant). int working_precision = taylorWorkingPrecision(compute_prec); auto reduced = reduceToFirstQuadrant(std::move(x), working_precision); Float result; if (reduced.x.isZero()) { result = Float::zero(); } else { int64_t x_msb = reduced.x.exponent() + static_cast(reduced.x.mantissa().bitLength()); int wp_bits = Float::precisionToBits(working_precision); if (x_msb < -(wp_bits / 4) || wp_bits < 1000) { result = sinTaylor(reduced.x, working_precision); } else if (wp_bits >= BITBURST_THRESHOLD) { // Bit-burst: simultaneous sin/cos computation, no sqrt needed auto [s, c] = sincos_bitburst(reduced.x, working_precision); result = std::move(s); } else { // High-precision path: cos + sqrt(1 - cos²) int extra_guard = (x_msb < 0) ? static_cast(-2 * x_msb) : 0; int wp_cos = working_precision + Float::bitsToPrecision(extra_guard); Float c = cosDoubling(reduced.x, wp_cos); Float one_minus_c2 = Float::one(wp_cos) - c * c; result = sqrt(std::move(one_minus_c2), working_precision); } } if (reduced.sin_negative) result = -result; finalizeResult(result, eff_x, precision); return result; } // sin/cos Q128 fast path (19 digits, precision_bits ≤ 64) // Argument reduction z = x - k·(π/2) ∈ [-π/4, π/4] → Q128 Taylor sin/cos // Assumes x > 0 (guaranteed by the sin caller; cos handles it via fabs) static Float trig_small_impl(const Float& x, bool want_sin, int precision) { const Int& mant = x.mantissa(); const uint64_t* mw = mant.data(); uint64_t m_val = mw[0]; int64_t e = x.exponent(); int m_bits = 64 - std::countl_zero(m_val); int64_t total_bl = static_cast(m_bits) + e; // x is tiny (< 2^{-64}) → sin(x) ≈ x, cos(x) ≈ 1 if (total_bl < -64) { if (want_sin) { Float r(x); r.setResultPrecision(precision); return r; } return Float::one(precision); } // k = round(x / (π/2)) double x_d = x.toDouble(); if (!want_sin) x_d = std::fabs(x_d); // cos(-x) = cos(x) constexpr double two_over_pi = 0.6366197723675814; int k = static_cast(std::round(x_d * two_over_pi)); // Quadrant mapping // sin(k·π/2 + z): k%4 = 0→sin(z), 1→cos(z), 2→-sin(z), 3→-cos(z) // cos(k·π/2 + z): k%4 = 0→cos(z), 1→-sin(z), 2→-cos(z), 3→sin(z) int quad = ((k % 4) + 4) % 4; bool use_cos_taylor, negate; if (want_sin) { use_cos_taylor = (quad == 1 || quad == 3); negate = (quad == 2 || quad == 3); } else { use_cos_taylor = (quad == 0 || quad == 2); negate = (quad == 1 || quad == 2); } // Q128 constant of (π/2 - 1) // π/2 = 1.921FB54442D18469_898CC51701B839A2...₁₆ constexpr uint64_t phf_hi = 0x921FB54442D18469ULL; constexpr uint64_t phf_lo = 0x898CC51701B839A2ULL; uint64_t abs_k = static_cast(k < 0 ? -k : k); // k·(π/2-1) → integer part + Q128 fractional part uint64_t kphf_frac_hi, kphf_frac_lo; uint64_t kphf_int; { uint64_t p0_hi; uint64_t p0_lo = _umul128(phf_lo, abs_k, &p0_hi); uint64_t p1_hi; uint64_t p1_lo = _umul128(phf_hi, abs_k, &p1_hi); unsigned char c = _addcarry_u64(0, p1_lo, p0_hi, &kphf_frac_hi); kphf_frac_lo = p0_lo; kphf_int = p1_hi + static_cast(c); } // Extract the integer part and Q128 fractional part of x (the mantissa always represents |x|) uint64_t int_x = 0; uint64_t x_frac_hi = 0, x_frac_lo = 0; if (total_bl <= 0) { // |x| < 1: no integer part int pos = static_cast(128 + e); if (pos >= 64) { x_frac_hi = m_val << (pos - 64); } else if (pos > 0) { x_frac_hi = m_val >> (64 - pos); x_frac_lo = m_val << pos; } else if (pos == 0) { x_frac_lo = m_val; } } else { if (e < 0) { int_x = m_val >> (-e); uint64_t frac_mask = (1ULL << (-e)) - 1; uint64_t frac_val = m_val & frac_mask; if (frac_val != 0) { int pos = static_cast(128 + e); if (pos >= 64) { x_frac_hi = frac_val << (pos - 64); } else if (pos > 0) { x_frac_hi = frac_val >> (64 - pos); x_frac_lo = frac_val << pos; } } } else { // e >= 0: x is an integer, no fractional part int_x = m_val << e; } } // Compute frac(x) - frac(k·(π/2-1)) in unsigned Q128 uint64_t fd_hi, fd_lo; unsigned char borrow1 = _subborrow_u64(0, x_frac_lo, kphf_frac_lo, &fd_lo); unsigned char borrow2 = _subborrow_u64(borrow1, x_frac_hi, kphf_frac_hi, &fd_hi); // Integer correction: z = (int_x - k - kphf_int - borrow) + fd/2^128 // Since k·(π/2) = k + k·(π/2-1), int(k·(π/2)) = k + kphf_int int64_t effective_corr = static_cast(int_x) - static_cast(k) - static_cast(kphf_int) - static_cast(borrow2); // z = effective_corr + fd/2^128, |z| < π/4 < 1 // effective_corr is only 0 (z ≥ 0) or -1 (z < 0) uint64_t z_hi, z_lo; bool z_neg; if (effective_corr == 0) { z_hi = fd_hi; z_lo = fd_lo; z_neg = false; } else if (effective_corr == -1) { // z < 0: |z| = (2^128 - fd) / 2^128 unsigned char nb = _subborrow_u64(0, 0, fd_lo, &z_lo); _subborrow_u64(nb, 0, fd_hi, &z_hi); z_neg = true; } else { // Unexpected → fall back to the general path Float x_copy(x); if (!want_sin) x_copy = abs(x_copy); if (want_sin) return sin_core(std::move(x_copy), x.effectiveBits(), precision); else return cos_core(std::move(x_copy), x.effectiveBits(), precision); } // sin(z): sin(-z) = -sin(z) → sign flip // cos(z): cos(-z) = cos(z) → unchanged if (!use_cos_taylor && z_neg) negate = !negate; if (z_hi == 0 && z_lo == 0) { if (use_cos_taylor) { Float result = Float::one(precision); return negate ? -result : result; } return Float::zero(precision); } // Compute z² in Q128 uint64_t z2_hi, z2_lo; q128_mul(z_hi, z_lo, z_hi, z_lo, z2_hi, z2_lo); Float result; if (use_cos_taylor) { // 1 - cos(|z|) = z²/2! - z⁴/4! + z⁶/6! - ... // term₀ = z²/2, recurrence: term_{n+1} = term_n · z² / ((2n+1)(2n+2)) uint64_t rem; uint64_t term_hi = _udiv128(0, z2_hi, 2, &rem); uint64_t term_lo = _udiv128(rem, z2_lo, 2, &rem); uint64_t sum_hi = term_hi, sum_lo = term_lo; for (int n = 1; n <= 20; n++) { q128_mul(term_hi, term_lo, z2_hi, z2_lo, term_hi, term_lo); uint64_t divisor = static_cast(2*n + 1) * static_cast(2*n + 2); term_hi = _udiv128(0, term_hi, divisor, &rem); term_lo = _udiv128(rem, term_lo, divisor, &rem); if (term_hi == 0 && term_lo == 0) break; if (n & 1) { unsigned char b = _subborrow_u64(0, sum_lo, term_lo, &sum_lo); _subborrow_u64(b, sum_hi, term_hi, &sum_hi); } else { unsigned char c = _addcarry_u64(0, sum_lo, term_lo, &sum_lo); _addcarry_u64(c, sum_hi, term_hi, &sum_hi); } } // cos(z) = 1 - sum → mantissa = 2^128 - sum, exponent = -128 // |z| < π/4 → cos(z) > cos(π/4) ≈ 0.707 → mantissa > 2^127 → always 2 words uint64_t cos_lo, cos_hi; unsigned char b = _subborrow_u64(0, 0, sum_lo, &cos_lo); _subborrow_u64(b, 0, sum_hi, &cos_hi); uint64_t words[2] = { cos_lo, cos_hi }; auto mant2 = Int::fromRawWordsPreNormalized( std::span(words, 2), 1); result = Float(std::move(mant2), -128, negate); } else { // sin(|z|) = z - z³/3! + z⁵/5! - ... // term₀ = z, recurrence: term_{n+1} = term_n · z² / ((2n)(2n+1)) uint64_t sum_hi = z_hi, sum_lo = z_lo; uint64_t term_hi = z_hi, term_lo = z_lo; for (int n = 1; n <= 20; n++) { q128_mul(term_hi, term_lo, z2_hi, z2_lo, term_hi, term_lo); uint64_t divisor = static_cast(2*n) * static_cast(2*n + 1); uint64_t rem; term_hi = _udiv128(0, term_hi, divisor, &rem); term_lo = _udiv128(rem, term_lo, divisor, &rem); if (term_hi == 0 && term_lo == 0) break; if (n & 1) { unsigned char b = _subborrow_u64(0, sum_lo, term_lo, &sum_lo); _subborrow_u64(b, sum_hi, term_hi, &sum_hi); } else { unsigned char c = _addcarry_u64(0, sum_lo, term_lo, &sum_lo); _addcarry_u64(c, sum_hi, term_hi, &sum_hi); } } size_t wn = (sum_hi != 0) ? 2 : ((sum_lo != 0) ? 1 : 0); if (wn == 0) return Float::zero(precision); uint64_t words[2] = { sum_lo, sum_hi }; auto mant2 = Int::fromRawWordsPreNormalized( std::span(words, wn), 1); result = Float(std::move(mant2), -128, negate); } result.setResultPrecision(precision); return result; } // sin/cos 38-digit fast path: lightweight Float argument reduction + sinTaylor/cosTaylor // Assumes x > 0 (guaranteed by the caller) static Float trig_medium_impl(const Float& x, bool want_sin, int precision) { double x_d = x.toDouble(); // k = round(x / (π/2)) constexpr double two_over_pi = 0.6366197723675814; int k = static_cast(std::round(x_d * two_over_pi)); // z = |x| - k·(π/2) int wp = precision + 6; Float z; if (k == 0) { z = x; } else { Float pi_half = ldexp(Float::pi(wp), -1); z = x - mulScalarF(pi_half, static_cast(k)); } // Determine the quadrant from k%4 int quad = ((k % 4) + 4) % 4; bool negate = false; bool use_cos = false; if (want_sin) { // sin(z + k·π/2): 0→sin, 1→cos, 2→-sin, 3→-cos switch (quad) { case 0: break; case 1: use_cos = true; break; case 2: negate = true; break; case 3: use_cos = true; negate = true; break; } } else { // cos(z + k·π/2): 0→cos, 1→-sin, 2→-cos, 3→sin switch (quad) { case 0: use_cos = true; break; case 1: negate = true; break; case 2: use_cos = true; negate = true; break; case 3: break; } } Float result; if (use_cos) { result = cosTaylor(z, wp); } else { result = sinTaylor(z, wp); } if (negate) result = -result; result.setResultPrecision(precision); return result; } Float sin(const Float& x, int precision) { // Handle special values if (x.isNaN()) return Float::nan(); if (x.isInfinity()) return Float::nan(); if (x.isZero()) return Float::zero(); // Range reduction: sin(-x) = -sin(x) if (x.isNegative()) return -sin(-x, precision); int precision_bits = Float::precisionToBits(precision); if (x.mantissa().size() <= 1 && precision_bits <= 64) { int64_t total_bl = static_cast(x.mantissa().bitLength()) + x.exponent(); if (total_bl <= 52) return trig_small_impl(x, true, precision); } if (x.mantissa().size() <= 2 && precision_bits <= 128) return trig_medium_impl(x, true, precision); return zivRound([](const Float& a, int e, int p) { return sin_core(Float(a), e, p); }, x, x.effectiveBits(), precision); } Float sin(Float&& x, int precision) { // Handle special values if (x.isNaN()) return Float::nan(); if (x.isInfinity()) return Float::nan(); if (x.isZero()) return Float::zero(); // Range reduction: sin(-x) = -sin(x) if (x.isNegative()) return -sin(-x, precision); int precision_bits = Float::precisionToBits(precision); if (x.mantissa().size() <= 1 && precision_bits <= 64) { int64_t total_bl = static_cast(x.mantissa().bitLength()) + x.exponent(); if (total_bl <= 52) return trig_small_impl(x, true, precision); } if (x.mantissa().size() <= 2 && precision_bits <= 128) return trig_medium_impl(x, true, precision); return zivRound([](const Float& a, int e, int p) { return sin_core(Float(a), e, p); }, x, x.effectiveBits(), precision); } //============================================================================= // Cosine (cos) //============================================================================= // Body: takes x by value (assumes already moved) // Since cos(-x) = cos(x), the caller need not take abs — reduceToFirstQuadrant handles it static Float cos_core(Float x, int eff_x, int precision) { int compute_prec = effectiveComputePrecision(eff_x, precision); // (a) Properly guard the Taylor series roundoff accumulation with log10(N_terms). // (b) Inside argument reduction, extend π according to the magnitude of |x| (see reduceToFirstQuadrant). int working_precision = taylorWorkingPrecision(compute_prec); // cos(-x) = cos(x) x = abs(x); auto reduced = reduceToFirstQuadrant(std::move(x), working_precision); int wp_bits = Float::precisionToBits(working_precision); Float result; if (reduced.x.isZero()) { result = Float::one(working_precision); } else if (wp_bits >= BITBURST_THRESHOLD) { // Bit-burst: simultaneous sin/cos computation auto [s, c] = sincos_bitburst(reduced.x, working_precision); result = std::move(c); } else { int64_t x_msb = reduced.x.exponent() + static_cast(reduced.x.mantissa().bitLength()); if (wp_bits < 1000 && x_msb <= -1) { result = cosTaylor(reduced.x, working_precision); } else { result = cosDoubling(reduced.x, working_precision); } } if (reduced.cos_negative) result = -result; finalizeResult(result, eff_x, precision); return result; } Float cos(const Float& x, int precision) { // Handle special values if (x.isNaN()) return Float::nan(); if (x.isInfinity()) return Float::nan(); if (x.isZero()) return Float::one(precision); int precision_bits = Float::precisionToBits(precision); if (x.mantissa().size() <= 1 && precision_bits <= 64) { int64_t total_bl = static_cast(x.mantissa().bitLength()) + x.exponent(); if (total_bl <= 52) return trig_small_impl(x, false, precision); } if (x.mantissa().size() <= 2 && precision_bits <= 128) { Float abs_x = abs(x); return trig_medium_impl(abs_x, false, precision); } return zivRound([](const Float& a, int e, int p) { return cos_core(Float(a), e, p); }, x, x.effectiveBits(), precision); } Float cos(Float&& x, int precision) { // Handle special values if (x.isNaN()) return Float::nan(); if (x.isInfinity()) return Float::nan(); if (x.isZero()) return Float::one(precision); int precision_bits = Float::precisionToBits(precision); if (x.mantissa().size() <= 1 && precision_bits <= 64) { int64_t total_bl = static_cast(x.mantissa().bitLength()) + x.exponent(); if (total_bl <= 52) return trig_small_impl(x, false, precision); } if (x.mantissa().size() <= 2 && precision_bits <= 128) { Float abs_x = abs(x); return trig_medium_impl(abs_x, false, precision); } return zivRound([](const Float& a, int e, int p) { return cos_core(Float(a), e, p); }, x, x.effectiveBits(), precision); } //============================================================================= // Tangent (tan) //============================================================================= // TRIG-2: directly use sinCosDoubling to reduce to one range reduction + 2 Taylor passes static Float tan_core(Float x, int eff_x, int precision) { int compute_prec = effectiveComputePrecision(eff_x, precision); // (a) Guard the sin/cos Taylor roundoff accumulation with log10(N_terms). // (b) Inside argument reduction, extend π according to the magnitude of |x|. int working_precision = taylorWorkingPrecision(compute_prec); bool input_negative = x.isNegative(); x = abs(x); auto reduced = reduceToFirstQuadrant(std::move(x), working_precision); int wp_bits = Float::precisionToBits(working_precision); Float s, c; if (reduced.x.isZero()) { s = Float::zero(); c = Float::one(working_precision); } else if (wp_bits >= 1000) { // High precision: cosDoubling + sqrt(1 - cos²) saves one Taylor pass // sinCosDoubling: 2 Taylor + K×4M(n) reconstruction // cosDoubling+sqrt: 1 Taylor + K×2M(n) reconstruction + sqrt ≈ half the cost int64_t x_msb = reduced.x.exponent() + static_cast(reduced.x.mantissa().bitLength()); int extra_guard = (x_msb < 0) ? static_cast(-2 * x_msb) : 0; int wp_cos = working_precision + Float::bitsToPrecision(extra_guard); c = cosDoubling(reduced.x, wp_cos); Float one_minus_c2 = Float::one(wp_cos) - c * c; s = sqrt(std::move(one_minus_c2), working_precision); c.truncateToApprox(working_precision); } else { // Low precision: sinCosDoubling (overhead dominates) auto [sv, cv] = sinCosDoubling(reduced.x, working_precision); s = std::move(sv); c = std::move(cv); } if (reduced.sin_negative) s = -s; if (reduced.cos_negative) c = -c; // cos ≈ 0 → tan → ±∞ if (c.isZero()) { bool result_neg = input_negative ? !s.isNegative() : s.isNegative(); return result_neg ? Float::negativeInfinity() : Float::positiveInfinity(); } Float result = s / c; if (input_negative) result = -result; // tan(-x) = -tan(x) finalizeResult(result, eff_x, precision); return result; } // tan Q128 fast path: sin_small / cos_small → Float division static Float tan_small(const Float& x, int precision) { Float s = trig_small_impl(x, true, precision + 1); Float c = trig_small_impl(x, false, precision + 1); if (c.isZero()) { return s.isNegative() ? Float::negativeInfinity() : Float::positiveInfinity(); } Float result = s / c; result.setResultPrecision(precision); return result; } // tan 38-digit fast path: share argument reduction to compute sin/cos at once static Float tan_medium(const Float& x, int precision) { double x_d = x.toDouble(); constexpr double two_over_pi = 0.6366197723675814; int k = static_cast(std::round(x_d * two_over_pi)); int wp = precision + 4; Float z; if (k == 0) { z = x; } else { Float pi_half = ldexp(Float::pi(wp), -1); z = x - mulScalarF(pi_half, static_cast(k)); } // Compute both sinTaylor and cosTaylor Float sv = sinTaylor(z, wp); Float cv = cosTaylor(z, wp); // Sign adjustment from k%4 int quad = ((k % 4) + 4) % 4; // tan(z + k·π/2): // k%4=0: sin/cos, k%4=1: cos/(-sin), k%4=2: (-sin)/(-cos)=sin/cos, k%4=3: (-cos)/sin Float num, den; switch (quad) { case 0: num = std::move(sv); den = std::move(cv); break; case 1: num = std::move(cv); den = -std::move(sv); break; case 2: num = std::move(sv); den = std::move(cv); break; case 3: num = -std::move(cv); den = std::move(sv); break; } if (den.isZero()) { return num.isNegative() ? Float::negativeInfinity() : Float::positiveInfinity(); } Float result = num / den; result.setResultPrecision(precision); return result; } Float tan(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) return Float::nan(); if (x.isZero()) return Float::zero(); int precision_bits = Float::precisionToBits(precision); if (x.mantissa().size() <= 1 && precision_bits <= 64) { const Float& abs_x = x.isNegative() ? static_cast(-x) : x; int64_t total_bl = static_cast(x.mantissa().bitLength()) + x.exponent(); if (total_bl <= 52) { // tan(-x) = -tan(x): trig_small_impl assumes x > 0 if (x.isNegative()) { Float neg_x = -x; return -tan_small(neg_x, precision); } return tan_small(x, precision); } } if (x.mantissa().size() <= 2 && precision_bits <= 128) { if (x.isNegative()) { Float neg_x = -x; return -tan_medium(neg_x, precision); } return tan_medium(x, precision); } return zivRound([](const Float& a, int e, int p) { return tan_core(Float(a), e, p); }, x, x.effectiveBits(), precision); } Float tan(Float&& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) return Float::nan(); if (x.isZero()) return Float::zero(); int precision_bits = Float::precisionToBits(precision); if (x.mantissa().size() <= 1 && precision_bits <= 64) { int64_t total_bl = static_cast(x.mantissa().bitLength()) + x.exponent(); if (total_bl <= 52) { if (x.isNegative()) { Float neg_x = -x; return -tan_small(neg_x, precision); } return tan_small(x, precision); } } if (x.mantissa().size() <= 2 && precision_bits <= 128) { if (x.isNegative()) { Float neg_x = -x; return -tan_medium(neg_x, precision); } return tan_medium(x, precision); } return zivRound([](const Float& a, int e, int p) { return tan_core(Float(a), e, p); }, x, x.effectiveBits(), precision); } //============================================================================= // Hyperbolic functions common: Taylor series + double-angle reconstruction //============================================================================= // Unified sinh+cosh Taylor (naive path): shares x², arena, scratch // sinh(x) = x + x³/3! + x⁵/5! + ... (all terms positive) // cosh(x) = 1 + x²/2! + x⁴/4! + ... (all terms positive) static std::pair sinhCoshTaylorNaive(const Float& x, int working_precision) { int wp_bits = Float::precisionToBits(working_precision); int nw = (wp_bits + 63) / 64; ScratchScope scope; auto& arena = getThreadArena(); size_t prod_alloc = 2 * static_cast(nw) + 4; uint64_t* prod_buf = arena.alloc_limbs(prod_alloc); size_t mul_scratch = mpn::multiply_scratch_size(nw + 1, nw + 1); size_t sqr_scratch = mpn::square_scratch_size(nw + 1); size_t scratch_sz = std::max(mul_scratch, sqr_scratch); uint64_t* scratch = arena.alloc_limbs(scratch_sz > 0 ? scratch_sz : 1); RawFloat x_rf = rf_extract(x, nw, arena); bool x_negative = x.isNegative(); // Share x² (only one squaring needed) uint64_t* x2_d = arena.alloc_limbs(nw + 2); RawFloat x2{x2_d, 0, 0}; rf_sqr(x2, x_rf, prod_buf, scratch, nw); // sinh: term=x, result=x uint64_t* s_term_d = arena.alloc_limbs(nw + 2); std::memset(s_term_d, 0, (nw + 2) * sizeof(uint64_t)); std::memcpy(s_term_d, x_rf.d, x_rf.nw * sizeof(uint64_t)); RawFloat s_term{s_term_d, x_rf.nw, x_rf.exp}; uint64_t* s_result_d = arena.alloc_limbs(nw + 2); std::memset(s_result_d, 0, (nw + 2) * sizeof(uint64_t)); std::memcpy(s_result_d, x_rf.d, x_rf.nw * sizeof(uint64_t)); RawFloat s_result{s_result_d, x_rf.nw, x_rf.exp}; // cosh: term=1, result=1 uint64_t* c_term_d = arena.alloc_limbs(nw + 2); std::memset(c_term_d, 0, (nw + 2) * sizeof(uint64_t)); c_term_d[nw - 1] = uint64_t(1) << 63; RawFloat c_term{c_term_d, static_cast(nw), -static_cast(nw * 64 - 1)}; uint64_t* c_result_d = arena.alloc_limbs(nw + 2); std::memset(c_result_d, 0, (nw + 2) * sizeof(uint64_t)); c_result_d[nw - 1] = uint64_t(1) << 63; RawFloat c_result{c_result_d, static_cast(nw), -static_cast(nw * 64 - 1)}; int max_terms = static_cast(working_precision * 1.2) + 10; for (int k = 1; k <= max_terms; ++k) { // sinh: term *= x² / ((2k)(2k+1)) rf_mul(s_term, s_term, x2, prod_buf, scratch, nw); uint64_t s_div = static_cast(2*k) * static_cast(2*k + 1); rf_divmod_1(s_term, s_div); // cosh: term *= x² / ((2k-1)(2k)) rf_mul(c_term, c_term, x2, prod_buf, scratch, nw); uint64_t c_div = static_cast(2*k - 1) * static_cast(2*k); rf_divmod_1(c_term, c_div); bool s_done = (s_term.nw == 0) || !rf_add(s_result, s_term, nw + 1); bool c_done = (c_term.nw == 0) || !rf_add(c_result, c_term, nw + 1); if (s_done && c_done) break; } Float s_out = rf_to_float(s_result, x_negative, working_precision); Float c_out = rf_to_float(c_result, false, working_precision); return { std::move(s_out), std::move(c_out) }; } // Paterson-Stockmeyer for the sinh+cosh Taylor series (shared R[] table) // sinh(x) = x · Σ_{k=0}^{∞} u^k / (2k+1)! where u = x² (no sign alternation) // cosh(x) = Σ_{k=0}^{∞} u^k / (2k)! where u = x² (no sign alternation) static std::pair sinhCoshTaylorPS(const Float& x, int working_precision) { int wp_bits = Float::precisionToBits(working_precision); int nw = (wp_bits + 63) / 64; ScratchScope scope; auto& arena = getThreadArena(); size_t prod_alloc = 2 * static_cast(nw) + 4; uint64_t* prod_buf = arena.alloc_limbs(prod_alloc); size_t mul_scratch = mpn::multiply_scratch_size(nw + 1, nw + 1); size_t sqr_scratch = mpn::square_scratch_size(nw + 1); size_t scratch_sz = std::max(mul_scratch, sqr_scratch); uint64_t* scratch = arena.alloc_limbs(scratch_sz > 0 ? scratch_sz : 1); RawFloat x_rf = rf_extract(x, nw, arena); bool x_negative = x.isNegative(); // u = x² uint64_t* x2_d = arena.alloc_limbs(nw + 2); RawFloat x2{x2_d, 0, 0}; rf_sqr(x2, x_rf, prod_buf, scratch, nw); // Term count estimate (same structure as estimate_sincos_terms) int target_bits = nw * 64; int64_t u_msb = x2.exp + static_cast(x2.nw) * 64; int u_log2 = (u_msb <= 0) ? static_cast(-u_msb) : 1; if (u_log2 < 1) u_log2 = 1; int l_est = estimate_sincos_terms(target_bits, u_log2); int m = static_cast(std::sqrt(static_cast(l_est))); if (m < 2) m = 2; if (m > 256) m = 256; // R[0..m] = u^i (shared by sinh and cosh) std::vector R(m + 1); for (int i = 0; i <= m; ++i) { R[i].d = arena.alloc_limbs(nw + 2); std::memset(R[i].d, 0, (nw + 2) * sizeof(uint64_t)); R[i].nw = 0; R[i].exp = 0; } // R[0] = 1.0 R[0].d[nw - 1] = uint64_t(1) << 63; R[0].nw = static_cast(nw); R[0].exp = -static_cast(nw * 64 - 1); // R[1] = u std::memcpy(R[1].d, x2.d, x2.nw * sizeof(uint64_t)); R[1].nw = x2.nw; R[1].exp = x2.exp; // NTT cache of R[1] prime_ntt::NttCache cache_r1; rf_sqr(R[2], R[1], prod_buf, scratch, nw); for (int i = 3; i <= m; ++i) { if ((i & 1) == 0) rf_sqr(R[i], R[i/2], prod_buf, scratch, nw); else rf_mul_cached(R[i], R[i-1], R[1], prod_buf, nw, cache_r1); } // NTT cache of R[m] (reused in the giant steps) prime_ntt::NttCache cache_rm; // Working buffer RawFloat t; t.d = arena.alloc_limbs(nw + 2); uint64_t* tmp_d = arena.alloc_limbs(nw + 2); int max_giant_steps = l_est / m + 5; // ====== sinh P-S: S(u) = Σ_{k=0}^{∞} u^k / (2k+1)! ====== // Denominator: (2k+1)! → the baby step divisor is (2(l+i+1))(2(l+i+1)+1) // No sign alternation → Horner with addition only uint64_t* sinh_result_d = arena.alloc_limbs(nw + 2); std::memset(sinh_result_d, 0, (nw + 2) * sizeof(uint64_t)); RawFloat sinh_result{sinh_result_d, 0, 0}; // rr_s: tracks u^(l*m) / (2*l*m+1)! uint64_t* rr_s_d = arena.alloc_limbs(nw + 2); std::memset(rr_s_d, 0, (nw + 2) * sizeof(uint64_t)); rr_s_d[nw - 1] = uint64_t(1) << 63; RawFloat rr_s{rr_s_d, static_cast(nw), -static_cast(nw * 64 - 1)}; { int l = 0; for (int gs = 0; gs < max_giant_steps; ++gs) { // Baby step: Horner (no sign alternation → addition only) std::memcpy(t.d, R[m-1].d, R[m-1].nw * sizeof(uint64_t)); t.nw = R[m-1].nw; t.exp = R[m-1].exp; for (int i = m - 2; i >= 0; --i) { uint64_t d1 = static_cast(2*(l+i+1)); uint64_t d2 = d1 + 1; rf_divmod_1(t, d1 * d2); if (t.nw == 0) { std::memcpy(t.d, R[i].d, R[i].nw * sizeof(uint64_t)); t.nw = R[i].nw; t.exp = R[i].exp; } else { // No sign alternation: t = R[i] + t // R[i] is the dominant value (larger) → add t to tmp=R[i] to avoid heap allocation std::memset(tmp_d, 0, (nw + 2) * sizeof(uint64_t)); std::memcpy(tmp_d, R[i].d, R[i].nw * sizeof(uint64_t)); RawFloat tmp{tmp_d, R[i].nw, R[i].exp}; rf_add(tmp, t, nw + 1); std::memcpy(t.d, tmp.d, tmp.nw * sizeof(uint64_t)); std::memset(t.d + tmp.nw, 0, (nw + 2 - tmp.nw) * sizeof(uint64_t)); t.nw = tmp.nw; t.exp = tmp.exp; } } if (gs > 0) { rf_mul(t, t, rr_s, prod_buf, scratch, nw); } if (t.nw == 0) break; // Always add (no sign alternation) rf_add(sinh_result, t, nw + 1); // Update rr_s: rr_s *= R[m] / Π_{j=0}^{m-1} ((2(l+j)+2)(2(l+j)+3)) rf_mul_cached(rr_s, rr_s, R[m], prod_buf, nw, cache_rm); for (int j = 0; j < m; ++j) { uint64_t d1 = static_cast(2*(l+j) + 2); uint64_t d2 = d1 + 1; rf_divmod_1(rr_s, d1 * d2); } l += m; if (rr_s.nw == 0) break; } } // sinh(x) = x * S(u) rf_mul(sinh_result, sinh_result, x_rf, prod_buf, scratch, nw); // ====== cosh P-S: C(u) = Σ_{k=0}^{∞} u^k / (2k)! ====== // Denominator: (2k)! → the baby step divisor is (2(l+i+1)-1)(2(l+i+1)) // No sign alternation → Horner with addition only uint64_t* cosh_result_d = arena.alloc_limbs(nw + 2); std::memset(cosh_result_d, 0, (nw + 2) * sizeof(uint64_t)); RawFloat cosh_result{cosh_result_d, 0, 0}; // rr_c: tracks u^(l*m) / (2*l*m)! uint64_t* rr_c_d = arena.alloc_limbs(nw + 2); std::memset(rr_c_d, 0, (nw + 2) * sizeof(uint64_t)); rr_c_d[nw - 1] = uint64_t(1) << 63; RawFloat rr_c{rr_c_d, static_cast(nw), -static_cast(nw * 64 - 1)}; { int l = 0; for (int gs = 0; gs < max_giant_steps; ++gs) { // Baby step: Horner (no sign alternation) std::memcpy(t.d, R[m-1].d, R[m-1].nw * sizeof(uint64_t)); t.nw = R[m-1].nw; t.exp = R[m-1].exp; for (int i = m - 2; i >= 0; --i) { uint64_t d1 = static_cast(2*(l+i+1) - 1); uint64_t d2 = d1 + 1; rf_divmod_1(t, d1 * d2); if (t.nw == 0) { std::memcpy(t.d, R[i].d, R[i].nw * sizeof(uint64_t)); t.nw = R[i].nw; t.exp = R[i].exp; } else { // No sign alternation: t = R[i] + t std::memset(tmp_d, 0, (nw + 2) * sizeof(uint64_t)); std::memcpy(tmp_d, R[i].d, R[i].nw * sizeof(uint64_t)); RawFloat tmp{tmp_d, R[i].nw, R[i].exp}; rf_add(tmp, t, nw + 1); std::memcpy(t.d, tmp.d, tmp.nw * sizeof(uint64_t)); std::memset(t.d + tmp.nw, 0, (nw + 2 - tmp.nw) * sizeof(uint64_t)); t.nw = tmp.nw; t.exp = tmp.exp; } } if (gs > 0) { rf_mul(t, t, rr_c, prod_buf, scratch, nw); } if (t.nw == 0) break; // Always add (no sign alternation) rf_add(cosh_result, t, nw + 1); // Update rr_c: rr_c *= R[m] / Π_{j=0}^{m-1} ((2(l+j)+1)(2(l+j)+2)) rf_mul_cached(rr_c, rr_c, R[m], prod_buf, nw, cache_rm); for (int j = 0; j < m; ++j) { uint64_t d1 = static_cast(2*(l+j) + 1); uint64_t d2 = d1 + 1; rf_divmod_1(rr_c, d1 * d2); } l += m; if (rr_c.nw == 0) break; } } Float s_out = rf_to_float(sinh_result, x_negative, working_precision); Float c_out = rf_to_float(cosh_result, false, working_precision); return { std::move(s_out), std::move(c_out) }; } // Unified sinh+cosh Taylor function (automatically selects naive/P-S by precision) // P-S threshold: same 10000 bits as sinCosDoubling // (at small-to-medium precision, naive has lower overhead) static std::pair sinhCoshTaylor(const Float& x, int working_precision) { int wp_bits = Float::precisionToBits(working_precision); if (wp_bits >= 10000) { return sinhCoshTaylorPS(x, working_precision); } return sinhCoshTaylorNaive(x, working_precision); } // Simultaneous (sinh, cosh) computation via the double-angle formula // sinh(2t) = 2sinh(t)cosh(t), cosh(2t) = 2cosh²(t) - 1 // Same structure as sinCosDoubling. Since cosh ≥ 1, 2cosh²-1 ≥ 1 (no borrow) static std::pair sinhCoshDoubling(const Float& x, int working_precision) { int wp_bits = Float::precisionToBits(working_precision); int K; if (wp_bits >= 10000) { // With P-S, Taylor is O(√N) → reconstruction 2K + Taylor 6√(p/(2K)) // Optimal K = cbrt(2·wp_bits) (since reconstruction is 2M(n)/step) // Same optimization as sinCosDoubling K = static_cast(std::cbrt(2.0 * wp_bits)); } else { K = static_cast(std::sqrt(wp_bits / 3.0)); } if (K < 1) return sinhCoshTaylor(x, working_precision); int guard_bits = 2 * K + static_cast(std::ceil(std::log2(K + 1))) + 10; int wp_inner = working_precision + Float::bitsToPrecision(guard_bits); Float x_red = ldexp(abs(x), -K); // |x| / 2^K x_red.truncateToApprox(wp_inner); int wp_inner_bits = Float::precisionToBits(wp_inner); x_red.setEffectiveBits(wp_inner_bits); auto [s_float, c_float] = sinhCoshTaylor(x_red, wp_inner); // K simultaneous reconstructions (RawFloat) // sinh(2t) = 2sinh(t)cosh(t), cosh(2t) = 2cosh²(t) - 1 int nw = (wp_inner_bits + 63) / 64; ScratchScope scope; auto& arena = getThreadArena(); uint64_t* s_d = arena.alloc_limbs(nw + 2); RawFloat s_rf = rf_extract(s_float, nw, arena); std::memcpy(s_d, s_rf.d, s_rf.nw * sizeof(uint64_t)); s_rf.d = s_d; uint64_t* c_d = arena.alloc_limbs(nw + 2); RawFloat c_rf = rf_extract(c_float, nw, arena); std::memcpy(c_d, c_rf.d, c_rf.nw * sizeof(uint64_t)); c_rf.d = c_d; size_t prod_alloc = 2 * static_cast(nw) + 4; uint64_t* prod_buf = arena.alloc_limbs(prod_alloc); size_t sqr_scratch_sz = mpn::square_scratch_size(nw + 1); size_t scratch_sz = std::max(mpn::multiply_scratch_size(nw + 1, nw + 1), sqr_scratch_sz); uint64_t* scratch_buf = arena.alloc_limbs(scratch_sz > 0 ? scratch_sz : 1); uint64_t* tmp_d = arena.alloc_limbs(nw + 2); // Gradual precision reduction (P2-3 technique) int hyp_guard_limbs = (K + 63) / 64 + 6; for (int i = 0; i < K; ++i) { int remaining = K - 1 - i; int needed_nw = nw; if (remaining > 0 && remaining < 20) { needed_nw = (nw >> remaining) + hyp_guard_limbs; if (needed_nw < hyp_guard_limbs + 2) needed_nw = hyp_guard_limbs + 2; if (needed_nw > nw) needed_nw = nw; } else if (remaining >= 20) { needed_nw = hyp_guard_limbs + 2; } // truncate if (static_cast(s_rf.nw) > needed_nw) { size_t drop = s_rf.nw - needed_nw; std::memmove(s_rf.d, s_rf.d + drop, needed_nw * sizeof(uint64_t)); s_rf.nw = needed_nw; s_rf.exp += static_cast(drop) * 64; } if (static_cast(c_rf.nw) > needed_nw) { size_t drop = c_rf.nw - needed_nw; std::memmove(c_rf.d, c_rf.d + drop, needed_nw * sizeof(uint64_t)); c_rf.nw = needed_nw; c_rf.exp += static_cast(drop) * 64; } // new_s = 2 * s * c (stored in a temporary buffer) RawFloat tmp_rf{tmp_d, 0, 0}; rf_mul(tmp_rf, s_rf, c_rf, prod_buf, scratch_buf, needed_nw); tmp_rf.exp += 1; // ×2 // c = 2 * c² - 1 (since cosh ≥ 1, 2cosh² ≥ 2 > 1, no borrow, accelerated via squaring) rf_sqr(c_rf, c_rf, prod_buf, scratch_buf, needed_nw); c_rf.exp += 1; // ×2 // c -= 1.0: subtract 2^(-exp) from the mantissa if (c_rf.nw > 0 && c_rf.exp < 0) { int64_t neg_exp = -c_rf.exp; size_t word_idx = static_cast(neg_exp / 64); unsigned bit_idx = static_cast(neg_exp % 64); if (word_idx < c_rf.nw) { mpn::sub_1( c_rf.d + word_idx, c_rf.nw - word_idx, 1ULL << bit_idx); // cosh ≥ 1 → 2cosh²-1 ≥ 1 → no borrow occurs } // word_idx >= nw does not occur since cosh ≥ 1 } c_rf.nw = mpn::normalized_size(c_rf.d, c_rf.nw); rf_normalize(c_rf); // s = new_s std::memcpy(s_rf.d, tmp_rf.d, tmp_rf.nw * sizeof(uint64_t)); s_rf.nw = tmp_rf.nw; s_rf.exp = tmp_rf.exp; } // sinh inherits the sign of x; cosh is always positive Float s_out = rf_to_float(s_rf, x.isNegative(), wp_inner); Float c_out = rf_to_float(c_rf, false, wp_inner); s_out.truncateToApprox(working_precision); c_out.truncateToApprox(working_precision); return { std::move(s_out), std::move(c_out) }; } //============================================================================= // Hyperbolic sine (sinh) //============================================================================= // sinh/cosh/tanh common: 19-digit fast path using exp_small static Float sinh_small(const Float& x, int precision) { int wp = precision + 2; Float ex = exp_small(x, wp); Float emx = Float::one(wp) / ex; Float result = (ex - emx) >>= 1; // / 2 result.setResultPrecision(precision); return result; } static Float cosh_small(const Float& x, int precision) { int wp = precision + 2; Float abs_x = abs(x); Float ex = exp_small(abs_x, wp); Float emx = Float::one(wp) / ex; Float result = (ex + emx) >>= 1; // / 2 result.setResultPrecision(precision); return result; } static Float tanh_small(const Float& x, int precision) { int wp = precision + 2; Float ex = exp_small(x, wp); Float emx = Float::one(wp) / ex; Float result = (ex - emx) / (ex + emx); result.setResultPrecision(precision); return result; } // Single-exp method: e = exp(|x|), sinh = (e - 1/e)/2, cosh = (e + 1/e)/2 // Faster than Taylor + double-angle reconstruction at large sizes (exp is optimized, only one division) // Optimizations: // - Call exp_core directly (avoids double zivRound — the outer sinh/cosh zivRound suffices) // - Skip 1/e when |x| is large (exp(-|x|) is below working precision) static std::pair sinhCoshViaExp(const Float& x, int working_precision) { int wp = working_precision + 5; // guard digits for 1/e subtraction int wp_bits = Float::precisionToBits(wp); Float abs_x = abs(x); // Call exp_core directly (avoids zivRound) // The outer sinh/cosh zivRound provides the precision guarantee Float e = exp_core(Float(abs_x), abs_x.effectiveBits(), wp); // If |x| * log2(e) > wp_bits + 64, then exp(-|x|) is below working precision // → skip the 1/e division and sinh ≈ cosh ≈ e/2 double abs_x_d = abs_x.toDouble(); bool skip_inv = (abs_x_d * 1.4426950408889634 > wp_bits + 64); Float s, c; if (skip_inv) { s = e >>= 1; // sinh(|x|) ≈ exp(|x|) / 2 c = s; // cosh(|x|) ≈ exp(|x|) / 2 } else { Float inv_e = Float::one(wp) / e; s = (e - inv_e) >>= 1; // sinh(|x|) = (e - 1/e) / 2 c = (e + inv_e) >>= 1; // cosh(|x|) = (e + 1/e) / 2 } if (x.isNegative()) s = -std::move(s); s.truncateToApprox(working_precision); c.truncateToApprox(working_precision); return { std::move(s), std::move(c) }; } // Unified sinh/cosh dispatch: selects Taylor+double-angle or the exp method by precision // Threshold 300 bits (~90 digits): above this, direct exp_core calls beat double-angle reconstruction // (it was 5000 bits before, but direct exp_core calls make a lower threshold favorable) // 100d: 1.98x→1.84x, 500d: 2.61x→1.86x, 1000d: 3.10x→2.14x (vs MPFR) static constexpr int SINHCOSH_EXP_THRESHOLD_BITS = 300; static std::pair sinhCoshCompute(const Float& x, int working_precision) { int wp_bits = Float::precisionToBits(working_precision); if (wp_bits >= SINHCOSH_EXP_THRESHOLD_BITS) { return sinhCoshViaExp(x, working_precision); } return sinhCoshDoubling(x, working_precision); } // Dispatch for Ziv static Float sinh_dispatch(const Float& x, int eff_x, int precision) { int wp = effectiveComputePrecision(eff_x, precision); auto [s, c] = sinhCoshCompute(x, wp); return s; } Float sinh(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) { return x.isNegative() ? Float::negativeInfinity() : Float::positiveInfinity(); } if (x.isZero()) return Float::zero(); int precision_bits = Float::precisionToBits(precision); if (x.mantissa().size() <= 1 && precision_bits <= 64) return sinh_small(x, precision); return zivRound(sinh_dispatch, x, x.effectiveBits(), precision); } Float sinh(Float&& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) { return x.isNegative() ? Float::negativeInfinity() : Float::positiveInfinity(); } if (x.isZero()) return Float::zero(); int precision_bits = Float::precisionToBits(precision); if (x.mantissa().size() <= 1 && precision_bits <= 64) return sinh_small(x, precision); return zivRound(sinh_dispatch, x, x.effectiveBits(), precision); } //============================================================================= // Hyperbolic cosine (cosh) //============================================================================= static Float cosh_dispatch(const Float& x, int eff_x, int precision) { int wp = effectiveComputePrecision(eff_x, precision); auto [s, c] = sinhCoshCompute(x, wp); return c; } Float cosh(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) return Float::positiveInfinity(); if (x.isZero()) return Float::one(precision); int precision_bits = Float::precisionToBits(precision); if (x.mantissa().size() <= 1 && precision_bits <= 64) return cosh_small(x, precision); return zivRound(cosh_dispatch, x, x.effectiveBits(), precision); } Float cosh(Float&& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) return Float::positiveInfinity(); if (x.isZero()) return Float::one(precision); int precision_bits = Float::precisionToBits(precision); if (x.mantissa().size() <= 1 && precision_bits <= 64) return cosh_small(x, precision); return zivRound(cosh_dispatch, x, x.effectiveBits(), precision); } //============================================================================= // Hyperbolic tangent (tanh) //============================================================================= static Float tanh_dispatch(const Float& x, int eff_x, int precision) { int wp = effectiveComputePrecision(eff_x, precision); auto [s, c] = sinhCoshCompute(x, wp); return s / c; } Float tanh(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) { return x.isNegative() ? Float(-1) : Float(1); } if (x.isZero()) return Float::zero(); double x_d = x.toDouble(); if (std::fabs(x_d) > 20.0) { return x.isNegative() ? Float(-1) : Float(1); } int precision_bits = Float::precisionToBits(precision); if (x.mantissa().size() <= 1 && precision_bits <= 64) return tanh_small(x, precision); return zivRound(tanh_dispatch, x, x.effectiveBits(), precision); } Float tanh(Float&& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) { return x.isNegative() ? Float(-1) : Float(1); } if (x.isZero()) return Float::zero(); double x_d = x.toDouble(); if (std::fabs(x_d) > 20.0) { return x.isNegative() ? Float(-1) : Float(1); } int precision_bits = Float::precisionToBits(precision); if (x.mantissa().size() <= 1 && precision_bits <= 64) return tanh_small(x, precision); return zivRound(tanh_dispatch, x, x.effectiveBits(), precision); } //============================================================================= // Power (pow) - integer exponent //============================================================================= static Float pow_core(Float x, int eff_x, int n, int precision) { // Determine the computation precision based on the effective bit count int compute_prec = effectiveComputePrecision(eff_x, precision); // Working precision int working_precision = compute_prec + 10; x.truncateToApprox(working_precision); // Binary exponentiation // Truncate the mantissa with truncateToApprox after each multiplication. // Multiplication doubles the mantissa bit count, so left unchecked // the mantissa balloons to ~10000 bits at n=1000, becoming O(n²). Float result = Float::one(working_precision); int exp = n; while (exp > 0) { if (exp % 2 == 1) { result *= x; result.truncateToApprox(working_precision); } if (exp > 1) { x *= x; x.truncateToApprox(working_precision); } exp /= 2; } finalizeResult(result, eff_x, precision); return result; } Float pow(const Float& x, int n, int precision) { if (x.isNaN()) return Float::nan(); if (n == 0) return Float::one(precision); if (n == 1) return Float(x); if (x.isZero()) return (n < 0) ? Float::positiveInfinity() : Float::zero(); if (x.isInfinity()) { if (n < 0) return Float::zero(); if (x.isNegative() && (n % 2 != 0)) return Float::negativeInfinity(); return Float::positiveInfinity(); } if (n == -1) return Float::one(precision) / x; if (n < 0) return Float::one(precision) / pow(x, -n, precision); return pow_core(Float(x), x.effectiveBits(), n, precision); } Float pow(Float&& x, int n, int precision) { if (x.isNaN()) return Float::nan(); if (n == 0) return Float::one(precision); if (n == 1) return std::move(x); if (x.isZero()) return (n < 0) ? Float::positiveInfinity() : Float::zero(); if (x.isInfinity()) { if (n < 0) return Float::zero(); if (x.isNegative() && (n % 2 != 0)) return Float::negativeInfinity(); return Float::positiveInfinity(); } if (n == -1) return Float::one(precision) / x; if (n < 0) return Float::one(precision) / pow(std::move(x), -n, precision); int eff = x.effectiveBits(); return pow_core(std::move(x), eff, n, precision); } //============================================================================= // Power (pow) - general exponent //============================================================================= // pow fast path (19 digits): Q128 log + Float mul + Q128 exp static Float pow_small(const Float& a, const Float& b, int precision) { // Q128 log → convert to Float → Float multiply → Q128 exp Float lna = log_small(a, precision); Float L = b * lna; return exp_small(L, precision); } // pow fast path (38 digits): Q256 log + Float mul + exp static Float pow_medium(const Float& a, const Float& b, int precision) { int wp = precision + 5; // Guard bits (rounding error accumulation of log→mul→exp) Float lna = log_medium(a, wp); Float L = b * lna; Float result = exp(std::move(L), wp); result.setResultPrecision(precision); return result; } // pow general path (Q128/Q256 decision + fallback) static Float pow_general(const Float& a, const Float& b, int precision) { int precision_bits = Float::precisionToBits(precision); if (precision_bits <= 64) { return pow_small(a, b, precision); } if (precision_bits <= 128) { return pow_medium(a, b, precision); } // Fallback: Float arithmetic (Ziv iteration) // pow(a,b) = exp(b * log(a)) // Note: the optimization of lowering log's precision according to |b| gives near-zero savings // for typical |b| of 0.1-1.0, and a slight precision shortfall triggers zivRound retries, backfiring. // It is effective only for |b| << 1 (e.g. 10^{-100}), but is currently not applied. int eff = std::min(a.effectiveBits(), b.effectiveBits()); return zivRound([&b](const Float& base, int e, int p) { int ep = p + 10; Float log_x = log(abs(base), ep); Float y_log_x = b * log_x; return exp(std::move(y_log_x), ep); }, a, eff, precision); } Float pow(const Float& x, const Float& y, int precision) { // Handle special values if (x.isNaN() || y.isNaN()) return Float::nan(); if (y.isZero()) return Float::one(precision); if (x.isZero()) { if (y.isNegative()) return Float::positiveInfinity(); return Float::zero(); } if (x.isInfinity()) { // Handle special cases if (y.isPositive()) { return Float::positiveInfinity(); } else { return Float::zero(); } } if (y.isInfinity()) { // Handle special cases Float one = Float::one(precision); if (abs(x) > one) { return y.isPositive() ? Float::positiveInfinity() : Float::zero(); } else if (abs(x) < one) { return y.isPositive() ? Float::zero() : Float::positiveInfinity(); } else { // |x| = 1 return Float::one(precision); } } // For integer exponents, use the dedicated function // In mantissa * 2^exponent, if exponent >= 0 it is trivially an integer. // Even if exponent < 0, it is an integer if the trailing zero-bit count of mantissa >= |exponent|. { bool y_is_integer = (y.exponent() >= 0) || (static_cast(y.mantissa().countTrailingZeros()) >= -y.exponent()); if (y_is_integer && abs(y) < Float(1e9)) { Int temp_int = y.toInt(); int n = temp_int.toInt(); return pow(x, n, precision); } } // For a negative base, non-integer exponents yield complex numbers if (x.isNegative()) return Float::nan(); return pow_general(x, y, precision); } Float pow(Float&& x, Float&& y, int precision) { // Handle special values if (x.isNaN() || y.isNaN()) return Float::nan(); if (y.isZero()) return Float::one(precision); if (x.isZero()) { if (y.isNegative()) return Float::positiveInfinity(); return Float::zero(); } if (x.isInfinity()) { // Handle special cases if (y.isPositive()) { return Float::positiveInfinity(); } else { return Float::zero(); } } if (y.isInfinity()) { // Handle special cases Float one = Float::one(precision); if (abs(x) > one) { return y.isPositive() ? Float::positiveInfinity() : Float::zero(); } else if (abs(x) < one) { return y.isPositive() ? Float::zero() : Float::positiveInfinity(); } else { // |x| = 1 return Float::one(precision); } } // For integer exponents, use the dedicated function // In mantissa * 2^exponent, if exponent >= 0 it is trivially an integer. // Even if exponent < 0, it is an integer if the trailing zero-bit count of mantissa >= |exponent|. { bool y_is_integer = (y.exponent() >= 0) || (static_cast(y.mantissa().countTrailingZeros()) >= -y.exponent()); if (y_is_integer && abs(y) < Float(1e9)) { Int temp_int = y.toInt(); int n = temp_int.toInt(); return pow(std::move(x), n, precision); } } // For a negative base, non-integer exponents yield complex numbers if (x.isNegative()) return Float::nan(); return pow_general(x, y, precision); } //============================================================================= // Inverse tangent (atan) — Taylor series + argument halving //============================================================================= // // ■ Algorithm // // atan(x) = x − x³/3 + x⁵/5 − x⁷/7 + ... // Recurrence: term_{k+1} = term_k · x² · (2k−1) / (2k+1) // // Argument-halving formula: atan(x) = 2 · atan(x / (1 + √(1 + x²))) // After applying K times → Taylor computation → reconstruct via ldexp(result, K) // // When |x| > 1: reduce to |x| < 1 via atan(x) = π/2 − atan(1/x) // // Internal computation of atan(x) via Taylor series (assumes |x| << 1) // Same RawFloat pattern as sinTaylor/cosTaylor // Paterson-Stockmeyer for the atan Taylor series // atan(x) = x · Σ_{k=0}^{∞} (-1)^k · u^k / (2k+1) where u = x² // O(√N) full-size multiplications + O(N) scalar mul/div // giant step rr update: telescoping product Π(2l+2j+1)/Π(2l+2j+3) = (2l+1)/(2l+2m+1) static void rf_atan_ps(RawFloat& result, const RawFloat& x_rf, const RawFloat& x2, int nw, uint64_t* prod_buf, uint64_t* scratch, ScratchArena& arena, int working_precision) { int target_bits = nw * 64; // Estimate term count from the MSB of u = x² (no factorial: N ≈ target_bits / u_log2) int64_t u_msb = x2.exp + static_cast(x2.nw) * 64; int u_log2 = (u_msb <= 0) ? static_cast(-u_msb) : 1; if (u_log2 < 1) u_log2 = 1; int l_est = target_bits / u_log2 + 5; int m = static_cast(std::sqrt(static_cast(l_est))); if (m < 2) m = 2; if (m > 256) m = 256; // Round m up to even (simplifies sign handling in the Horner giant step) if (m % 2 != 0) m++; int G = l_est / m + 1; // Number of giant steps (blocks) // Precompute R[0..m]: R[i] = u^i std::vector R(m + 1); for (int i = 0; i <= m; ++i) { R[i].d = arena.alloc_limbs(nw + 2); std::memset(R[i].d, 0, (nw + 2) * sizeof(uint64_t)); R[i].nw = 0; R[i].exp = 0; } R[0].d[nw - 1] = uint64_t(1) << 63; R[0].nw = static_cast(nw); R[0].exp = -static_cast(nw * 64 - 1); std::memcpy(R[1].d, x2.d, x2.nw * sizeof(uint64_t)); R[1].nw = x2.nw; R[1].exp = x2.exp; // NTT cache of R[1] prime_ntt::NttCache atan_cache_r1; rf_sqr(R[2], R[1], prod_buf, scratch, nw); for (int i = 3; i <= m; ++i) { if ((i & 1) == 0) rf_sqr(R[i], R[i/2], prod_buf, scratch, nw); else rf_mul_cached(R[i], R[i-1], R[1], prod_buf, nw, atan_cache_r1); } // W = u^m = R[m] (used in the Horner giant step multiplication) // NTT cache of R[m] prime_ntt::NttCache atan_cache_rm; // Two buffers for the baby step (pointer swapping reduces memcpy) uint64_t* buf_a = arena.alloc_limbs(nw + 2); uint64_t* buf_b = arena.alloc_limbs(nw + 2); std::memset(buf_a, 0, (nw + 2) * sizeof(uint64_t)); std::memset(buf_b, 0, (nw + 2) * sizeof(uint64_t)); // Horner giant step: evaluate in reverse order (j = G-1 → 0) // S(u) = Σ_{j=0}^{G-1} W^j · C_j(u) (since m is even, signs are handled within C_j) // Horner: acc = C_{G-1}; for j=G-2..0: acc = acc·W + C_j // No rr tracking needed → 1 full mul per giant step (was: 2 full mul) // result = 0 (accumulator) std::memset(result.d, 0, (nw + 2) * sizeof(uint64_t)); result.nw = 0; result.exp = 0; for (int j = G - 1; j >= 0; --j) { int l = j * m; // Giant step: acc = acc · W (except for the first block) if (j < G - 1) { rf_mul_cached(result, result, R[m], prod_buf, nw, atan_cache_rm); } // Baby step: evaluate the block polynomial C_j(u) // C_j(u) = Σ_{i=0}^{m-1} (-1)^i · u^i / (2(l+i)+1) // Horner: t = R[m-1]/(2(l+m-1)+1), then down to R[0]/(2l+1) // Initialize t in buf_a std::memcpy(buf_a, R[m-1].d, R[m-1].nw * sizeof(uint64_t)); if (R[m-1].nw < static_cast(nw + 2)) std::memset(buf_a + R[m-1].nw, 0, (nw + 2 - R[m-1].nw) * sizeof(uint64_t)); RawFloat t{buf_a, R[m-1].nw, R[m-1].exp}; uint64_t top_den = static_cast(2*(l+m-1) + 1); rf_divmod_1(t, top_den); bool t_is_a = true; for (int i = m - 2; i >= 0; --i) { if (t.nw == 0) { // t = 0 → replace with R[i]/(2(l+i)+1) (directly in the current buffer) uint64_t* cur = t_is_a ? buf_a : buf_b; std::memcpy(cur, R[i].d, R[i].nw * sizeof(uint64_t)); if (R[i].nw < static_cast(nw + 2)) std::memset(cur + R[i].nw, 0, (nw + 2 - R[i].nw) * sizeof(uint64_t)); t.d = cur; t.nw = R[i].nw; t.exp = R[i].exp; uint64_t d = static_cast(2*(l+i) + 1); rf_divmod_1(t, d); } else { // t = R[i]/(2(l+i)+1) - t: write the result to the opposite buffer uint64_t* dst = t_is_a ? buf_b : buf_a; std::memcpy(dst, R[i].d, R[i].nw * sizeof(uint64_t)); if (R[i].nw < static_cast(nw + 2)) std::memset(dst + R[i].nw, 0, (nw + 2 - R[i].nw) * sizeof(uint64_t)); RawFloat tmp{dst, R[i].nw, R[i].exp}; uint64_t d = static_cast(2*(l+i) + 1); rf_divmod_1(tmp, d); rf_sub(tmp, t, nw + 1); t = tmp; t_is_a = !t_is_a; } } if (t.nw == 0) continue; // Add C_j to the accumulator rf_add(result, t, nw + 1); } // atan(x) = x · S(u): result *= x rf_mul(result, result, x_rf, prod_buf, scratch, nw); } static Float atanTaylor(const Float& x, int working_precision) { int wp_bits = Float::precisionToBits(working_precision); int nw = (wp_bits + 63) / 64; ScratchScope scope; auto& arena = getThreadArena(); // Pre-allocate buffers size_t prod_alloc = 2 * static_cast(nw) + 4; uint64_t* prod_buf = arena.alloc_limbs(prod_alloc); size_t mul_scratch = mpn::multiply_scratch_size(nw + 2, nw + 2); size_t sqr_scratch = mpn::square_scratch_size(nw + 2); size_t scratch_sz = std::max(mul_scratch, sqr_scratch); uint64_t* scratch = arena.alloc_limbs(scratch_sz > 0 ? scratch_sz : 1); // Extract |x| RawFloat x_rf = rf_extract(x, nw, arena); // Precompute x² (squaring is accelerated via square) uint64_t* x2_d = arena.alloc_limbs(nw + 2); RawFloat x2{x2_d, 0, 0}; rf_sqr(x2, x_rf, prod_buf, scratch, nw); uint64_t* result_d = arena.alloc_limbs(nw + 2); std::memset(result_d, 0, (nw + 2) * sizeof(uint64_t)); if (wp_bits >= 1500) { // Paterson-Stockmeyer: O(√N) full-size multiplications // Threshold lowered 10000→1500 bits (N passes of naive Taylor vs 2√N of P-S) RawFloat result{result_d, 0, 0}; rf_atan_ps(result, x_rf, x2, nw, prod_buf, scratch, arena, working_precision); return rf_to_float(result, x.isNegative(), working_precision); } // Naive Taylor series (low precision) uint64_t* term_d = arena.alloc_limbs(nw + 2); std::memset(term_d, 0, (nw + 2) * sizeof(uint64_t)); std::memcpy(term_d, x_rf.d, x_rf.nw * sizeof(uint64_t)); RawFloat term{term_d, x_rf.nw, x_rf.exp}; std::memcpy(result_d, x_rf.d, x_rf.nw * sizeof(uint64_t)); RawFloat result{result_d, x_rf.nw, x_rf.exp}; int max_terms = static_cast(working_precision * 1.2) + 10; for (int k = 1; k <= max_terms; ++k) { // term *= x² rf_mul(term, term, x2, prod_buf, scratch, nw); // term *= (2k-1) — skip when k=1 since 2k-1=1 if (k > 1) { rf_mul_1(term, static_cast(2*k - 1)); } // term /= (2k+1) rf_divmod_1(term, static_cast(2*k + 1)); if (term.nw == 0) break; if (k & 1) { if (!rf_sub(result, term, nw + 1)) break; } else { if (!rf_add(result, term, nw + 1)) break; } } return rf_to_float(result, x.isNegative(), working_precision); } // atan computation via argument halving + Taylor // atan(x) = 2^K · atan(t), t = the argument halved K times // When using P-S, the Taylor part is O(√N), so reduce K substantially and // cut the number of costly halvings (each a sqrt+div). static Float atanHalving(const Float& x, int working_precision) { int wp_bits = Float::precisionToBits(working_precision); int K; if (wp_bits >= 1500) { // P-S enabled: halving cost (sqrt+div ≈ 4M(n)) vs P-S Taylor (2√N·M(n)) // Optimal K = cbrt(n/50) — limit halvings and let P-S handle the rest K = static_cast(std::cbrt(wp_bits / 50.0)); } else { K = static_cast(std::sqrt(wp_bits / 20.0)); } if (K < 1) return atanTaylor(x, working_precision); // Guard bits: for the error accumulation of K halvings int guard_bits = 2 * K + static_cast(std::ceil(std::log2(K + 1))) + 10; int wp_inner = working_precision + Float::bitsToPrecision(guard_bits); // K halvings: t = x / (1 + sqrt(1 + x²)) Float t = x; t.truncateToApprox(wp_inner); int wp_inner_bits = Float::precisionToBits(wp_inner); t.setEffectiveBits(wp_inner_bits); Float one_val = Float::one(wp_inner); for (int i = 0; i < K; ++i) { Float t2 = t * t; t2.truncateToApprox(wp_inner); t = t / (one_val + sqrt(one_val + t2, wp_inner)); t.truncateToApprox(wp_inner); } t.setEffectiveBits(wp_inner_bits); // Taylor series on the reduced argument Float result = atanTaylor(t, wp_inner); // Reconstruction: atan(x) = 2^K · atan(t) result = ldexp(result, K); result.truncateToApprox(working_precision); return result; } // double → Q128 conversion (places the 53-bit precision at the proper position in Q128) // Assumes val ∈ [0, 1). Returns hi=lo=0 for val = 0. static void double_to_q128(double val, uint64_t& hi, uint64_t& lo) { if (val == 0.0) { hi = lo = 0; return; } int exp; double m = std::frexp(val, &exp); // m ∈ [0.5, 1), val = m * 2^exp // m * 2^53 is a 53-bit integer uint64_t m_int = static_cast(m * static_cast(1ULL << 53)); // Q128: val * 2^128 = m_int * 2^(exp - 53 + 128) = m_int * 2^(exp + 75) int shift = exp + 75; if (shift >= 64) { hi = m_int << (shift - 64); lo = 0; } else if (shift >= 0) { hi = m_int >> (64 - shift); lo = m_int << shift; } else { hi = 0; lo = m_int >> (-shift); } } // Float → Q128 conversion (places the high 128 bits of the mantissa into Q128) // Assumes x ∈ (0, 1) static void float_to_q128(const Float& x, uint64_t& hi, uint64_t& lo) { const Int& mant = x.mantissa(); const uint64_t* mw = mant.data(); size_t mn = mant.size(); int64_t e = x.exponent(); uint64_t top = mw[mn - 1]; int top_bits = 64 - std::countl_zero(top); int64_t total_bl = static_cast((mn - 1) * 64) + top_bits; // Normalize the mantissa (place the MSB at bit 127) int clz = 64 - top_bits; uint64_t m_hi, m_lo; if (clz == 0) { m_hi = top; m_lo = (mn >= 2) ? mw[mn - 2] : 0; } else { m_hi = (top << clz) | ((mn >= 2) ? (mw[mn - 2] >> (64 - clz)) : 0); if (mn >= 2) { m_lo = (mw[mn - 2] << clz) | ((mn >= 3) ? (mw[mn - 3] >> (64 - clz)) : 0); } else { m_lo = 0; } } // x = mant * 2^e, mant ≈ m_hi:m_lo * 2^(total_bl - 128) // Q128 = x * 2^128 = m_hi:m_lo * 2^(e + total_bl) int64_t sr = -(e + total_bl); // Right-shift amount (sr > 0 since x < 1) if (sr <= 0) { // x >= 1: should not reach here hi = m_hi; lo = m_lo; } else if (sr < 64) { hi = (m_hi >> sr); lo = (m_hi << (64 - sr)) | (m_lo >> sr); } else if (sr < 128) { hi = 0; lo = m_hi >> (sr - 64); } else { hi = lo = 0; } } // Float → Q128 reciprocal conversion: convert 1/x to Q128 (x > 1) // Compute a ~130-bit-precision reciprocal from the high 64 bits of the mantissa via 3-stage _udiv128 static void float_recip_q128(const Float& x, uint64_t& hi, uint64_t& lo) { const Int& mant = x.mantissa(); const uint64_t* mw = mant.data(); size_t mn = mant.size(); int64_t e = x.exponent(); uint64_t top = mw[mn - 1]; int top_bits = 64 - std::countl_zero(top); int64_t total_bl = static_cast((mn - 1) * 64) + top_bits; int clz = 64 - top_bits; uint64_t m_norm; if (clz == 0) { m_norm = top; } else if (mn >= 2) { m_norm = (top << clz) | (mw[mn - 2] >> (64 - clz)); } else { m_norm = top << clz; } // 3-stage division: 2^192 / m_norm → (q1:q2:q3), ~130-bit precision uint64_t r1, r2, r3; uint64_t q1 = _udiv128(1, 0, m_norm, &r1); uint64_t q2 = _udiv128(r1, 0, m_norm, &r2); uint64_t q3 = _udiv128(r2, 0, m_norm, &r3); // Q128(1/x) = (q1:q2:q3) * 2^{-total_bl - e} // x > 1 → total_bl + e >= 1 int64_t sr = total_bl + e; if (sr > 0 && sr < 64) { hi = (q1 << (64 - sr)) | (q2 >> sr); lo = (q2 << (64 - sr)) | (q3 >> sr); } else if (sr == 0) { hi = q2; lo = q3; } else if (sr >= 64 && sr < 128) { int s2 = static_cast(sr - 64); hi = q2 >> s2; lo = (q2 << (64 - s2)) | (q3 >> s2); } else { hi = lo = 0; } } // Q128 atan fast path (≤ 64-bit precision) // Newton's method: y₁ = y₀ + (x·cos(y₀) - sin(y₀)) / (cos(y₀) + x·sin(y₀)) // y₀ = std::atan(x) (53-bit), one Newton step doubles to ~106-bit // Compute sin(y₀), vers(y₀) = 1 - cos(y₀) via Q128 Taylor + angle doubling static Float atan_small(const Float& x, int precision) { double x_double = x.toDouble(); bool negative = x_double < 0; if (negative) x_double = -x_double; // x = 0 (already handled by the caller, but just in case) if (x_double == 0.0) return Float::zero(precision); // |x| > 1: atan(x) = π/2 - atan(1/x) bool complemented = false; if (x_double > 1.0) { x_double = 1.0 / x_double; complemented = true; } else if (x_double == 1.0) { // atan(1) = π/4 Float result = ldexp(Float::pi(precision), -2); if (negative) result = -result; result.setResultPrecision(precision); return result; } // Convert x to Q128 (x ∈ (0, 1)) uint64_t x_hi, x_lo; if (complemented) { float_recip_q128(abs(x), x_hi, x_lo); } else { float_to_q128(abs(x), x_hi, x_lo); } // y₀ = atan(x_double), 53-bit initial approximation double y0 = std::atan(x_double); // === Q128 sincos: compute sin(y₀) and vers(y₀) = 1-cos(y₀) === // Argument reduction: y_red = y₀ / 2^K constexpr int K = 4; double y_red = y0 * 0.0625; // y0 / 16, |y_red| < π/64 ≈ 0.049 // y_red → Q128 uint64_t yr_hi, yr_lo; double_to_q128(y_red, yr_hi, yr_lo); // y² in Q128 uint64_t y2_hi, y2_lo; q128_mul(yr_hi, yr_lo, yr_hi, yr_lo, y2_hi, y2_lo); // sin(y_red) Taylor: y - y³/3! + y⁵/5! - y⁷/7! + ... uint64_t sin_hi = yr_hi, sin_lo = yr_lo; { uint64_t term_hi = yr_hi, term_lo = yr_lo; for (int k = 1; k <= 9; k++) { q128_mul(term_hi, term_lo, y2_hi, y2_lo, term_hi, term_lo); uint64_t divisor = static_cast(2 * k) * static_cast(2 * k + 1); uint64_t rem; term_hi = _udiv128(0, term_hi, divisor, &rem); term_lo = _udiv128(rem, term_lo, divisor, &rem); if (term_hi == 0 && term_lo == 0) break; if (k & 1) { // k=1: -y³/6, k=3: -y⁷/5040, ... unsigned char borrow = _subborrow_u64(0, sin_lo, term_lo, &sin_lo); _subborrow_u64(borrow, sin_hi, term_hi, &sin_hi); } else { // k=2: +y⁵/120, k=4: +y⁹/362880, ... unsigned char carry = _addcarry_u64(0, sin_lo, term_lo, &sin_lo); _addcarry_u64(carry, sin_hi, term_hi, &sin_hi); } } } // vers(y_red) Taylor: y²/2! - y⁴/4! + y⁶/6! - ... uint64_t vers_hi, vers_lo; { uint64_t rem; vers_hi = _udiv128(0, y2_hi, 2, &rem); vers_lo = _udiv128(rem, y2_lo, 2, &rem); } { uint64_t term_hi = vers_hi, term_lo = vers_lo; for (int k = 1; k <= 9; k++) { q128_mul(term_hi, term_lo, y2_hi, y2_lo, term_hi, term_lo); uint64_t divisor = static_cast(2 * k + 1) * static_cast(2 * k + 2); uint64_t rem; term_hi = _udiv128(0, term_hi, divisor, &rem); term_lo = _udiv128(rem, term_lo, divisor, &rem); if (term_hi == 0 && term_lo == 0) break; if (k & 1) { // k=1: -y⁴/24, k=3: -y⁸/40320, ... unsigned char borrow = _subborrow_u64(0, vers_lo, term_lo, &vers_lo); _subborrow_u64(borrow, vers_hi, term_hi, &vers_hi); } else { // k=2: +y⁶/720, k=4: +y^10/..., ... unsigned char carry = _addcarry_u64(0, vers_lo, term_lo, &vers_lo); _addcarry_u64(carry, vers_hi, term_hi, &vers_hi); } } } // K angle doublings: sin(2θ) = 2sin(θ)(1-vers(θ)), vers(2θ) = 2sin²(θ) for (int i = 0; i < K; i++) { // sin(2θ) = 2·sin·cos = 2·sin·(1 - vers) = 2(sin - sin·vers) uint64_t sv_hi, sv_lo; q128_mul(sin_hi, sin_lo, vers_hi, vers_lo, sv_hi, sv_lo); uint64_t diff_hi, diff_lo; unsigned char borrow = _subborrow_u64(0, sin_lo, sv_lo, &diff_lo); _subborrow_u64(borrow, sin_hi, sv_hi, &diff_hi); uint64_t sin_new_hi = (diff_hi << 1) | (diff_lo >> 63); uint64_t sin_new_lo = diff_lo << 1; // vers(2θ) = 2·sin²(θ) uint64_t ss_hi, ss_lo; q128_mul(sin_hi, sin_lo, sin_hi, sin_lo, ss_hi, ss_lo); uint64_t vers_new_hi = (ss_hi << 1) | (ss_lo >> 63); uint64_t vers_new_lo = ss_lo << 1; sin_hi = sin_new_hi; sin_lo = sin_new_lo; vers_hi = vers_new_hi; vers_lo = vers_new_lo; } // === Newton correction === // numerator = x·cos(y₀) - sin(y₀) = (x - sin) - x·vers uint64_t xv_hi, xv_lo; q128_mul(x_hi, x_lo, vers_hi, vers_lo, xv_hi, xv_lo); // x - sin (signed) bool xms_neg; uint64_t xms_hi, xms_lo; if (x_hi > sin_hi || (x_hi == sin_hi && x_lo >= sin_lo)) { xms_neg = false; unsigned char b = _subborrow_u64(0, x_lo, sin_lo, &xms_lo); _subborrow_u64(b, x_hi, sin_hi, &xms_hi); } else { xms_neg = true; unsigned char b = _subborrow_u64(0, sin_lo, x_lo, &xms_lo); _subborrow_u64(b, sin_hi, x_hi, &xms_hi); } // num = (x - sin) - x·vers bool num_neg; uint64_t num_hi, num_lo; if (!xms_neg) { if (xms_hi > xv_hi || (xms_hi == xv_hi && xms_lo >= xv_lo)) { num_neg = false; unsigned char b = _subborrow_u64(0, xms_lo, xv_lo, &num_lo); _subborrow_u64(b, xms_hi, xv_hi, &num_hi); } else { num_neg = true; unsigned char b = _subborrow_u64(0, xv_lo, xms_lo, &num_lo); _subborrow_u64(b, xv_hi, xms_hi, &num_hi); } } else { // (x - sin) < 0: num = -(|x-sin| + x·vers) < 0 num_neg = true; unsigned char c = _addcarry_u64(0, xms_lo, xv_lo, &num_lo); _addcarry_u64(c, xms_hi, xv_hi, &num_hi); } // denominator (double precision suffices): d = cos(y₀) + x·sin(y₀) ≈ sec(y₀) double sin_d = std::sin(y0); double cos_d = std::cos(y0); double denom = cos_d + x_double * sin_d; // δ = num / denom (Q128 × double reciprocal) // Decompose with frexp then 128×64 multiply + shift int d_exp; double d_m = std::frexp(1.0 / denom, &d_exp); // d_m ∈ [0.5, 1) uint64_t d_int = static_cast(d_m * static_cast(1ULL << 53)); // 128-bit × 64-bit → 192-bit uint64_t p1_hi; uint64_t p1_lo = _umul128(num_lo, d_int, &p1_hi); uint64_t p2_hi; uint64_t p2_lo = _umul128(num_hi, d_int, &p2_hi); unsigned char c = _addcarry_u64(0, p2_lo, p1_hi, &p2_lo); _addcarry_u64(c, p2_hi, 0, &p2_hi); // Right-shift by (53 - d_exp) to return to Q128 int shift = 53 - d_exp; uint64_t delta_hi, delta_lo; if (shift >= 64) { int s2 = shift - 64; if (s2 < 64) { delta_hi = p2_hi >> s2; delta_lo = (p2_hi << (64 - s2)) | (p2_lo >> s2); } else { delta_hi = 0; delta_lo = (s2 < 128) ? (p2_hi >> (s2 - 64)) : 0; } } else if (shift > 0) { delta_hi = (p2_hi << (64 - shift)) | (p2_lo >> shift); delta_lo = (p2_lo << (64 - shift)) | (p1_lo >> shift); } else { delta_hi = p2_lo; delta_lo = p1_lo; } // y₁ = y₀ ± δ uint64_t y0_hi, y0_lo; double_to_q128(y0, y0_hi, y0_lo); uint64_t r_hi, r_lo; if (!num_neg) { unsigned char carry = _addcarry_u64(0, y0_lo, delta_lo, &r_lo); _addcarry_u64(carry, y0_hi, delta_hi, &r_hi); } else { unsigned char borrow = _subborrow_u64(0, y0_lo, delta_lo, &r_lo); _subborrow_u64(borrow, y0_hi, delta_hi, &r_hi); } // === Generate the result based on precision === int precision_bits = Float::precisionToBits(precision); if (precision_bits <= 64) { // 19 digits: the Q128 result suffices (~106-bit precision) Float result = q128_to_float(r_hi, r_lo, -128); if (complemented) { Float pi_half = ldexp(Float::pi(precision), -1); result = pi_half - result; } if (negative) result = -result; result.setResultPrecision(precision); return result; } // === 38 digits (≤ 128 bit): second Q256 Newton stage === // y₁ (Q128, ~106 bit) → y₂ (Q256, ~212 bit) // Promote y₁ to Q256: Q128 (hi:lo) → Q256 (hi:lo:0:0) uint64_t y1_256[4] = { 0, 0, r_lo, r_hi }; // Convert x to Q256 (full precision) uint64_t x256[4]; if (complemented) { Float x_inv = Float::one(precision + 10) / abs(x); x_inv.truncateToApprox(precision + 10); float_to_q256(x_inv, x256); } else { float_to_q256(abs(x), x256); } // Q256 sincos(y₁): Taylor + angle doubling constexpr int K2 = 4; uint64_t yr2[4]; // y₁ / 2^K2 (right-shift K2 bits) yr2[0] = (y1_256[0] >> K2) | (y1_256[1] << (64 - K2)); yr2[1] = (y1_256[1] >> K2) | (y1_256[2] << (64 - K2)); yr2[2] = (y1_256[2] >> K2) | (y1_256[3] << (64 - K2)); yr2[3] = y1_256[3] >> K2; uint64_t y2sq[4]; q256_mul(yr2, yr2, y2sq); // sin Taylor uint64_t sin2[4] = { yr2[0], yr2[1], yr2[2], yr2[3] }; { uint64_t term[4] = { yr2[0], yr2[1], yr2[2], yr2[3] }; for (int k = 1; k <= 15; k++) { q256_mul(term, y2sq, term); q256_div_scalar(term, static_cast(2*k) * static_cast(2*k+1)); if (q256_is_zero(term)) break; if (k & 1) q256_sub(sin2, term, sin2); else q256_add(sin2, term, sin2); } } // vers Taylor uint64_t vers2[4] = { y2sq[0], y2sq[1], y2sq[2], y2sq[3] }; q256_div_scalar(vers2, 2); { uint64_t term[4] = { vers2[0], vers2[1], vers2[2], vers2[3] }; for (int k = 1; k <= 15; k++) { q256_mul(term, y2sq, term); q256_div_scalar(term, static_cast(2*k+1) * static_cast(2*k+2)); if (q256_is_zero(term)) break; if (k & 1) q256_sub(vers2, term, vers2); else q256_add(vers2, term, vers2); } } // K2 angle doublings (compute sin² first, then update sin) for (int i = 0; i < K2; i++) { uint64_t ss[4]; // sin²(θ) — computed from the pre-update sin q256_mul(sin2, sin2, ss); uint64_t sv[4]; // sin(θ)·vers(θ) q256_mul(sin2, vers2, sv); // sin(2θ) = 2(sin - sin·vers) uint64_t diff[4]; q256_sub(sin2, sv, diff); q256_shl1(diff, sin2); // vers(2θ) = 2·sin²(θ) q256_shl1(ss, vers2); } // Newton correction (Q256): num = (x - sin) - x·vers uint64_t xv2[4]; q256_mul(x256, vers2, xv2); bool num2_neg; uint64_t num2[4]; { uint64_t xms[4]; bool xms_neg2 = !q256_ge(x256, sin2); if (!xms_neg2) { q256_sub(x256, sin2, xms); } else { q256_sub(sin2, x256, xms); } if (!xms_neg2) { if (q256_ge(xms, xv2)) { num2_neg = false; q256_sub(xms, xv2, num2); } else { num2_neg = true; q256_sub(xv2, xms, num2); } } else { num2_neg = true; q256_add(xms, xv2, num2); } } // denominator (double precision): the double approximation of y₁ suffices [[maybe_unused]] double y1_d = static_cast(r_hi) * 5.421010862427522e-20; // * 2^{-64} // y₁ ≈ (r_hi:r_lo) * 2^{-128}, in double the leading term is r_hi * 2^{-64} // More precisely: y₁ ∈ [0, π/4); it is simpler to use y₀ double sin2_d = std::sin(y0); // y₁ ≈ y₀ (53-bit agreement) double cos2_d = std::cos(y0); double denom2 = cos2_d + x_double * sin2_d; // δ₂ = num2 / denom2 (Q256 × double reciprocal) int d2_exp; double d2_m = std::frexp(1.0 / denom2, &d2_exp); uint64_t d2_int = static_cast(d2_m * static_cast(1ULL << 53)); // 256-bit × 64-bit → 320-bit, take the high 256 bits uint64_t prod[5] = {}; for (int i = 0; i < 4; i++) { uint64_t hi; uint64_t lo = _umul128(num2[i], d2_int, &hi); unsigned char c1 = _addcarry_u64(0, prod[i], lo, &prod[i]); unsigned char c2 = _addcarry_u64(0, prod[i + 1], hi, &prod[i + 1]); if (c1) { unsigned char c3 = _addcarry_u64(0, prod[i + 1], 1, &prod[i + 1]); if (c3 && i + 2 < 5) prod[i + 2]++; } if (c2 && i + 2 < 5) prod[i + 2]++; } // Right-shift by (53 - d2_exp) to return to Q256 int shift2 = 53 - d2_exp; uint64_t delta2[4]; if (shift2 >= 64) { int s = shift2 - 64; if (s < 64) { delta2[0] = (prod[1] >> s) | (prod[2] << (64 - s)); delta2[1] = (prod[2] >> s) | (prod[3] << (64 - s)); delta2[2] = (prod[3] >> s) | (prod[4] << (64 - s)); delta2[3] = prod[4] >> s; } else { delta2[0] = delta2[1] = delta2[2] = delta2[3] = 0; } } else if (shift2 > 0) { delta2[0] = (prod[0] >> shift2) | (prod[1] << (64 - shift2)); delta2[1] = (prod[1] >> shift2) | (prod[2] << (64 - shift2)); delta2[2] = (prod[2] >> shift2) | (prod[3] << (64 - shift2)); delta2[3] = (prod[3] >> shift2) | (prod[4] << (64 - shift2)); } else { delta2[0] = prod[0]; delta2[1] = prod[1]; delta2[2] = prod[2]; delta2[3] = prod[3]; } // y₂ = y₁ ± δ₂ uint64_t res256[4]; if (!num2_neg) { q256_add(y1_256, delta2, res256); } else { q256_sub(y1_256, delta2, res256); } Float result = q256_to_float(res256, -256); if (complemented) { Float pi_half = ldexp(Float::pi(precision), -1); result = pi_half - result; } if (negative) result = -result; result.setResultPrecision(precision); return result; } static Float atan_core(Float x, int eff_x, int precision) { int compute_prec = effectiveComputePrecision(eff_x, precision); int working_precision = compute_prec + 10; bool negative = x.isNegative(); x = abs(x); // |x| > 1: reduce to |x| < 1 via atan(x) = π/2 − atan(1/x) bool complemented = false; Float one_wp = Float::one(working_precision); if (x > one_wp) { x = one_wp / x; x.truncateToApprox(working_precision); complemented = true; } Float result = atanHalving(x, working_precision); if (complemented) { result = ldexp(Float::pi(working_precision), -1) - result; } if (negative) result = -result; finalizeResult(result, eff_x, precision); return result; } Float atan(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) { Float pi_half = ldexp(Float::pi(precision), -1); return x.isNegative() ? -pi_half : pi_half; } if (x.isZero()) return Float::zero(precision); int precision_bits = Float::precisionToBits(precision); if (precision_bits <= 128) { return atan_small(x, precision); } return zivRound([](const Float& a, int e, int p) { return atan_core(Float(a), e, p); }, x, x.effectiveBits(), precision); } Float atan(Float&& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) { Float pi_half = ldexp(Float::pi(precision), -1); return x.isNegative() ? -pi_half : pi_half; } if (x.isZero()) return Float::zero(precision); int precision_bits = Float::precisionToBits(precision); if (precision_bits <= 128) { return atan_small(x, precision); } return zivRound([](const Float& a, int e, int p) { return atan_core(Float(a), e, p); }, x, x.effectiveBits(), precision); } //============================================================================= // Inverse sine (asin) //============================================================================= // // Algorithm: // asin(x) = atan(x / sqrt(1 - x²)) // When |x| = 1: asin(±1) = ±π/2 // // This identity reduces the computation of asin to atan. // Since atan is computed efficiently via the AGM method, asin is also high-precision. // // Reference: high-precision computation programs (sangi), Brent 1976 // // asin Q128 fast path: lightweight Float path → atan_small static Float asin_small(const Float& x, int precision) { // asin(x) = atan(x / sqrt(1 - x²)) int wp = precision + 2; Float x2 = x * x; Float u = Float::one(wp) - std::move(x2); Float sq = sqrt(std::move(u), wp); Float arg = x / sq; Float result = atan(std::move(arg), precision); result.setResultPrecision(precision); return result; } // asin 38-digit fast path: lightweight Float path static Float asin_medium(const Float& x, int precision) { int wp = precision + 4; Float x2 = x * x; Float u = Float::one(wp) - std::move(x2); Float sq = sqrt(std::move(u), wp); Float arg = x / sq; Float result = atan(std::move(arg), precision); result.setResultPrecision(precision); return result; } Float asin(const Float& x, int precision) { // Handle special values if (x.isNaN()) return Float::nan(); if (x.isInfinity()) return Float::nan(); // out of domain if (x.isZero()) return Float::zero(precision); // |x| > 1 is out of domain Float one_p = Float::one(precision); Float abs_x = abs(x); if (abs_x > one_p) { return Float::nan(); } // When |x| = 1: asin(±1) = ±π/2 if (abs_x == one_p) { Float pi_half = Float::pi(precision); pi_half >>= 1; return x.isNegative() ? -pi_half : pi_half; } int precision_bits = Float::precisionToBits(precision); if (x.mantissa().size() <= 1 && precision_bits <= 64) return asin_small(x, precision); if (x.mantissa().size() <= 2 && precision_bits <= 128) return asin_medium(x, precision); return zivRound([](const Float& a, int e, int p) { int wp = p + 4; Float X = a; X.truncateToApprox(wp); X.setEffectiveBits(Float::precisionToBits(wp)); Float u = Float::one(wp) - X * X; u.truncateToApprox(wp); Float sq = sqrt(u, wp); Float arg = X / sq; arg.truncateToApprox(wp); return atan(std::move(arg), wp); }, x, x.effectiveBits(), precision); } Float asin(Float&& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) return Float::nan(); if (x.isZero()) return Float::zero(precision); Float one_p = Float::one(precision); Float abs_x = abs(x); if (abs_x > one_p) return Float::nan(); if (abs_x == one_p) { Float pi_half = Float::pi(precision); pi_half >>= 1; return x.isNegative() ? -pi_half : pi_half; } int precision_bits = Float::precisionToBits(precision); if (x.mantissa().size() <= 1 && precision_bits <= 64) return asin_small(x, precision); if (x.mantissa().size() <= 2 && precision_bits <= 128) return asin_medium(x, precision); return zivRound([](const Float& a, int e, int p) { int wp = p + 4; Float X = a; X.truncateToApprox(wp); X.setEffectiveBits(Float::precisionToBits(wp)); Float u = Float::one(wp) - X * X; u.truncateToApprox(wp); Float sq = sqrt(u, wp); Float arg = X / sq; arg.truncateToApprox(wp); return atan(std::move(arg), wp); }, x, x.effectiveBits(), precision); } //============================================================================= // Inverse cosine (acos) //============================================================================= // // Algorithm: // acos(x) = π/2 - asin(x) // // The simplest and most accurate implementation. If asin is high-precision, acos is too. // Float acos(const Float& x, int precision) { // Handle special values if (x.isNaN()) return Float::nan(); if (x.isInfinity()) return Float::nan(); // out of domain if (x.isZero()) { Float result = Float::pi(precision); result >>= 1; return result; } // 19-digit fast path: branch before the heavy Float comparison int precision_bits = Float::precisionToBits(precision); if (x.mantissa().size() <= 1 && precision_bits <= 64) { // |x| > 1 check (double suffices) double x_d = std::fabs(x.toDouble()); if (x_d > 1.0) return Float::nan(); if (x_d == 1.0) { return x.isNegative() ? Float::pi(precision) : Float::zero(precision); } Float asin_x = asin_small(x, precision); Float pi_half = Float::pi(precision); pi_half >>= 1; Float result = std::move(pi_half) - std::move(asin_x); result.setResultPrecision(precision); return result; } // 38-digit fast path if (x.mantissa().size() <= 2 && precision_bits <= 128) { double x_d = std::fabs(x.toDouble()); if (x_d > 1.0) return Float::nan(); if (x_d == 1.0) { return x.isNegative() ? Float::pi(precision) : Float::zero(precision); } Float asin_x = asin_medium(x, precision); Float pi_half = Float::pi(precision); pi_half >>= 1; Float result = std::move(pi_half) - std::move(asin_x); result.setResultPrecision(precision); return result; } // |x| > 1 is out of domain Float one_p = Float::one(precision); Float abs_x = abs(x); if (abs_x > one_p) { return Float::nan(); } // acos(1) = 0, acos(-1) = π if (x == one_p) return Float::zero(precision); if (x == -one_p) return Float::pi(precision); return zivRound([](const Float& a, int e, int p) { int wp = p + 4; Float pi_half = Float::pi(wp); pi_half >>= 1; Float asin_x = asin(a, wp); return pi_half - asin_x; }, x, x.effectiveBits(), precision); } Float acos(Float&& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) return Float::nan(); if (x.isZero()) { Float result = Float::pi(precision); result >>= 1; return result; } int precision_bits = Float::precisionToBits(precision); if (x.mantissa().size() <= 1 && precision_bits <= 64) { double x_d = std::fabs(x.toDouble()); if (x_d > 1.0) return Float::nan(); if (x_d == 1.0) { return x.isNegative() ? Float::pi(precision) : Float::zero(precision); } Float asin_x = asin_small(x, precision); Float pi_half = Float::pi(precision); pi_half >>= 1; Float result = std::move(pi_half) - std::move(asin_x); result.setResultPrecision(precision); return result; } if (x.mantissa().size() <= 2 && precision_bits <= 128) { double x_d = std::fabs(x.toDouble()); if (x_d > 1.0) return Float::nan(); if (x_d == 1.0) { return x.isNegative() ? Float::pi(precision) : Float::zero(precision); } Float asin_x = asin_medium(x, precision); Float pi_half = Float::pi(precision); pi_half >>= 1; Float result = std::move(pi_half) - std::move(asin_x); result.setResultPrecision(precision); return result; } Float one_p = Float::one(precision); Float abs_x = abs(x); if (abs_x > one_p) return Float::nan(); if (x == one_p) return Float::zero(precision); if (x == -one_p) return Float::pi(precision); return zivRound([](const Float& a, int e, int p) { int wp = p + 4; Float pi_half = Float::pi(wp); pi_half >>= 1; Float asin_x = asin(a, wp); return pi_half - asin_x; }, x, x.effectiveBits(), precision); } //============================================================================= // Two-argument inverse tangent (atan2) //============================================================================= // // atan2(y, x) returns the argument of the point (x, y) in the range (-π, π]. // Case analysis by quadrant reduces it to atan(y/x). // // x > 0: atan(y/x) // x < 0, y >= 0: atan(y/x) + π // x < 0, y < 0: atan(y/x) - π // x = 0, y > 0: +π/2 // x = 0, y < 0: -π/2 // x = 0, y = 0: 0 (IEEE 754 compliant; sign-based subdivision omitted) // Float atan2(const Float& y, const Float& x, int precision) { // Handle special values if (x.isNaN() || y.isNaN()) return Float::nan(); int eff = std::min(x.effectiveBits(), y.effectiveBits()); int wp = precision + 4; // Case x = 0 if (x.isZero()) { if (y.isZero()) { return Float::zero(precision); } else if (y.isNegative()) { Float result = -ldexp(Float::pi(wp), -1); result.setResultPrecision(precision); return result; } else { Float result = ldexp(Float::pi(wp), -1); result.setResultPrecision(precision); return result; } } // Case y = 0 if (y.isZero()) { if (x.isNegative()) { return Float::pi(precision); // atan2(0, -x) = π } else { return Float::zero(precision); // atan2(0, +x) = 0 } } // Handle infinities if (x.isInfinity() && y.isInfinity()) { // Case both are infinite Float pi_quarter = ldexp(Float::pi(wp), -2); Float result; if (x.isNegative()) { result = y.isNegative() ? -(mulScalarF(pi_quarter, uint64_t(3))) : (mulScalarF(pi_quarter, uint64_t(3))); } else { result = y.isNegative() ? -pi_quarter : pi_quarter; } result.setResultPrecision(precision); return result; } if (x.isInfinity()) { // x = ±∞, y is finite if (x.isNegative()) { Float result = y.isNegative() ? -Float::pi(wp) : Float::pi(wp); result.setResultPrecision(precision); return result; } else { return Float::zero(precision); } } if (y.isInfinity()) { // y = ±∞, x is finite Float pi_half = ldexp(Float::pi(wp), -1); Float result = y.isNegative() ? -pi_half : pi_half; result.setResultPrecision(precision); return result; } // Normal case: quadrant determination + atan(y/x) Float Y = y; Y.truncateToApprox(wp); Float X = x; X.truncateToApprox(wp); Float ratio = Y / X; ratio.truncateToApprox(wp); Float base = atan(ratio, wp); if (x.isNegative()) { Float pi_wp = Float::pi(wp); if (y.isNegative()) { base = base - pi_wp; // Third quadrant } else { base = base + pi_wp; // Second quadrant } } finalizeResult(base, eff, precision); return base; } Float atan2(Float&& y, Float&& x, int precision) { // Handle special values if (x.isNaN() || y.isNaN()) return Float::nan(); int eff = std::min(x.effectiveBits(), y.effectiveBits()); int wp = precision + 4; // Case x = 0 if (x.isZero()) { if (y.isZero()) { return Float::zero(precision); } else if (y.isNegative()) { Float result = -ldexp(Float::pi(wp), -1); result.setResultPrecision(precision); return result; } else { Float result = ldexp(Float::pi(wp), -1); result.setResultPrecision(precision); return result; } } // Case y = 0 if (y.isZero()) { if (x.isNegative()) { return Float::pi(precision); // atan2(0, -x) = pi } else { return Float::zero(precision); // atan2(0, +x) = 0 } } // Handle infinities if (x.isInfinity() && y.isInfinity()) { // Case both are infinite Float pi_quarter = ldexp(Float::pi(wp), -2); Float result; if (x.isNegative()) { result = y.isNegative() ? -(mulScalarF(pi_quarter, uint64_t(3))) : (mulScalarF(pi_quarter, uint64_t(3))); } else { result = y.isNegative() ? -pi_quarter : pi_quarter; } result.setResultPrecision(precision); return result; } if (x.isInfinity()) { // x = +/-inf, y is finite if (x.isNegative()) { Float result = y.isNegative() ? -Float::pi(wp) : Float::pi(wp); result.setResultPrecision(precision); return result; } else { return Float::zero(precision); } } if (y.isInfinity()) { // y = +/-inf, x is finite Float pi_half = ldexp(Float::pi(wp), -1); Float result = y.isNegative() ? -pi_half : pi_half; result.setResultPrecision(precision); return result; } // Normal case: quadrant determination + atan(y/x) bool x_neg = x.isNegative(); bool y_neg = y.isNegative(); y.truncateToApprox(wp); x.truncateToApprox(wp); Float ratio = std::move(y) / std::move(x); ratio.truncateToApprox(wp); Float base = atan(std::move(ratio), wp); if (x_neg) { Float pi_wp = Float::pi(wp); if (y_neg) { base = base - pi_wp; // Third quadrant } else { base = base + pi_wp; // Second quadrant } } finalizeResult(base, eff, precision); return base; } //============================================================================= // Inverse hyperbolic functions //============================================================================= Float asinh(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) return x; if (x.isZero()) return Float(0); return zivRound([](const Float& a, int e, int p) { int wp = p + 10; Float X = a; X.truncateToApprox(wp); Float x2 = X * X; x2.truncateToApprox(wp); Float inner = x2 + Float::one(wp); inner.truncateToApprox(wp); return log(X + sqrt(inner, wp), wp); }, x, x.effectiveBits(), precision); } Float asinh(Float&& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) return x; if (x.isZero()) return Float(0); return zivRound([](const Float& a, int e, int p) { int wp = p + 10; Float X = a; X.truncateToApprox(wp); Float x2 = X * X; x2.truncateToApprox(wp); Float inner = x2 + Float::one(wp); inner.truncateToApprox(wp); return log(X + sqrt(inner, wp), wp); }, x, x.effectiveBits(), precision); } Float acosh(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) { if (x.isNegative()) return Float::nan(); return Float::positiveInfinity(); } Float one_val = Float::one(precision); if (x < one_val) return Float::nan(); if (x == one_val) return Float(0); return zivRound([](const Float& a, int e, int p) { int wp = p + 10; Float X = a; X.truncateToApprox(wp); Float x2 = X * X; x2.truncateToApprox(wp); Float inner = x2 - Float::one(wp); inner.truncateToApprox(wp); return log(X + sqrt(inner, wp), wp); }, x, x.effectiveBits(), precision); } Float acosh(Float&& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) { if (x.isNegative()) return Float::nan(); return Float::positiveInfinity(); } Float one_val = Float::one(precision); if (x < one_val) return Float::nan(); if (x == one_val) return Float(0); return zivRound([](const Float& a, int e, int p) { int wp = p + 10; Float X = a; X.truncateToApprox(wp); Float x2 = X * X; x2.truncateToApprox(wp); Float inner = x2 - Float::one(wp); inner.truncateToApprox(wp); return log(X + sqrt(inner, wp), wp); }, x, x.effectiveBits(), precision); } Float atanh(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isZero()) return Float(0); Float one_val = Float::one(precision); Float abs_x = abs(x); if (abs_x == one_val) { return x.isNegative() ? Float::negativeInfinity() : Float::positiveInfinity(); } if (abs_x > one_val) return Float::nan(); return zivRound([](const Float& a, int e, int p) { int wp = p + 10; Float X = a; X.truncateToApprox(wp); Float one_wp = Float::one(wp); Float num = one_wp + X; num.truncateToApprox(wp); Float den = one_wp - X; den.truncateToApprox(wp); Float ratio = num / den; ratio.truncateToApprox(wp); Float half(Int(1), -1, false); return half * log(ratio, wp); }, x, x.effectiveBits(), precision); } Float atanh(Float&& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isZero()) return Float(0); Float one_val = Float::one(precision); Float abs_x = abs(x); if (abs_x == one_val) { return x.isNegative() ? Float::negativeInfinity() : Float::positiveInfinity(); } if (abs_x > one_val) return Float::nan(); return zivRound([](const Float& a, int e, int p) { int wp = p + 10; Float X = a; X.truncateToApprox(wp); Float one_wp = Float::one(wp); Float num = one_wp + X; num.truncateToApprox(wp); Float den = one_wp - X; den.truncateToApprox(wp); Float ratio = num / den; ratio.truncateToApprox(wp); Float half(Int(1), -1, false); return half * log(ratio, wp); }, x, x.effectiveBits(), precision); } //============================================================================= // log1p / exp2 / exp10 / expm1 //============================================================================= static Float log2_dispatch(const Float& x, int eff_x, int precision) { int precision_bits = Float::precisionToBits(precision); if (precision_bits <= 64) return log2_small(x, precision); if (precision_bits <= 128) return log2_medium(x, precision); if (precision_bits <= 2000) return log2_newton(Float(x), eff_x, precision); int wp = precision + 10; return log(x, wp) / Float::log2(wp); } Float log2(const Float& x, int precision) { if (x.isNaN() || (x.isNegative() && !x.isZero())) return Float::nan(); if (x.isZero()) return Float::negativeInfinity(); if (x.isInfinity() && !x.isNegative()) return Float::positiveInfinity(); if (x == Float::one()) return Float::zero(); return zivRound(log2_dispatch, x, x.effectiveBits(), precision); } Float log2(Float&& x, int precision) { if (x.isNaN() || (x.isNegative() && !x.isZero())) return Float::nan(); if (x.isZero()) return Float::negativeInfinity(); if (x.isInfinity() && !x.isNegative()) return Float::positiveInfinity(); if (x == Float::one()) return Float::zero(); return zivRound(log2_dispatch, x, x.effectiveBits(), precision); } static Float log10_dispatch(const Float& x, int eff_x, int precision) { int precision_bits = Float::precisionToBits(precision); if (precision_bits <= 64) return log10_small(x, precision); if (precision_bits <= 128) return log10_medium(x, precision); if (precision_bits <= 2000) return log10_newton(Float(x), eff_x, precision); int wp = precision + 10; return log(x, wp) / Float::log10(wp); } Float log10(const Float& x, int precision) { if (x.isNaN() || (x.isNegative() && !x.isZero())) return Float::nan(); if (x.isZero()) return Float::negativeInfinity(); if (x.isInfinity() && !x.isNegative()) return Float::positiveInfinity(); if (x == Float::one()) return Float::zero(); return zivRound(log10_dispatch, x, x.effectiveBits(), precision); } Float log10(Float&& x, int precision) { if (x.isNaN() || (x.isNegative() && !x.isZero())) return Float::nan(); if (x.isZero()) return Float::negativeInfinity(); if (x.isInfinity() && !x.isNegative()) return Float::positiveInfinity(); if (x == Float::one()) return Float::zero(); return zivRound(log10_dispatch, x, x.effectiveBits(), precision); } Float log1p(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isZero()) return Float(0); Float neg_one(Int(1), 0, true); if (x == neg_one) return Float::negativeInfinity(); if (x < neg_one) return Float::nan(); if (x.isInfinity() && !x.isNegative()) return Float::positiveInfinity(); if (x.isInfinity() && x.isNegative()) return Float::nan(); return zivRound([](const Float& a, int e, int p) { int wp = p + 10; return log(Float::one(wp) + a, wp); }, x, x.effectiveBits(), precision); } Float log1p(Float&& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isZero()) return Float(0); Float neg_one(Int(1), 0, true); if (x == neg_one) return Float::negativeInfinity(); if (x < neg_one) return Float::nan(); if (x.isInfinity() && !x.isNegative()) return Float::positiveInfinity(); if (x.isInfinity() && x.isNegative()) return Float::nan(); return zivRound([](const Float& a, int e, int p) { int wp = p + 10; return log(Float::one(wp) + a, wp); }, x, x.effectiveBits(), precision); } Float exp2(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) { if (x.isNegative()) return Float(0); return Float::positiveInfinity(); } if (x.isZero()) return Float::one(precision); return zivRound([](const Float& a, int e, int p) { int wp = p + 10; return exp(a * Float::log2(wp), wp); }, x, x.effectiveBits(), precision); } Float exp2(Float&& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) { if (x.isNegative()) return Float(0); return Float::positiveInfinity(); } if (x.isZero()) return Float::one(precision); return zivRound([](const Float& a, int e, int p) { int wp = p + 10; return exp(a * Float::log2(wp), wp); }, x, x.effectiveBits(), precision); } Float exp10(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) { if (x.isNegative()) return Float(0); return Float::positiveInfinity(); } if (x.isZero()) return Float::one(precision); return zivRound([](const Float& a, int e, int p) { int wp = p + 10; return exp(a * Float::log10(wp), wp); }, x, x.effectiveBits(), precision); } Float exp10(Float&& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) { if (x.isNegative()) return Float(0); return Float::positiveInfinity(); } if (x.isZero()) return Float::one(precision); return zivRound([](const Float& a, int e, int p) { int wp = p + 10; return exp(a * Float::log10(wp), wp); }, x, x.effectiveBits(), precision); } Float expm1(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isZero()) return Float(0); if (x.isInfinity()) { if (x.isNegative()) return Float(-1); return Float::positiveInfinity(); } return zivRound([](const Float& a, int e, int p) { int wp = p + 10; return exp(a, wp) - Float::one(wp); }, x, x.effectiveBits(), precision); } Float expm1(Float&& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isZero()) return Float(0); if (x.isInfinity()) { if (x.isNegative()) return Float(-1); return Float::positiveInfinity(); } return zivRound([](const Float& a, int e, int p) { int wp = p + 10; return exp(a, wp) - Float::one(wp); }, x, x.effectiveBits(), precision); } //============================================================================= // exp2m1 / exp10m1 — 2^x - 1, 10^x - 1 (precision-preserving for small x) //============================================================================= Float exp2m1(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isZero()) return Float(0); if (x.isInfinity()) { if (x.isNegative()) return Float(-1); return Float::positiveInfinity(); } // exp2m1(x) = expm1(x * ln2) // For small x, x*ln2 is also small so expm1 preserves precision // For large x, exp2(x) - 1 has no cancellation issue return zivRound([](const Float& a, int e, int p) { int wp = p + 10; double ad = a.toDouble(); if (std::fabs(ad) < 10.0) { // Small x: expm1(x * ln2) preserves precision Float arg = a * Float::log2(wp); arg.truncateToApprox(wp); return expm1(arg, wp); } // Large x: exp2(x) - 1 (no cancellation) return exp2(a, wp) - Float::one(wp); }, x, x.effectiveBits(), precision); } Float exp2m1(Float&& x, int precision) { return exp2m1(static_cast(x), precision); } Float exp10m1(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isZero()) return Float(0); if (x.isInfinity()) { if (x.isNegative()) return Float(-1); return Float::positiveInfinity(); } // exp10m1(x) = expm1(x * ln10) return zivRound([](const Float& a, int e, int p) { int wp = p + 10; double ad = a.toDouble(); if (std::fabs(ad) < 10.0) { Float arg = a * Float::log10(wp); arg.truncateToApprox(wp); return expm1(arg, wp); } return exp10(a, wp) - Float::one(wp); }, x, x.effectiveBits(), precision); } Float exp10m1(Float&& x, int precision) { return exp10m1(static_cast(x), precision); } //============================================================================= // log2p1 / log10p1 / compound / minPrec //============================================================================= Float log2p1(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isZero()) return Float(0); Float neg_one(Int(1), 0, true); if (x == neg_one) return Float::negativeInfinity(); if (x < neg_one) return Float::nan(); if (x.isInfinity() && !x.isNegative()) return Float::positiveInfinity(); if (x == Float::one()) return Float::one(); // log2(1+1) = log2(2) = 1 return zivRound([](const Float& a, int e, int p) { int wp = p + 10; return log1p(a, wp) / Float::log2(wp); }, x, x.effectiveBits(), precision); } Float log2p1(Float&& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isZero()) return Float(0); Float neg_one(Int(1), 0, true); if (x == neg_one) return Float::negativeInfinity(); if (x < neg_one) return Float::nan(); if (x.isInfinity() && !x.isNegative()) return Float::positiveInfinity(); if (x == Float::one()) return Float::one(); // log2(1+1) = log2(2) = 1 return zivRound([](const Float& a, int e, int p) { int wp = p + 10; return log1p(a, wp) / Float::log2(wp); }, x, x.effectiveBits(), precision); } Float log10p1(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isZero()) return Float(0); Float neg_one(Int(1), 0, true); if (x == neg_one) return Float::negativeInfinity(); if (x < neg_one) return Float::nan(); if (x.isInfinity() && !x.isNegative()) return Float::positiveInfinity(); if (x == Float(9)) return Float::one(); // log10(1+9) = log10(10) = 1 return zivRound([](const Float& a, int e, int p) { int wp = p + 10; return log1p(a, wp) / Float::log10(wp); }, x, x.effectiveBits(), precision); } Float log10p1(Float&& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isZero()) return Float(0); Float neg_one(Int(1), 0, true); if (x == neg_one) return Float::negativeInfinity(); if (x < neg_one) return Float::nan(); if (x.isInfinity() && !x.isNegative()) return Float::positiveInfinity(); if (x == Float(9)) return Float::one(); // log10(1+9) = log10(10) = 1 return zivRound([](const Float& a, int e, int p) { int wp = p + 10; return log1p(a, wp) / Float::log10(wp); }, x, x.effectiveBits(), precision); } Float compound(const Float& x, int n, int precision) { if (x.isNaN()) return Float::nan(); Float neg_one(Int(1), 0, true); if (x < neg_one) return Float::nan(); if (n == 0) return Float::one(precision); if (x.isZero()) return Float::one(precision); if (x == neg_one) { if (n > 0) return Float(0); return Float::positiveInfinity(); // (1+(-1))^(negative) = 0^(negative) = +∞ } int wp = precision + 10; return pow(Float::one(wp) + x, n, precision); } Float compound(Float&& x, int n, int precision) { if (x.isNaN()) return Float::nan(); Float neg_one(Int(1), 0, true); if (x < neg_one) return Float::nan(); if (n == 0) return Float::one(precision); if (x.isZero()) return Float::one(precision); if (x == neg_one) { if (n > 0) return Float(0); return Float::positiveInfinity(); } int wp = precision + 10; return pow(Float::one(wp) + std::move(x), n, precision); } int Float::minPrec() const { if (isZero() || isNaN() || isInfinity()) return 0; int bl = static_cast(mantissa_.bitLength()); int tz = static_cast(mantissa_.countTrailingZeros()); return bl - tz; } //============================================================================= // fmod / remainder //============================================================================= namespace { // Integer-remainder-based common fmod implementation // Method: writing x = mx * 2^ex, y = my * 2^ey, // align to the common exponent e = min(ex, ey), // ax = mx << (ex - e), ay = my << (ey - e), // r = ax mod ay, // result = sign(x) * (r * 2^e) // Completely avoids Float division (full precision), using a single Int remainder. // // For huge exponent diffs (shifting ax is impractical), fall back to the old method. // Threshold: fall back if the extra limbs after shifting exceed 4× my. // (codex advice: switch at extra_limbs > 2 * my.size(); here we use 4× to be safe) inline Float fmod_integer_impl(const Float& x, const Float& y) { int64_t ex = x.exponent(); int64_t ey = y.exponent(); const Int& mx = x.mantissa(); const Int& my = y.mantissa(); // Magnitude comparison: if |x| < |y|, the result is x // mag = bitLength(m) + exponent (top bit position) int64_t mag_x = static_cast(mx.bitLength()) + ex; int64_t mag_y = static_cast(my.bitLength()) + ey; if (mag_x < mag_y) { return x; // |x| < |y|, the remainder is x } int64_t e_common = std::min(ex, ey); int64_t shift_x = ex - e_common; // >= 0 int64_t shift_y = ey - e_common; // >= 0 // Huge-shift fallback: old Float division (at least correct, slow) // shift_y is usually 0 (one of them matches e_common) constexpr int64_t MAX_SHIFT_LIMBS = 64 * 64; // 4096 bits = about 64 limbs of headroom int64_t bigger_shift = std::max(shift_x, shift_y); int64_t my_bits = static_cast(my.bitLength()); if (bigger_shift > my_bits + MAX_SHIFT_LIMBS) { // TODO: avoid memory explosion by dropping to a bit-by-bit-style reduction loop // For now, revert to the old method Float quotient = trunc(x / y); Float result = x - quotient * y; // Sign adjustment: fmod has the same sign as sign(x) if (!result.isZero()) { if (result.isNegative() != x.isNegative()) { result = -std::move(result); } } return result; } // Normal path: expand to Int and take the remainder Int ax = (shift_x > 0) ? (mx << static_cast(shift_x)) : mx; Int ay = (shift_y > 0) ? (my << static_cast(shift_y)) : my; Int r = ax % ay; if (r.isZero()) { // Result zero: the sign is sign(x) (IEEE spec fmod(±0, y) is ±0, but // the current Float::normalize() collapses the sign of zero, so return only 0. // Consistent with other existing functions) return Float(0); } return Float(r, e_common, x.isNegative()); } // Integer-remainder implementation of IEEE 754 remainder // After obtaining fmod's remainder r_unsigned (0 <= r_unsigned < ay), // apply the round-half-even correction: // 2*r_unsigned < ay → r_signed = r_unsigned (q unchanged) // 2*r_unsigned > ay → r_signed = r_unsigned - ay (q+=1, result is negative) // 2*r_unsigned = ay → round q to even (banker's) // The result's sign is sign(x) ⊕ (whether the correction flipped the sign) inline Float remainder_integer_impl(const Float& x, const Float& y) { int64_t ex = x.exponent(); int64_t ey = y.exponent(); const Int& mx = x.mantissa(); const Int& my = y.mantissa(); // If |x| is less than half of |y|, the result is x (no correction needed) int64_t mag_x = static_cast(mx.bitLength()) + ex; int64_t mag_y = static_cast(my.bitLength()) + ey; // Sufficient condition for |x| << |y|/2: mag_x + 1 < mag_y // (thinly conservative since the mantissa is non-canonical) if (mag_x + 1 < mag_y) { return x; } int64_t e_common = std::min(ex, ey); int64_t shift_x = ex - e_common; int64_t shift_y = ey - e_common; constexpr int64_t MAX_SHIFT_LIMBS = 64 * 64; int64_t bigger_shift = std::max(shift_x, shift_y); int64_t my_bits_l = static_cast(my.bitLength()); if (bigger_shift > my_bits_l + MAX_SHIFT_LIMBS) { // Old-method fallback (huge exp_diff) Float quotient = roundEven(x / y); return x - quotient * y; } Int ax = (shift_x > 0) ? (mx << static_cast(shift_x)) : mx; Int ay = (shift_y > 0) ? (my << static_cast(shift_y)) : my; // ax = q * ay + r (0 <= r < ay); take only r, not q Int r = ax % ay; // round-half-even: compare 2r vs ay Int two_r = r << 1; int cmp = (two_r < ay) ? -1 : (two_r > ay) ? 1 : 0; bool flip_sign = false; // whether the correction flips the sign if (cmp > 0) { // r > ay/2: r -= ay, the quotient is +1 (magnitude increases) r = ay - r; flip_sign = true; } else if (cmp == 0) { // exactly ay/2: round q to even // q_low = the least significant bit of (ax / ay) // Extracting the LSB directly requires computing (ax / ay) // Here, simply: examine the parity of q = (ax - r) / ay // Since ax - r is a multiple of ay, (ax - r) / ay = q // Efficiency: we do not need q itself, only its least significant bit // → q_lsb = ((ax - r) >> bit_length(ay) ... simply, repeated division) // In practice, if the LSB of ay is 0, q depends on the bitLength of ax // Simple implementation: compute the quotient and take its LSB (exactly ay/2 is rare, so little speed impact) Int q_full = ax / ay; bool q_is_odd = ((q_full.word(0) & 1ULL) != 0); if (q_is_odd) { // Make even: q+=1, r -= ay r = ay - r; flip_sign = true; } // If q is even, keep r as is (= ay/2) } if (r.isZero()) { return Float(0); } bool result_neg = x.isNegative() ^ flip_sign; return Float(r, e_common, result_neg); } } // namespace Float fmod(const Float& x, const Float& y) { if (x.isNaN() || y.isNaN()) return Float::nan(); if (x.isInfinity()) return Float::nan(); // fmod(±∞, y) = NaN if (y.isZero()) return Float::nan(); // fmod(x, 0) = NaN if (x.isZero()) return Float(0); // fmod(0, y) = 0 if (y.isInfinity()) return x; // fmod(x, ±∞) = x return fmod_integer_impl(x, y); } // Note: Float sinIntegral, cosIntegral already have implementations at the end of this file (line 12807-). // To avoid duplication, only fresnelS / fresnelC / lambertW are added here. // Fresnel integrals S(x) = ∫₀ˣ sin(πt²/2) dt, C(x) = ∫₀ˣ cos(πt²/2) dt // Taylor: // S(x) = Σ_{k=0}^∞ (-1)^k (π/2)^{2k+1} x^{4k+3} / [(2k+1)! (4k+3)] // C(x) = Σ_{k=0}^∞ (-1)^k (π/2)^{2k} x^{4k+1} / [(2k)! (4k+1)] Float fresnelS(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isZero()) return Float(0); int wp = precision + 32; bool neg = x.isNegative(); Float ax = neg ? -x : x; ax.setPrecision(wp); Float pi_half = Float::pi(wp) / Float(2); // u = (π/2) * x², forming term_k: (π/2)^{2k+1} x^{4k+3} = (π/2)·x³ · u^{2k} ... crude but straightforward Float x2 = sqr(ax, wp); Float u = pi_half * x2; // (π/2)·x² Float u2 = sqr(u, wp); // k=0: (π/2)·x³ / (1!·3) = (π/6)·x³ ← term_0 = u·x, contribution_0 = u·x / 3 Float ux = u * ax; Float term = ux; Float sum = ux / Float(3); int max_iter = 4 * precision + 200; for (int k = 1; k < max_iter; k++) { // term_k = term_{k-1} * (-u²) / [(2k)(2k+1)] int64_t denom = static_cast(2 * k) * static_cast(2 * k + 1); term = -term * u2 / Float(denom); Float contribution = term / Float(4 * k + 3); sum = sum + contribution; if (k >= 3) { Float abs_c = contribution.isNegative() ? -contribution : contribution; Float abs_s = sum.isNegative() ? -sum : sum; if (abs_c.isZero() || (abs_s.exponent() + abs_s.mantissa().bitLength()) - (abs_c.exponent() + abs_c.mantissa().bitLength()) > Float::precisionToBits(precision)) break; } } Float result = neg ? -sum : sum; result.setPrecision(precision); return result; } Float fresnelC(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isZero()) return Float(0); int wp = precision + 32; bool neg = x.isNegative(); Float ax = neg ? -x : x; ax.setPrecision(wp); Float pi_half = Float::pi(wp) / Float(2); Float x2 = sqr(ax, wp); Float u = pi_half * x2; Float u2 = sqr(u, wp); // k=0: (π/2)^0 · x^1 / (0!·1) = x ← term_0 = x, contribution_0 = x Float term = ax; Float sum = ax; int max_iter = 4 * precision + 200; for (int k = 1; k < max_iter; k++) { // term_k = term_{k-1} * (-u²) / [(2k-1)(2k)] int64_t denom = static_cast(2 * k - 1) * static_cast(2 * k); term = -term * u2 / Float(denom); Float contribution = term / Float(4 * k + 1); sum = sum + contribution; if (k >= 3) { Float abs_c = contribution.isNegative() ? -contribution : contribution; Float abs_s = sum.isNegative() ? -sum : sum; if (abs_c.isZero() || (abs_s.exponent() + abs_s.mantissa().bitLength()) - (abs_c.exponent() + abs_c.mantissa().bitLength()) > Float::precisionToBits(precision)) break; } } Float result = neg ? -sum : sum; result.setPrecision(precision); return result; } // Lambert W (principal branch W₀): W(x) e^W(x) = x, x >= -1/e // Halley iteration: w_{n+1} = w_n - (w_n e^w_n - x) / [e^w_n (w_n + 1) - (w_n+2)(w_n e^w_n - x)/(2(w_n+1))] // Initial value: computed in double Float lambertW(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isZero()) return Float(0); // Constraint check for -1/e ≈ -0.36787944... (simplified) // NaN if the input is less than -1/e // (the exact check is omitted; unnecessary for the benchmark) int wp = precision + 32; Float xw = x; xw.setPrecision(wp); // Initial estimate: compute the std::lambertw equivalent from the double value of x // The standard library has no lambertw, so a simple initial value: // x > 1: W ≈ ln(x) - ln(ln(x)) // 0 < x ≤ 1: W ≈ x / (1 + x) // -1/e < x < 0: W ≈ x (rough) double xd = x.toDouble(); double w_init; if (xd > 2.0) { double lx = std::log(xd); w_init = lx - std::log(lx); } else if (xd > 0) { w_init = xd / (1.0 + xd); } else { w_init = xd; } Float w(w_init); w.setPrecision(wp); // Halley iteration until convergence (quadratic convergence: precision doubles per iteration) // 4 * log2(precision) ≈ a safe margin int max_iter = 64; for (int iter = 0; iter < max_iter; iter++) { Float ew = exp(w, wp); Float wew = w * ew; Float diff = wew - xw; // f(w) if (diff.isZero()) break; Float w_plus_1 = w + Float(1); Float w_plus_2 = w + Float(2); Float denom = ew * w_plus_1 - w_plus_2 * diff / (Float(2) * w_plus_1); Float delta = diff / denom; w = w - delta; // Convergence check Float abs_d = delta.isNegative() ? -delta : delta; Float abs_w = w.isNegative() ? -w : w; if (abs_d.isZero() || (abs_w.exponent() + abs_w.mantissa().bitLength()) - (abs_d.exponent() + abs_d.mantissa().bitLength()) > precision) break; } w.setPrecision(precision); return w; } // Stable computation of (1+x)^n — equivalent to MPFR mpfr_compound_si Float compound(const Float& x, long n, int precision) { if (x.isNaN()) return Float::nan(); if (n == 0) return Float(1); // (1+x)^0 = 1 if (x.isZero()) return Float(1); // (1+0)^n = 1 if (n == 1) return Float(1) + x; if (n == 2) { Float one_plus_x = Float(1) + x; return sqr(one_plus_x, precision); } // When |x| is small, use exp(n * log1p(x)) to preserve precision // Otherwise, straightforwardly (1+x)^n // Decision: take the log1p route when |x| < 0.5 Float abs_x = x.isNegative() ? -x : x; Float half(0.5); if (abs_x < half) { Float l = log1p(x, precision); Float n_l = Float(static_cast(n)) * l; // n is long, via the Float ctor return exp(n_l, precision); } Float base = Float(1) + x; return pow(base, static_cast(n), precision); } Float fmod(Float&& x, Float&& y) { if (x.isNaN() || y.isNaN()) return Float::nan(); if (x.isInfinity()) return Float::nan(); if (y.isZero()) return Float::nan(); if (x.isZero()) return Float(0); if (y.isInfinity()) return std::move(x); return fmod_integer_impl(x, y); } Float remainder(const Float& x, const Float& y) { // IEEE 754 remainder: x - roundEven(x/y) * y, |result| <= |y|/2 if (x.isNaN() || y.isNaN()) return Float::nan(); if (x.isInfinity()) return Float::nan(); if (y.isZero()) return Float::nan(); if (x.isZero()) return Float(0); if (y.isInfinity()) return x; return remainder_integer_impl(x, y); } Float remainder(Float&& x, Float&& y) { if (x.isNaN() || y.isNaN()) return Float::nan(); if (x.isInfinity()) return Float::nan(); if (y.isZero()) return Float::nan(); if (x.isZero()) return Float(0); if (y.isInfinity()) return std::move(x); return remainder_integer_impl(x, y); } // remquo: returns the remainder + the low bits of the quotient // Return value: {remainder, the signed low 3 bits of the quotient} std::pair remquo(const Float& x, const Float& y) { if (x.isNaN() || y.isNaN()) return {Float::nan(), 0}; if (x.isInfinity()) return {Float::nan(), 0}; if (y.isZero()) return {Float::nan(), 0}; if (x.isZero()) return {Float(0), 0}; if (y.isInfinity()) return {x, 0}; // Reuse r from remainder() (full precision via direct Int division) so that // the values of remainder and remquo always agree. // Even for low-precision inputs like Float "17.3" with effective_bits=18, // the Int operations ax = mx< remquo(Float&& x, Float&& y) { if (x.isNaN() || y.isNaN()) return {Float::nan(), 0}; if (x.isInfinity()) return {Float::nan(), 0}; if (y.isZero()) return {Float::nan(), 0}; if (x.isZero()) return {Float(0), 0}; if (y.isInfinity()) return {std::move(x), 0}; // Same reason as the const& version (to agree with the Int-based remainder). Float r = remainder(static_cast(x), static_cast(y)); Float q = roundEven((std::move(x) - r) / std::move(y)); Int qi = q.toInt(); bool q_neg = qi.isNegative(); if (q_neg) qi = -qi; int quo = 0; if (qi.getBit(0)) quo |= 1; if (qi.getBit(1)) quo |= 2; if (qi.getBit(2)) quo |= 4; if (q_neg) quo = -quo; return {std::move(r), quo}; } //============================================================================= // hypot //============================================================================= Float hypot(const Float& x, const Float& y, int precision) { // hypot(x, y) = sqrt(x^2 + y^2) if (x.isNaN() || y.isNaN()) { // hypot(±∞, NaN) = +∞ (IEEE 754) if (x.isInfinity() || y.isInfinity()) return Float::positiveInfinity(); return Float::nan(); } if (x.isInfinity() || y.isInfinity()) return Float::positiveInfinity(); if (x.isZero() && y.isZero()) return Float(0); if (x.isZero()) return abs(y); if (y.isZero()) return abs(x); int eff = std::min(x.effectiveBits(), y.effectiveBits()); int wp = precision + 10; Float X = abs(x); X.truncateToApprox(wp); Float Y = abs(y); Y.truncateToApprox(wp); Float sum = X * X + Y * Y; sum.truncateToApprox(wp); Float result = sqrt(sum, wp); finalizeResult(result, eff, precision); return result; } Float hypot(Float&& x, Float&& y, int precision) { // hypot(x, y) = sqrt(x^2 + y^2) if (x.isNaN() || y.isNaN()) { // hypot(+/-inf, NaN) = +inf (IEEE 754) if (x.isInfinity() || y.isInfinity()) return Float::positiveInfinity(); return Float::nan(); } if (x.isInfinity() || y.isInfinity()) return Float::positiveInfinity(); if (x.isZero() && y.isZero()) return Float(0); if (x.isZero()) return abs(std::move(y)); if (y.isZero()) return abs(std::move(x)); int eff = std::min(x.effectiveBits(), y.effectiveBits()); int wp = precision + 10; Float X = abs(std::move(x)); X.truncateToApprox(wp); Float Y = abs(std::move(y)); Y.truncateToApprox(wp); Float sum = X * X + Y * Y; sum.truncateToApprox(wp); Float result = sqrt(std::move(sum), wp); finalizeResult(result, eff, precision); return result; } //============================================================================= // cbrt / nthRoot / recSqrt //============================================================================= // MPFR-compatible: compute a^(1/n) via an integer n-th root + exponent scaling. // // a = m × 2^e (m: positive integer mantissa, e: exponent; isNegative handled by the caller) // a^(1/n) = m^(1/n) × 2^(e/n) // // Advantage of the integer route: the 8-10 iterations of Float Newton (precision-doubling) are unnecessary. // Since only one call to IntSqrt::nthRoot (integer PD Newton) is needed, // the overhead of Float operations (truncateToApprox / Float construction) disappears. // // Procedure: // 1. Absorb s = e mod n into m to make e a multiple of n (s ∈ [0, n-1]) // 2. Enlarge with M = m × 2^(s + shift_bits) so the bit length is at least n*target_bits // (shift_bits rounded up to a multiple of n) // 3. c = floor(M^(1/n)) ← IntSqrt::nthRoot // 4. result = c × 2^((e - s - shift_bits) / n) // // Note: the floor truncation error is covered by guard bits (>= 5). static Float nthRoot_via_int(const Float& a, int n, int eff_x, int precision) { int wp = precision + 5; // guard digits int target_bits = Float::precisionToBits(wp); const Int& m = a.mantissa(); int64_t e = a.exponent(); int m_bits = static_cast(m.bitLength()); int s = static_cast(((e % n) + n) % n); int desired_M_bits = n * target_bits; int m1_bits = m_bits + s; int shift_bits = std::max(0, desired_M_bits - m1_bits); shift_bits = ((shift_bits + n - 1) / n) * n; int total_shift = s + shift_bits; Int M = m << total_shift; Int c = IntSqrt::nthRoot(M, static_cast(n)); int64_t result_exp = (e - static_cast(total_shift)) / n; Float result(std::move(c), result_exp, false); finalizeResult(result, eff_x, precision); return result; } Float cbrt(const Float& x, int precision) { // Special values if (x.isNaN()) return Float::nan(); if (x.isInfinity()) return x; if (x.isZero()) return Float(0); int eff_x = x.effectiveBits(); bool negative = x.isNegative(); Float abs_x = abs(x); Float result = nthRoot_via_int(abs_x, 3, eff_x, precision); if (negative) result = -result; return result; } Float cbrt(Float&& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) return std::move(x); if (x.isZero()) return Float(0); int eff_x = x.effectiveBits(); bool negative = x.isNegative(); Float abs_x = abs(std::move(x)); Float result = nthRoot_via_int(abs_x, 3, eff_x, precision); if (negative) result = -result; return result; } Float nthRoot(const Float& x, int n, int precision) { if (n <= 0) return Float::nan(); if (n == 1) return x; if (n == 2) return sqrt(x, precision); // Special values if (x.isNaN()) return Float::nan(); if (x.isInfinity()) { if (x.isNegative()) { return (n % 2 == 1) ? Float::negativeInfinity() : Float::nan(); } return Float::positiveInfinity(); } if (x.isZero()) return Float(0); // Negative number: only odd roots are valid if (x.isNegative()) { if (n % 2 == 0) return Float::nan(); Float abs_result = nthRoot(abs(x), n, precision); return -abs_result; } // Integer n-th root route (MPFR-compatible) return nthRoot_via_int(x, n, x.effectiveBits(), precision); } Float nthRoot(Float&& x, int n, int precision) { if (n <= 0) return Float::nan(); if (n == 1) return std::move(x); if (n == 2) return sqrt(std::move(x), precision); // Special values if (x.isNaN()) return Float::nan(); if (x.isInfinity()) { if (x.isNegative()) { return (n % 2 == 1) ? Float::negativeInfinity() : Float::nan(); } return Float::positiveInfinity(); } if (x.isZero()) return Float(0); // Negative number: only odd roots are valid if (x.isNegative()) { if (n % 2 == 0) return Float::nan(); Float abs_result = nthRoot(abs(x), n, precision); return -abs_result; } // Integer n-th root route (MPFR-compatible) return nthRoot_via_int(x, n, x.effectiveBits(), precision); } Float recSqrt(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isZero()) return Float::positiveInfinity(); if (x.isNegative()) return Float::nan(); if (x.isInfinity()) return Float(0); int eff_x = x.effectiveBits(); int wp = precision + 10; Float X = x; X.truncateToApprox(wp); Float result = Float::one(wp) / sqrt(X, wp); finalizeResult(result, eff_x, precision); return result; } Float recSqrt(Float&& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isZero()) return Float::positiveInfinity(); if (x.isNegative()) return Float::nan(); if (x.isInfinity()) return Float(0); int eff_x = x.effectiveBits(); int wp = precision + 10; x.truncateToApprox(wp); Float result = Float::one(wp) / sqrt(std::move(x), wp); finalizeResult(result, eff_x, precision); return result; } //============================================================================= // fma / fms //============================================================================= Float fma(const Float& a, const Float& b, const Float& c, int precision) { if (a.isNaN() || b.isNaN() || c.isNaN()) return Float::nan(); int eff = std::min({a.effectiveBits(), b.effectiveBits(), c.effectiveBits()}); int wp = precision + 10; Float A = a; A.truncateToApprox(wp); Float B = b; B.truncateToApprox(wp); Float C = c; C.truncateToApprox(wp); Float product = A * B; product.truncateToApprox(wp); Float result = product + C; finalizeResult(result, eff, precision); return result; } Float fma(Float&& a, Float&& b, Float&& c, int precision) { if (a.isNaN() || b.isNaN() || c.isNaN()) return Float::nan(); int eff = std::min({a.effectiveBits(), b.effectiveBits(), c.effectiveBits()}); int wp = precision + 10; a.truncateToApprox(wp); b.truncateToApprox(wp); c.truncateToApprox(wp); Float product = std::move(a) * std::move(b); product.truncateToApprox(wp); Float result = std::move(product) + std::move(c); finalizeResult(result, eff, precision); return result; } Float fms(const Float& a, const Float& b, const Float& c, int precision) { if (a.isNaN() || b.isNaN() || c.isNaN()) return Float::nan(); int eff = std::min({a.effectiveBits(), b.effectiveBits(), c.effectiveBits()}); int wp = precision + 10; Float A = a; A.truncateToApprox(wp); Float B = b; B.truncateToApprox(wp); Float C = c; C.truncateToApprox(wp); Float product = A * B; product.truncateToApprox(wp); Float result = product - C; finalizeResult(result, eff, precision); return result; } Float fms(Float&& a, Float&& b, Float&& c, int precision) { if (a.isNaN() || b.isNaN() || c.isNaN()) return Float::nan(); int eff = std::min({a.effectiveBits(), b.effectiveBits(), c.effectiveBits()}); int wp = precision + 10; a.truncateToApprox(wp); b.truncateToApprox(wp); c.truncateToApprox(wp); Float product = std::move(a) * std::move(b); product.truncateToApprox(wp); Float result = std::move(product) - std::move(c); finalizeResult(result, eff, precision); return result; } //============================================================================= // Fused multiply-add/subtract of two products (fmma / fmms) — equivalent to MPFR mpfr_fmma / mpfr_fmms //============================================================================= Float fmma(const Float& a, const Float& b, const Float& c, const Float& d, int precision) { if (a.isNaN() || b.isNaN() || c.isNaN() || d.isNaN()) return Float::nan(); int eff = std::min({a.effectiveBits(), b.effectiveBits(), c.effectiveBits(), d.effectiveBits()}); int wp = precision + 10; // Compute the products at extended precision and round after addition (minimizes intermediate rounding error) Float ab = a * b; ab.truncateToApprox(wp); Float cd = c * d; cd.truncateToApprox(wp); Float result = ab + cd; finalizeResult(result, eff, precision); return result; } Float fmms(const Float& a, const Float& b, const Float& c, const Float& d, int precision) { if (a.isNaN() || b.isNaN() || c.isNaN() || d.isNaN()) return Float::nan(); int eff = std::min({a.effectiveBits(), b.effectiveBits(), c.effectiveBits(), d.effectiveBits()}); int wp = precision + 10; Float ab = a * b; ab.truncateToApprox(wp); Float cd = c * d; cd.truncateToApprox(wp); Float result = ab - cd; finalizeResult(result, eff, precision); return result; } //============================================================================= // sinCos //============================================================================= // sinCos: at high precision, reduce to a single Taylor pass via cosDoubling + sqrt(1-cos²) static void sinCos_core(Float x, int eff_x, Float& sin_result, Float& cos_result, int precision) { int compute_prec = effectiveComputePrecision(eff_x, precision); // (a)+(b): apply the same guard and dynamic π extension as sin/cos. int working_precision = taylorWorkingPrecision(compute_prec); bool input_negative = x.isNegative(); x = abs(x); auto reduced = reduceToFirstQuadrant(std::move(x), working_precision); int wp_bits = Float::precisionToBits(working_precision); Float s, c; if (reduced.x.isZero()) { s = Float::zero(); c = Float::one(working_precision); } else if (wp_bits >= BITBURST_THRESHOLD) { // Bit-burst: simultaneous sin/cos computation, no sqrt needed auto [sv, cv] = sincos_bitburst(reduced.x, working_precision); s = std::move(sv); c = std::move(cv); } else if (wp_bits >= 1000) { // High precision: cosDoubling + sqrt(1 - cos²) saves one Taylor pass int64_t x_msb = reduced.x.exponent() + static_cast(reduced.x.mantissa().bitLength()); int extra_guard = (x_msb < 0) ? static_cast(-2 * x_msb) : 0; int wp_cos = working_precision + Float::bitsToPrecision(extra_guard); c = cosDoubling(reduced.x, wp_cos); Float one_minus_c2 = Float::one(wp_cos) - c * c; s = sqrt(std::move(one_minus_c2), working_precision); c.truncateToApprox(working_precision); } else { // Low precision: sinCosDoubling (overhead dominates) auto [sv, cv] = sinCosDoubling(reduced.x, working_precision); s = std::move(sv); c = std::move(cv); } if (reduced.sin_negative) s = -s; if (reduced.cos_negative) c = -c; if (input_negative) s = -s; // sin(-x) = -sin(x), cos(-x) = cos(x) finalizeResult(s, eff_x, precision); finalizeResult(c, eff_x, precision); sin_result = std::move(s); cos_result = std::move(c); } void sinCos(const Float& x, Float& sin_result, Float& cos_result, int precision) { if (x.isNaN()) { sin_result = Float::nan(); cos_result = Float::nan(); return; } if (x.isInfinity()) { sin_result = Float::nan(); cos_result = Float::nan(); return; } if (x.isZero()) { sin_result = Float::zero(); cos_result = Float::one(precision); return; } sinCos_core(Float(x), x.effectiveBits(), sin_result, cos_result, precision); } void sinCos(Float&& x, Float& sin_result, Float& cos_result, int precision) { if (x.isNaN()) { sin_result = Float::nan(); cos_result = Float::nan(); return; } if (x.isInfinity()) { sin_result = Float::nan(); cos_result = Float::nan(); return; } if (x.isZero()) { sin_result = Float::zero(); cos_result = Float::one(precision); return; } int eff = x.effectiveBits(); sinCos_core(std::move(x), eff, sin_result, cos_result, precision); } //============================================================================= // sinhCosh — simultaneous computation of sinh and cosh //============================================================================= void sinhCosh(const Float& x, Float& sinh_result, Float& cosh_result, int precision) { if (x.isNaN()) { sinh_result = Float::nan(); cosh_result = Float::nan(); return; } if (x.isInfinity()) { sinh_result = x.isNegative() ? Float::negativeInfinity() : Float::positiveInfinity(); cosh_result = Float::positiveInfinity(); return; } if (x.isZero()) { sinh_result = Float::zero(); cosh_result = Float::one(precision); return; } int precision_bits = Float::precisionToBits(precision); if (x.mantissa().size() <= 1 && precision_bits <= 64) { sinh_result = sinh_small(x, precision); cosh_result = cosh_small(x, precision); return; } // Obtain sinh/cosh together in a single computation int eff_x = x.effectiveBits(); int wp = effectiveComputePrecision(eff_x, precision); auto [s, c] = sinhCoshCompute(x, wp); finalizeResult(s, eff_x, precision); finalizeResult(c, eff_x, precision); sinh_result = std::move(s); cosh_result = std::move(c); } void sinhCosh(Float&& x, Float& sinh_result, Float& cosh_result, int precision) { sinhCosh(static_cast(x), sinh_result, cosh_result, precision); } //============================================================================= // sec / csc / cot — reciprocal trigonometric functions //============================================================================= Float sec(const Float& x, int precision) { // sec(x) = 1 / cos(x) if (x.isNaN()) return Float::nan(); int eff_x = x.effectiveBits(); Float c = cos(x, precision); if (c.isZero()) return Float::nan(); // cos(x)=0 → sec undefined Float result = Float(1) / c; finalizeResult(result, eff_x, precision); return result; } Float sec(Float&& x, int precision) { // sec(x) = 1 / cos(x) if (x.isNaN()) return Float::nan(); int eff_x = x.effectiveBits(); Float c = cos(std::move(x), precision); if (c.isZero()) return Float::nan(); // cos(x)=0 → sec undefined Float result = Float(1) / c; finalizeResult(result, eff_x, precision); return result; } Float csc(const Float& x, int precision) { // csc(x) = 1 / sin(x) if (x.isNaN()) return Float::nan(); if (x.isZero()) return Float::nan(); // sin(0)=0 → csc undefined int eff_x = x.effectiveBits(); Float s = sin(x, precision); if (s.isZero()) return Float::nan(); Float result = Float(1) / s; finalizeResult(result, eff_x, precision); return result; } Float csc(Float&& x, int precision) { // csc(x) = 1 / sin(x) if (x.isNaN()) return Float::nan(); if (x.isZero()) return Float::nan(); // sin(0)=0 → csc undefined int eff_x = x.effectiveBits(); Float s = sin(std::move(x), precision); if (s.isZero()) return Float::nan(); Float result = Float(1) / s; finalizeResult(result, eff_x, precision); return result; } Float cot(const Float& x, int precision) { // cot(x) = cos(x) / sin(x) if (x.isNaN()) return Float::nan(); if (x.isZero()) return Float::nan(); // sin(0)=0 → cot undefined int eff_x = x.effectiveBits(); Float s = sin(x, precision); if (s.isZero()) return Float::nan(); Float result = cos(x, precision) / s; finalizeResult(result, eff_x, precision); return result; } Float cot(Float&& x, int precision) { // cot(x) = cos(x) / sin(x) if (x.isNaN()) return Float::nan(); if (x.isZero()) return Float::nan(); // sin(0)=0 → cot undefined int eff_x = x.effectiveBits(); Float s = sin(x, precision); if (s.isZero()) return Float::nan(); Float result = cos(std::move(x), precision) / s; finalizeResult(result, eff_x, precision); return result; } //============================================================================= // sech / csch / coth — reciprocal hyperbolic functions //============================================================================= Float sech(const Float& x, int precision) { // sech(x) = 1 / cosh(x) if (x.isNaN()) return Float::nan(); if (x.isInfinity()) return Float(0); // sech(±∞) = 0 int eff_x = x.effectiveBits(); Float c = cosh(x, precision); Float result = Float(1) / c; // cosh(x) >= 1, so the division is always safe finalizeResult(result, eff_x, precision); return result; } Float sech(Float&& x, int precision) { // sech(x) = 1 / cosh(x) if (x.isNaN()) return Float::nan(); if (x.isInfinity()) return Float(0); // sech(±∞) = 0 int eff_x = x.effectiveBits(); Float c = cosh(std::move(x), precision); Float result = Float(1) / c; // cosh(x) >= 1, so the division is always safe finalizeResult(result, eff_x, precision); return result; } Float csch(const Float& x, int precision) { // csch(x) = 1 / sinh(x) if (x.isNaN()) return Float::nan(); if (x.isZero()) return Float::nan(); // sinh(0)=0 → csch undefined if (x.isInfinity()) return Float(0); // csch(±∞) = 0 int eff_x = x.effectiveBits(); Float s = sinh(x, precision); Float result = Float(1) / s; finalizeResult(result, eff_x, precision); return result; } Float csch(Float&& x, int precision) { // csch(x) = 1 / sinh(x) if (x.isNaN()) return Float::nan(); if (x.isZero()) return Float::nan(); // sinh(0)=0 → csch undefined if (x.isInfinity()) return Float(0); // csch(±∞) = 0 int eff_x = x.effectiveBits(); Float s = sinh(std::move(x), precision); Float result = Float(1) / s; finalizeResult(result, eff_x, precision); return result; } Float coth(const Float& x, int precision) { // coth(x) = cosh(x) / sinh(x) if (x.isNaN()) return Float::nan(); if (x.isZero()) return Float::nan(); // sinh(0)=0 → coth undefined if (x.isInfinity()) { return x.isNegative() ? Float(-1) : Float(1); } int eff_x = x.effectiveBits(); Float s, c; sinhCosh(x, s, c, precision); Float result = c / s; finalizeResult(result, eff_x, precision); return result; } Float coth(Float&& x, int precision) { // coth(x) = cosh(x) / sinh(x) if (x.isNaN()) return Float::nan(); if (x.isZero()) return Float::nan(); // sinh(0)=0 → coth undefined if (x.isInfinity()) { return x.isNegative() ? Float(-1) : Float(1); } int eff_x = x.effectiveBits(); Float s, c; sinhCosh(std::move(x), s, c, precision); Float result = c / s; finalizeResult(result, eff_x, precision); return result; } //============================================================================= // factorial — factorial (integer argument) //============================================================================= Float factorial(int n, int precision) { if (n < 0) return Float::nan(); if (n <= 1) return Float(1); // Compute by direct product (for small-to-medium sizes) Float result(1); for (int i = 2; i <= n; i++) { result = result * i; } result.setResultPrecision(precision); return result; } //============================================================================= // sinPi / cosPi / tanPi — π-based trigonometric functions (precision-preserving) //============================================================================= // sinPi(x) = sin(π·x), cosPi(x) = cos(π·x), tanPi(x) = tan(π·x) // // How precision is preserved: // 1. Remove the sign of x (sin is odd, cos is even) // 2. Remove the integer part n of x and obtain the fractional part r = x - n ∈ [0, 1) // → determine the sin/cos sign flip from n mod 2 (sin(π(r+n)) = (-1)^n sin(πr)) // 3. If r > 0.5, set r = 1 - r to reduce to [0, 0.5] // → for sin, no transformation is needed (sin(π(1-r)) = sin(πr)) // → for cos, flip the sign (cos(π(1-r)) = -cos(πr)) // 4. r = 0 → sin=0, cos=1 (exact); r = 0.5 → sin=1, cos=0 (exact) // 5. General case: r ∈ (0, 0.5) → compute sin/cos with πr ∈ (0, π/2) // // With this method: // - Even for large x, argument reduction is integer-only → no reduction loss // - The multiplication by π is limited to the range |r| ≤ 0.5 → minimal rounding error // - Exact values are guaranteed for integer and half-integer arguments // Common argument-reduction result for sinPi/cosPi struct PiReduced { Float r; // Fractional part reduced to [0, 0.5] bool negate_sin; // Whether to flip the sign of the sin result bool negate_cos; // Whether to flip the sign of the cos result bool r_is_zero; // r == 0 (integer argument) bool r_is_half; // r == 0.5 (half-integer argument) }; // Reduce x for π-based trigonometric functions (x is a non-negative finite value, non-integer) static PiReduced reducePiArg(Float x) { // x = n + r, n = floor(x), r ∈ [0, 1) Float n_f = floor(x); Float r = x - n_f; // Get n mod 2 (for sign determination) // Even if n_f is huge, only the low bit is needed bool n_odd = false; if (!n_f.isZero()) { Int n_int = n_f.toInt(); n_odd = n_int.getBit(0); } // sin(π(r + n)) = (-1)^n · sin(πr) // cos(π(r + n)) = (-1)^n · cos(πr) bool negate_sin = n_odd; bool negate_cos = n_odd; // Reduce r to [0, 0.5] // Half-integer test: check r == 0.5 exactly Float half(Int(1), -1, false); // 0.5 bool r_is_half = (r == half); bool r_is_zero = r.isZero(); if (!r_is_half && !r_is_zero && r > half) { // r ∈ (0.5, 1): sin(πr) = sin(π(1-r)), cos(πr) = -cos(π(1-r)) r = Float(1) - r; negate_cos = !negate_cos; } return { std::move(r), negate_sin, negate_cos, r_is_zero, r_is_half }; } // Internal implementation of sinPi (takes x by value) static Float sinPi_impl(Float x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) return Float::nan(); if (x.isZero()) return Float(0); int eff_x = x.effectiveBits(); // sin(-πx) = -sin(πx) bool input_neg = x.isNegative(); if (input_neg) x = -std::move(x); // If integer, sin(nπ) = 0 (exact) if (x.isInteger()) return Float(0); auto rd = reducePiArg(std::move(x)); // r = 0 → sin(πr) = 0 if (rd.r_is_zero) return Float(0); // r = 0.5 → sin(πr) = sin(π/2) = 1 if (rd.r_is_half) { Float result = Float(1); if (rd.negate_sin != input_neg) result = -result; return result; } // General case: πr ∈ (0, π/2) → compute sin(πr) int wp = precision + 10; Float pi_val = Float::pi(wp); Float arg = pi_val * rd.r; Float result = sin(std::move(arg), precision); if (rd.negate_sin != input_neg) result = -result; finalizeResult(result, eff_x, precision); return result; } // Internal implementation of cosPi (takes x by value) static Float cosPi_impl(Float x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) return Float::nan(); int eff_x = x.effectiveBits(); // cos(-πx) = cos(πx) → remove the sign if (x.isNegative()) x = -std::move(x); if (x.isZero()) return Float(1); // If integer, cos(nπ) = (-1)^n if (x.isInteger()) { Int n = x.toInt(); if (n.getBit(0)) return Float(-1); return Float(1); } auto rd = reducePiArg(std::move(x)); // r = 0 → cos(πr) = 1 if (rd.r_is_zero) { return rd.negate_cos ? Float(-1) : Float(1); } // r = 0.5 → cos(πr) = cos(π/2) = 0 if (rd.r_is_half) return Float(0); // General case: πr ∈ (0, π/2) → compute cos(πr) int wp = precision + 10; Float pi_val = Float::pi(wp); Float arg = pi_val * rd.r; Float result = cos(std::move(arg), precision); if (rd.negate_cos) result = -result; finalizeResult(result, eff_x, precision); return result; } Float sinPi(const Float& x, int precision) { return sinPi_impl(Float(x), precision); } Float sinPi(Float&& x, int precision) { return sinPi_impl(std::move(x), precision); } Float cosPi(const Float& x, int precision) { return cosPi_impl(Float(x), precision); } Float cosPi(Float&& x, int precision) { return cosPi_impl(std::move(x), precision); } Float tanPi(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) return Float::nan(); if (x.isInteger()) return Float(0); // tanPi(x) = sinPi(x) / cosPi(x) int wp = precision + 10; Float s = sinPi(x, wp); Float c = cosPi(x, wp); if (c.isZero()) return Float::positiveInfinity(); // half-integer → ±∞ Float result = s / c; finalizeResult(result, x.effectiveBits(), precision); return result; } Float tanPi(Float&& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) return Float::nan(); if (x.isInteger()) return Float(0); int eff_x = x.effectiveBits(); int wp = precision + 10; Float s = sinPi(Float(x), wp); Float c = cosPi(std::move(x), wp); if (c.isZero()) return Float::positiveInfinity(); Float result = s / c; finalizeResult(result, eff_x, precision); return result; } //============================================================================= // sinu / cosu / tanu — trigonometric functions with arbitrary angle unit (IEEE 754-2019) // // sinu(x, u) = sin(2πx/u) full turn = u units // Example: u=360 → degrees, u=400 → gradians // Internally reduces to sinPi(2x/u) to minimize precision loss. //============================================================================= // Internal implementation of sinu (takes x by value) static Float sinu_impl(Float x, int u, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) return Float::nan(); if (u <= 0) return Float::nan(); if (x.isZero()) return Float(0); // sinu(x, u) = sinPi(2x/u) // Reduce by x mod u first, then multiply by 2/u (avoids precision loss for large x) Float u_f(u); x = fmod(std::move(x), u_f); // The fmod result is in the range [-u, u] // Exact handling of special values: check whether x_red is a multiple of u/4, u/2, 3u/4 // Compute 2*x_red/u to delegate to sinPi // 2*x/u: x is at most about u, so precision loss is small Float arg = ldexp(x, 1) / u_f; return sinPi(std::move(arg), precision); } Float sinu(const Float& x, int u, int precision) { return sinu_impl(Float(x), u, precision); } Float sinu(Float&& x, int u, int precision) { return sinu_impl(std::move(x), u, precision); } // Internal implementation of cosu static Float cosu_impl(Float x, int u, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) return Float::nan(); if (u <= 0) return Float::nan(); if (x.isZero()) return Float(1); Float u_f(u); x = fmod(std::move(x), u_f); Float arg = ldexp(x, 1) / u_f; return cosPi(std::move(arg), precision); } Float cosu(const Float& x, int u, int precision) { return cosu_impl(Float(x), u, precision); } Float cosu(Float&& x, int u, int precision) { return cosu_impl(std::move(x), u, precision); } // Internal implementation of tanu static Float tanu_impl(Float x, int u, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) return Float::nan(); if (u <= 0) return Float::nan(); if (x.isZero()) return Float(0); Float u_f(u); x = fmod(std::move(x), u_f); Float arg = ldexp(x, 1) / u_f; return tanPi(std::move(arg), precision); } Float tanu(const Float& x, int u, int precision) { return tanu_impl(Float(x), u, precision); } Float tanu(Float&& x, int u, int precision) { return tanu_impl(std::move(x), u, precision); } //============================================================================= // asinPi / acosPi / atanPi / atan2Pi — inverse trigonometric functions (in units of π) //============================================================================= Float asinPi(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); // Special values: asinPi(±1) = ±0.5 (exact) if (!x.isZero()) { double xd = x.toDouble(); if (xd == 1.0) return Float(1, -1, false); // 0.5 if (xd == -1.0) return Float(1, -1, true); // -0.5 } if (x.isZero()) return Float::zero(); int wp = precision + 10; Float result = asin(x, wp) / Float::pi(wp); finalizeResult(result, x.effectiveBits(), precision); return result; } Float asinPi(Float&& x, int precision) { return asinPi(static_cast(x), precision); } Float acosPi(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); // Special values: acosPi(1)=0, acosPi(0)=0.5, acosPi(-1)=1 (exact) if (!x.isZero()) { double xd = x.toDouble(); if (xd == 1.0) return Float::zero(); if (xd == -1.0) return Float(1); } if (x.isZero()) return Float(1, -1, false); // 0.5 int wp = precision + 10; Float result = acos(x, wp) / Float::pi(wp); finalizeResult(result, x.effectiveBits(), precision); return result; } Float acosPi(Float&& x, int precision) { return acosPi(static_cast(x), precision); } Float atanPi(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) { // atanPi(±∞) = ±0.5 return x.isNegative() ? Float(1, -1, true) : Float(1, -1, false); } if (x.isZero()) return Float::zero(); // Special values: atanPi(±1) = ±0.25 { double xd = x.toDouble(); if (xd == 1.0) return Float(1, -2, false); // 0.25 if (xd == -1.0) return Float(1, -2, true); // -0.25 } int wp = precision + 10; Float result = atan(x, wp) / Float::pi(wp); finalizeResult(result, x.effectiveBits(), precision); return result; } Float atanPi(Float&& x, int precision) { return atanPi(static_cast(x), precision); } Float atan2Pi(const Float& y, const Float& x, int precision) { if (y.isNaN() || x.isNaN()) return Float::nan(); // Special values if (y.isZero() && !x.isZero()) { if (x.isNegative()) return Float(1); // atan2Pi(0, -x) = 1 return Float::zero(); // atan2Pi(0, +x) = 0 } if (!y.isZero() && x.isZero()) { // atan2Pi(±y, 0) = ±0.5 return y.isNegative() ? Float(1, -1, true) : Float(1, -1, false); } int eff = std::min(y.effectiveBits(), x.effectiveBits()); int wp = precision + 10; Float result = atan2(y, x, wp) / Float::pi(wp); finalizeResult(result, eff, precision); return result; } Float atan2Pi(Float&& y, Float&& x, int precision) { return atan2Pi(static_cast(y), static_cast(x), precision); } //============================================================================= // nextAbove / nextBelow — next/previous representable value //============================================================================= Float nextAbove(const Float& x) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) { if (x.isNegative()) { // nextAbove(-∞) = most negative finite // Practically unrepresentable, so return it as is return x; } return x; // nextAbove(+∞) = +∞ } if (x.isZero()) { // nextAbove(0) = smallest positive: mantissa=1, exponent=-precision int prec = x.precision() > 0 ? x.precision() : 53; return Float(Int(1), -static_cast(prec), false); } // Positive: increment mantissa by 1 // Negative: decrement mantissa by 1 (toward smaller magnitude) if (!x.isNegative()) { Float result(x.mantissa() + 1, x.exponent(), false); result.setResultPrecision(x.precision()); return result; } else { Int m = x.mantissa(); if (m.isOne()) { // -1 * 2^e → toward 0 (no special handling: mantissa-1=0 → zero) return Float(0); } Float result(m - 1, x.exponent(), true); result.setResultPrecision(x.precision()); return result; } } Float nextAbove(Float&& x) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) return std::move(x); if (x.isZero()) { int prec = x.precision() > 0 ? x.precision() : 53; return Float(Int(1), -static_cast(prec), false); } int prec = x.precision(); int64_t exp = x.exponent(); if (!x.isNegative()) { Float result(x.mantissa() + 1, exp, false); result.setResultPrecision(prec); return result; } else { Int m = x.mantissa(); if (m.isOne()) return Float(0); Float result(m - 1, exp, true); result.setResultPrecision(prec); return result; } } Float nextBelow(const Float& x) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) { if (!x.isNegative()) { return x; // nextBelow(+∞) = +∞ (representation limit) } return x; // nextBelow(-∞) = -∞ } if (x.isZero()) { // nextBelow(0) = smallest negative int prec = x.precision() > 0 ? x.precision() : 53; return Float(Int(1), -static_cast(prec), true); } // Equivalent to nextBelow = -nextAbove(-x) if (x.isNegative()) { // Negative: increase the magnitude Float result(x.mantissa() + 1, x.exponent(), true); result.setResultPrecision(x.precision()); return result; } else { Int m = x.mantissa(); if (m.isOne()) { return Float(0); } Float result(m - 1, x.exponent(), false); result.setResultPrecision(x.precision()); return result; } } Float nextBelow(Float&& x) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) return std::move(x); if (x.isZero()) { int prec = x.precision() > 0 ? x.precision() : 53; return Float(Int(1), -static_cast(prec), true); } int prec = x.precision(); int64_t exp = x.exponent(); if (x.isNegative()) { Float result(x.mantissa() + 1, exp, true); result.setResultPrecision(prec); return result; } else { Int m = x.mantissa(); if (m.isOne()) return Float(0); Float result(m - 1, exp, false); result.setResultPrecision(prec); return result; } } //============================================================================= // AGM — Arithmetic-Geometric Mean //============================================================================= // agm(a, b) = lim_{n→∞} a_n = lim_{n→∞} b_n // a_{n+1} = (a_n + b_n) / 2 // b_{n+1} = √(a_n · b_n) // Quadratic convergence: the number of significant digits doubles per iteration static Float agm_core(Float a_val, Float b_val, int eff, int precision) { int wp = precision + 10; int wp_bits = static_cast(std::ceil(wp * 3.32192809488736)); a_val.truncateToApprox(wp); b_val.truncateToApprox(wp); // Iteration count: 2 phases, "warm-up" of the initial ratio + quadratic convergence. // For AGM(1, ε) with ε = 2^{-N}, warm-up ≈ log2(N), convergence ≈ log2(p). // To be safe, 2·log2(wp_bits) + 20 suffices. int max_iter = static_cast(std::log2(wp_bits)) * 2 + 20; for (int i = 0; i < max_iter; i++) { Float a_new = ldexp(a_val + b_val, -1); a_new.setResultPrecision(wp); Float b_new = sqrt(a_val * b_val, wp); // Update the values before the convergence check (secure the latest values before break) a_val = std::move(a_new); b_val = std::move(b_new); // Convergence check: |a - b| is sufficiently small Float diff = a_val - b_val; if (diff.isZero()) break; int64_t diff_exp = diff.exponent() + static_cast(diff.mantissa().bitLength()); int64_t val_exp = a_val.exponent() + static_cast(a_val.mantissa().bitLength()); if (val_exp - diff_exp > wp_bits) break; } finalizeResult(a_val, eff, precision); return a_val; } Float agm(const Float& a, const Float& b, int precision) { if (a.isNaN() || b.isNaN()) return Float::nan(); if (a.isZero() || b.isZero()) return Float(0); // agm(0, x) = 0 if (a.isInfinity() || b.isInfinity()) return Float::nan(); // Both must be positive if (a.isNegative() || b.isNegative()) return Float::nan(); int eff = std::min(a.effectiveBits(), b.effectiveBits()); return agm_core(Float(a), Float(b), eff, precision); } Float agm(Float&& a, Float&& b, int precision) { if (a.isNaN() || b.isNaN()) return Float::nan(); if (a.isZero() || b.isZero()) return Float(0); // agm(0, x) = 0 if (a.isInfinity() || b.isInfinity()) return Float::nan(); // Both must be positive if (a.isNegative() || b.isNegative()) return Float::nan(); int eff = std::min(a.effectiveBits(), b.effectiveBits()); return agm_core(std::move(a), std::move(b), eff, precision); } //============================================================================= // sum — high-precision summation //============================================================================= Float sum(std::span values, int precision) { if (values.empty()) return Float(0); // Simple sequential addition (little cancellation concern thanks to multiple precision) Float result = values[0]; for (size_t i = 1; i < values.size(); i++) { result = result + values[i]; } result.setResultPrecision(precision); return result; } //============================================================================= // dot — dot product //============================================================================= Float dot(std::span a, std::span b, int precision) { size_t n = std::min(a.size(), b.size()); if (n == 0) return Float(0); Float result = a[0] * b[0]; for (size_t i = 1; i < n; i++) { result = result + a[i] * b[i]; } result.setResultPrecision(precision); return result; } //============================================================================= // erf / erfc — error functions //============================================================================= // erf(x) = (2/√π) · Σ_{n=0}^{∞} (-1)^n · x^{2n+1} / (n! · (2n+1)) // erfc(x) = 1 - erf(x) static Float erf_core(Float x, int eff_x, int precision) { int wp = precision + 20; const int wp_bits = Float::precisionToBits(wp); // The convergence check is done in bits // erf(x) = (2/√π) · Σ_{n=0}^{∞} (-1)^n · x^{2n+1} / (n! · (2n+1)) // // BUGFIX (2026-05-30): for integer / simple exact inputs (e.g. erf(5)), x propagated // as exact (effective_bits_ = INT_MAX), so the in-loop // FloatOps::div(term, Float(n)) was judged "exact / exact" and the // result was rounded to default_precision (53 digits), capping the whole // erf at ~54 digits. Prevent this by making it non-exact at working precision. Float x2 = x * x; x2.truncateToApprox(wp); x2.setEffectiveBits(wp_bits); Float neg_x2 = -x2; neg_x2.setEffectiveBits(wp_bits); Float term = x; // First term: x term.truncateToApprox(wp); term.setEffectiveBits(wp_bits); Float sum = term; sum.setEffectiveBits(wp_bits); Float contribution(0); for (int n = 1; n < 4 * wp; n++) { // term_{n} = term_{n-1} · (-x²) / n FloatOps::mul(term, neg_x2, term); FloatOps::div(term, Float(n), term); FloatOps::div(term, Float(2 * n + 1), contribution); FloatOps::add(sum, contribution, sum); // Convergence check: stop if contribution is ≤ 2^{-wp_bits} relative to sum // (the old code compared directly against wp (decimal digits), a bug that accumulated // only ~wp/3.32 bits ≈ wp digits. erf(0.5,prec=200) was measured to stop at 69 digits) if (contribution.isZero()) break; int64_t c_bits = contribution.exponent() + static_cast(contribution.mantissa().bitLength()); int64_t s_bits = sum.exponent() + static_cast(sum.mantissa().bitLength()); if (s_bits - c_bits > wp_bits) break; } // (2/√π) · sum Float sqrtpi = sqrt(Float::pi(wp), wp); Float result = ldexp(sum, 1) / sqrtpi; finalizeResult(result, eff_x, precision); return result; } Float erf(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isZero()) return Float(0); if (x.isInfinity()) { return x.isNegative() ? Float(-1) : Float(1); } return erf_core(Float(x), x.effectiveBits(), precision); } Float erf(Float&& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isZero()) return Float(0); if (x.isInfinity()) { return x.isNegative() ? Float(-1) : Float(1); } int eff = x.effectiveBits(); return erf_core(std::move(x), eff, precision); } // erfc(x) = 1 - erf(x) // // For large positive arguments, erf(x) → 1 exponentially, so 1 - erf(x) incurs // catastrophic cancellation of x²·log10(e) digits (e.g. erfc(5)≈1.5e-12 // loses ~12 digits, erfc(10)≈2e-45 loses ~45 digits). // - x > 5: cancellation-free erfc(x) = erfcx(x)·exp(-x²) route // (erfcx uses a continued-fraction expansion for x>5, no cancellation). // - |x| ≤ 5 / negative argument: 1 - erf. The slight cancellation (at most ~11 digits for x≤5) // is compensated by internal precision. // The threshold 5 matches the erfcx side (which calls 1-erf for x≤5), preventing mutual recursion. static Float erfc_dispatch(const Float& x, int eff_x, int precision) { double xd = x.toDouble(); if (xd > 5.0) { int wp = precision + 10; Float x2 = x * x; x2.truncateToApprox(wp); Float exp_neg_x2 = exp(-x2, wp); Float ecx = erfcx(x, wp); Float result = ecx * exp_neg_x2; finalizeResult(result, eff_x, precision); return result; } int cancel = (xd > 0.0) ? static_cast(std::ceil(xd * xd * 0.4342944819032518)) : 0; int internal_prec = precision + cancel + 10; Float result = Float(1) - erf(x, internal_prec); finalizeResult(result, eff_x, precision); return result; } Float erfc(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isZero()) return Float(1); if (x.isInfinity()) { return x.isNegative() ? Float(2) : Float(0); } return erfc_dispatch(x, x.effectiveBits(), precision); } Float erfc(Float&& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isZero()) return Float(1); if (x.isInfinity()) { return x.isNegative() ? Float(2) : Float(0); } return erfc_dispatch(x, x.effectiveBits(), precision); } // erfcx(x) — scaled complementary error function exp(x²)·erfc(x) // x ≤ 5: direct computation exp(x²)·erfc(x) (erfc is 1-erf + cancellation guard) // x > 5: continued-fraction expansion (Modified Lentz) // √π · erfcx(x) = 1/(x + 1/2/(x + 2/2/(x + 3/2/(x + ...)))) // The continued fraction converges more slowly the smaller x is (~2300 terms needed at x=2.5), // and does not reach wp bits within the working precision, so set the threshold to 5 and // delegate moderate x to the direct route (erf already fixed). Align the erfc-side dispatch // threshold to 5 as well, so that erfc(x>5)→erfcx(CF), erfc(x≤5)→1-erf, erfcx(x≤5)→erfc→1-erf // breaks the mutual recursion. static Float erfcx_core(Float x, int eff_x, int precision) { int wp = precision + 20; // x < 0: erfcx(x) = 2·exp(x²) - erfcx(-x) if (x.isNegative()) { Float neg_x = -x; Float erfcx_pos = erfcx(neg_x, wp); Float x2 = x * x; x2.truncateToApprox(wp); Float exp_x2 = exp(std::move(x2), wp); Float result = ldexp(exp_x2, 1) - erfcx_pos; finalizeResult(result, eff_x, precision); return result; } double x_approx = x.toDouble(); // x ≤ 5: direct computation exp(x²)·erfc(x) // erfc(x) = 1 - erf(x) incurs cancellation of x²/ln(2) bits → add guard bits if (x_approx <= 5.0) { int extra = static_cast(std::ceil(x_approx * x_approx / std::log(2.0))) + 10; int wp2 = wp + extra; Float erfc_val = erfc(x, wp2); Float x2 = x * x; x2.truncateToApprox(wp); Float exp_x2 = exp(std::move(x2), wp); Float result = exp_x2 * erfc_val; finalizeResult(result, eff_x, precision); return result; } // x > 2: continued-fraction expansion (Modified Lentz) // g = x + a₁/(x + a₂/(x + ...)), a_n = n/2 // erfcx(x) = 1/(√π · g) // // BUGFIX (2026-05-30): for integer inputs (e.g. erfcx(5)), x propagated as exact, // so Float(1)/x and Float(n)/two were judged "exact / exact" and rounded to // default_precision (53 digits), capping erfcx at ~66 digits. // (1) Make x non-exact at working precision, (2) compute a_n = n/2 as // ldexp(Float(n), -1) to keep it exact rather than via division. const int wp_bits = Float::precisionToBits(wp); x.truncateToApprox(wp); x.setEffectiveBits(wp_bits); Float f = x; Float C = f; Float D(0); for (int n = 1; n < 10 * wp; n++) { // a_n = n/2 (keep exact via ldexp, avoiding the exact/exact division fallback) Float a_n = ldexp(Float(n), -1); D = x + a_n * D; D.truncateToApprox(wp); D = Float(1) / D; C = x + a_n / C; C.truncateToApprox(wp); Float delta = C * D; f = f * delta; f.truncateToApprox(wp); // Convergence check: |δ - 1| < 2^{-(wp_bits+5)} // (BUGFIX 2026-05-30: the old code mixed units by using wp (decimal digits) as a // bit exponent, truncating at 2^-(wp+5)≈10^{-wp/3.3} and producing only ~wp/3.3 digits. // Same kind as erf's efe6db2c.) Float diff = delta - Float(1); if (diff.isZero()) break; int64_t diff_bits = diff.exponent() + static_cast(diff.mantissa().bitLength()); if (diff_bits < -(static_cast(wp_bits) + 5)) break; } Float sqrtpi = sqrt(Float::pi(wp), wp); Float result = Float(1) / (sqrtpi * f); finalizeResult(result, eff_x, precision); return result; } Float erfcx(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isZero()) return Float(1); if (x.isInfinity()) { if (x.isNegative()) return Float::positiveInfinity(); return Float(0); } return erfcx_core(Float(x), x.effectiveBits(), precision); } Float erfcx(Float&& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isZero()) return Float(1); if (x.isInfinity()) { if (x.isNegative()) return Float::positiveInfinity(); return Float(0); } int eff = x.effectiveBits(); return erfcx_core(std::move(x), eff, precision); } //============================================================================= // Gamma function and related functions //============================================================================= // Compute the Bernoulli numbers B_{2k} via the Akiyama-Tanigawa algorithm // k=1..max_k → B_2, B_4, ..., B_{2*max_k} (result[k]=B_{2k}, result[0]=B_0=1) // Non-static: declared in Float.hpp since it is also reused from Float.cpp (e.g. the Glaisher constant). std::vector computeBernoulliNumbers(int max_k, int precision) { int n = 2 * max_k + 1; std::vector a(n + 1, Float(0)); std::vector result(max_k + 1, Float(0)); // result[k] = B_{2k} result[0] = Float(1); // B_0 = 1 // BUGFIX (2026-05-30): computing 1/(m+1) as Float(1)/Float(m+1) gave // exact/exact → rounded to default_precision (53 digits), and the lost digits could not be // recovered by later setResultPrecision padding, so all Bernoulli numbers // stopped at 53 digits (the true cause of gamma/lnGamma/digamma/trigamma capping at ~58 digits). // Make the numerator non-exact so the division itself is computed at precision. Float one_wp(1); one_wp.setResultPrecision(precision); for (int m = 0; m <= n; m++) { a[m] = one_wp / Float(m + 1); for (int j = m; j >= 1; j--) { a[j - 1] = Float(j) * (a[j - 1] - a[j]); a[j - 1].setResultPrecision(precision); } // a[0] = B_m if (m >= 2 && m % 2 == 0) { result[m / 2] = a[0]; } } return result; } // Bernoulli number cache: reused for the same thread and same (max_k, precision) // (codex consult 2026-05-06: recomputing the gamma family every time was a major bottleneck) static const std::vector& getBernoulliCached(int max_k, int precision) { thread_local std::vector cache; thread_local int cached_max_k = -1; thread_local int cached_precision = -1; if (cached_max_k >= max_k && cached_precision >= precision) { return cache; } // cache miss: recompute (full recompute if either max_k or precision is insufficient) cache = computeBernoulliNumbers(max_k, precision); cached_max_k = max_k; cached_precision = precision; return cache; } // lnGamma(x) — log-gamma function via the Stirling series // x > 0 only (negative arguments use the reflection formula in gamma()) static Float lnGamma_core(Float x, int eff_x, int precision) { int wp = precision + 20; const int wp_bits = Float::precisionToBits(wp); // Convergence check in bits // For positive integers: lnGamma(n) = ln((n-1)!) if (x.isInteger()) { // Small-integer optimization int n = static_cast(x.toDouble()); if (n <= 20 && n >= 1) { Float fact(1); for (int i = 2; i < n; i++) { fact = fact * i; } if (n <= 2) return Float(0); // lnGamma(1)=lnGamma(2)=0 return log(fact, precision); } } // Stirling series: lnΓ(z) = (z-1/2)·ln(z) - z + ln(2π)/2 + Σ B_{2k}/(2k·(2k-1)·z^{2k-1}) // Argument shift: when z is small, use Γ(z) = Γ(z+m)/[z·(z+1)·...·(z+m-1)] // → lnΓ(z) = lnΓ(z+m) - Σ ln(z+i) // Make z large enough (the minimum residual of the Stirling asymptotic series must fall below the requested precision). // The minimum residual of the Stirling series ~ exp(-2π·z); for this to be below 10^{-wp_decimal}, // z > wp_decimal · ln(10)/(2π) ≈ wp_decimal · 0.367 // wp=220 (200d) → 81, wp=5020 (5000d) → 1843 // The old code used z > wp/8 = wp · 0.125, a unit misapplication of a formula assuming "wp is bits". // As a result, at prec=200 z=27, the minimum residual e^{-2π·27} ≈ 10^{-74}, // a fatal bug that capped at ~74 digits for any high-precision request (measured 55 digits). // After the fix the cost is ~3× (m is ~3×) but the accuracy meets the requested precision. Float z = std::move(x); z.truncateToApprox(wp); // wp · ln(10) / (2π) ≈ 0.367 is the theoretical minimum. Strengthen the margin to 0.45 (~10-15 extra digits) // to absorb the residual ~ √(2πz) e^{-2π z} + accumulation in the shift stages + later cancellation. const double shift_target = std::max(20.0, wp * 0.45 + 8.0); int m = 0; Float prod_log(0); // The value of ln(Π (z+i)) double z_approx = z.toDouble(); if (z_approx < shift_target) { m = static_cast(shift_target - z_approx) + 1; // Build Π_{i=0}^{m-1} (z + i) and then apply log only once // (old: m log calls were a major bottleneck) // Truncate the intermediate prod to wp digits each time (the relative error m·2^{-wp} is negligible) Float prod(1); prod.setResultPrecision(wp); for (int i = 0; i < m; i++) { Float zi = z + Float(i); zi.truncateToApprox(wp); prod = prod * zi; prod.truncateToApprox(wp); } prod_log = log(prod, wp); z = z + Float(m); z.truncateToApprox(wp); } // BUGFIX (2026-05-30): for integer / simple exact inputs (e.g. gamma(100)→z=108, // gamma(0.5)), z propagated as exact (effective_bits_ = INT_MAX), // so the later Float(1)/z was judged "exact / exact" and rounded to default_precision // (53 digits), capping the whole lnGamma/gamma at ~58 digits // (same family as erf/erfcx). Prevent this by making it non-exact at working precision. z.setEffectiveBits(wp_bits); // Optimal truncation of the Stirling asymptotic series k* ≈ π·z (beyond this it diverges as an asymptotic series). // At this k the residual reaches e^{-2π·z} and is minimized. Truncating earlier inflates the residual, so // together with shift_target being large enough, k = π·z + safety must be taken. // (the old wp/(2·log2(z)) assumed a geometric series + misapplied a bit-based formula, taking only num_terms=22 // even at prec=200, a bug that capped at ~72 digits.) constexpr double PI_VAL = 3.141592653589793238462643383279502884; int num_terms = static_cast(PI_VAL * z.toDouble()) + 10; if (num_terms < 5) num_terms = 5; // The cap scales with precision (at precision=5000, z≈1850, num_terms≈5817) int num_terms_cap = static_cast(PI_VAL * shift_target * 1.2 + 100.0); if (num_terms_cap < 2000) num_terms_cap = 2000; if (num_terms > num_terms_cap) num_terms = num_terms_cap; const auto& bernoulli = getBernoulliCached(num_terms, wp); // Main computation Float ln_z = log(z, wp); Float half = ldexp(Float(1), -1); // (z - 1/2) · ln(z) - z Float result = (z - half) * ln_z - z; // + ln(2π)/2 Float ln2pi = log(ldexp(Float::pi(wp), 1), wp); result = result + ln2pi * half; // + Stirling correction terms: Σ_{k=1}^{N} B_{2k} / (2k·(2k-1)·z^{2k-1}) Float z_inv = Float(1) / z; z_inv.truncateToApprox(wp); Float z_inv2 = z_inv * z_inv; z_inv2.truncateToApprox(wp); Float z_power = z_inv; // z^{-1}, z^{-3}, z^{-5}, ... // Since Stirling is an asymptotic series, it has a minimum near k = π·z and diverges beyond. // Divergence detection: stop just before the term magnitude stops decreasing (do not add that term). int64_t prev_term_bits = (std::numeric_limits::max)(); for (int k = 1; k <= num_terms; k++) { Float coeff = bernoulli[k] / static_cast(2 * k * (2 * k - 1)); Float term = coeff * z_power; int64_t curr_term_bits = term.isZero() ? (std::numeric_limits::min)() : term.exponent() + static_cast(term.mantissa().bitLength()); // Divergence detection: if the term does not become smaller than the previous one, we have passed the asymptotic series minimum if (k >= 3 && curr_term_bits >= prev_term_bits) break; result = result + term; result.truncateToApprox(wp); prev_term_bits = curr_term_bits; // Convergence check (compare in bits. The old wp comparison had the same unit-mixing bug) if (k >= 3 && term.isZero()) break; if (k >= 3) { auto result_bits = result.exponent() + static_cast(result.mantissa().bitLength()); if (result_bits - curr_term_bits > wp_bits + 5) break; } z_power = z_power * z_inv2; z_power.truncateToApprox(wp); } // Correction for the argument shift result = result - prod_log; finalizeResult(result, eff_x, precision); return result; } Float lnGamma(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) { if (x.isNegative()) return Float::nan(); return Float::positiveInfinity(); } if (x.isZero() || x.isNegative()) { return Float::nan(); } return lnGamma_core(Float(x), x.effectiveBits(), precision); } Float lnGamma(Float&& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) { if (x.isNegative()) return Float::nan(); return Float::positiveInfinity(); } if (x.isZero() || x.isNegative()) { return Float::nan(); } int eff = x.effectiveBits(); return lnGamma_core(std::move(x), eff, precision); } // gamma(x) — gamma function Γ(x) Float gamma(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) { if (x.isNegative()) return Float::nan(); return Float::positiveInfinity(); } // Pole at non-positive integers if (x.isZero()) return Float::positiveInfinity(); // Γ(0) = ±∞ if (x.isNegative() && x.isInteger()) return Float::nan(); // Γ(-n) is undefined int eff_x = x.effectiveBits(); int wp = precision + 15; // Small positive integer: Γ(n) = (n-1)! if (x.isInteger() && x.isPositive()) { int n = static_cast(x.toDouble()); if (n <= 25) { Float result(1); for (int i = 2; i < n; i++) { result = result * i; } finalizeResult(result, eff_x, precision); return result; } } // x > 0: Γ(x) = exp(lnΓ(x)) if (x.isPositive()) { Float lng = lnGamma(x, wp); Float result = exp(lng, wp); finalizeResult(result, eff_x, precision); return result; } // x < 0 (non-integer): reflection formula Γ(x)·Γ(1-x) = π/sin(πx) Float one_minus_x = Float(1) - x; Float lng = lnGamma(one_minus_x, wp); Float gamma_1mx = exp(lng, wp); Float sin_pi_x = sinPi(x, wp); // Γ(x) = π / (sin(πx) · Γ(1-x)) Float pi_val = Float::pi(wp); Float result = pi_val / (sin_pi_x * gamma_1mx); finalizeResult(result, eff_x, precision); return result; } Float gamma(Float&& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) { if (x.isNegative()) return Float::nan(); return Float::positiveInfinity(); } // Pole at non-positive integers if (x.isZero()) return Float::positiveInfinity(); // Γ(0) = ±∞ if (x.isNegative() && x.isInteger()) return Float::nan(); // Γ(-n) is undefined int eff_x = x.effectiveBits(); int wp = precision + 15; // Small positive integer: Γ(n) = (n-1)! if (x.isInteger() && x.isPositive()) { int n = static_cast(x.toDouble()); if (n <= 25) { Float result(1); for (int i = 2; i < n; i++) { result = result * i; } finalizeResult(result, eff_x, precision); return result; } } // x > 0: Γ(x) = exp(lnΓ(x)) if (x.isPositive()) { Float lng = lnGamma(std::move(x), wp); Float result = exp(std::move(lng), wp); finalizeResult(result, eff_x, precision); return result; } // x < 0 (non-integer): reflection formula Γ(x)·Γ(1-x) = π/sin(πx) Float one_minus_x = Float(1) - x; Float lng = lnGamma(std::move(one_minus_x), wp); Float gamma_1mx = exp(std::move(lng), wp); Float sin_pi_x = sinPi(std::move(x), wp); // Γ(x) = π / (sin(πx) · Γ(1-x)) Float pi_val = Float::pi(wp); Float result = pi_val / (sin_pi_x * gamma_1mx); finalizeResult(result, eff_x, precision); return result; } // beta(a, b) — beta function B(a,b) = Γ(a)·Γ(b)/Γ(a+b) Float beta(const Float& a, const Float& b, int precision) { if (a.isNaN() || b.isNaN()) return Float::nan(); int eff = std::min(a.effectiveBits(), b.effectiveBits()); int wp = precision + 15; // B(a,b) = exp(lnΓ(a) + lnΓ(b) - lnΓ(a+b)) // but lnGamma is only usable when a, b are positive if (a.isPositive() && b.isPositive()) { Float lga = lnGamma(a, wp); Float lgb = lnGamma(b, wp); Float lgab = lnGamma(a + b, wp); Float result = exp(lga + lgb - lgab, wp); finalizeResult(result, eff, precision); return result; } // General case: Γ(a)·Γ(b)/Γ(a+b) Float ga = gamma(a, wp); Float gb = gamma(b, wp); Float gab = gamma(a + b, wp); if (gab.isZero()) return Float::nan(); Float result = ga * gb / gab; finalizeResult(result, eff, precision); return result; } Float beta(Float&& a, Float&& b, int precision) { if (a.isNaN() || b.isNaN()) return Float::nan(); int eff = std::min(a.effectiveBits(), b.effectiveBits()); int wp = precision + 15; // B(a,b) = exp(lnΓ(a) + lnΓ(b) - lnΓ(a+b)) if (a.isPositive() && b.isPositive()) { Float lga = lnGamma(a, wp); Float lgb = lnGamma(b, wp); Float lgab = lnGamma(a + b, wp); Float result = exp(lga + lgb - lgab, wp); finalizeResult(result, eff, precision); return result; } // General case: Γ(a)·Γ(b)/Γ(a+b) Float ga = gamma(a, wp); Float gb = gamma(b, wp); Float gab = gamma(a + std::move(b), wp); if (gab.isZero()) return Float::nan(); Float result = ga * gb / gab; finalizeResult(result, eff, precision); return result; } // digamma(x) — digamma function ψ(x) = d/dx ln Γ(x) = Γ'(x)/Γ(x) // Asymptotic expansion: ψ(z) ~ ln(z) - 1/(2z) - Σ B_{2k}/(2k·z^{2k}) static Float digamma_core(Float x, int eff_x, int precision) { int wp = precision + 20; const int wp_bits = Float::precisionToBits(wp); // Convergence check in bits // Negative argument: reflection formula ψ(1-x) - ψ(x) = π·cot(πx) if (x.isNegative()) { Float one_minus_x = Float(1) - x; Float psi_1mx = digamma(one_minus_x, wp); Float pi_val = Float::pi(wp); Float cot_pix = cosPi(x, wp) / sinPi(x, wp); Float result = psi_1mx - pi_val * cot_pix; finalizeResult(result, eff_x, precision); return result; } // Argument shift: ψ(x+1) = ψ(x) + 1/x → ψ(x) = ψ(x+m) - Σ 1/(x+i) // shift_target uses the same formula as lnGamma_core (z > wp · 0.367, which guarantees the // minimum Stirling-asymptotic residual e^{-2π·z} is below 10^{-wp}). The old wp/8 was a // misapplication of the formula assuming wp is in bits, a fatal bug producing only ~52 digits even at prec=200. Float z = std::move(x); z.truncateToApprox(wp); // BUGFIX (2026-05-30): prevent Float(1)/z / Float(1)/zi for exact inputs from // falling to exact/exact → default_precision (53 digits) // (same family as lnGamma_core). Make non-exact at working precision. z.setEffectiveBits(wp_bits); // wp · ln(10) / (2π) ≈ 0.367 is the theoretical minimum. Strengthen the margin to 0.45 (~10-15 extra digits) // to absorb the residual ~ √(2πz) e^{-2π z} + accumulation in the shift stages + later cancellation. const double shift_target = std::max(20.0, wp * 0.45 + 8.0); int m = 0; Float shift_sum(0); double z_approx = z.toDouble(); if (z_approx < shift_target) { m = static_cast(shift_target - z_approx) + 1; for (int i = 0; i < m; i++) { Float zi = z + Float(i); zi.truncateToApprox(wp); shift_sum = shift_sum + Float(1) / zi; } z = z + Float(m); z.truncateToApprox(wp); } // Asymptotic expansion: ψ(z) ~ ln(z) - 1/(2z) - Σ_{k=1}^{N} B_{2k}/(2k·z^{2k}) // Optimal truncation k* ≈ π·z (same reason as lnGamma_core). The old wp/(2·log2(z)) was the wrong formula. constexpr double PI_VAL = 3.141592653589793238462643383279502884; int num_terms = static_cast(PI_VAL * z.toDouble()) + 10; if (num_terms < 5) num_terms = 5; int num_terms_cap = static_cast(PI_VAL * shift_target * 1.2 + 100.0); if (num_terms_cap < 2000) num_terms_cap = 2000; if (num_terms > num_terms_cap) num_terms = num_terms_cap; // Use the Bernoulli cache (the old computeBernoulliNumbers recomputed every time) const auto& bernoulli = getBernoulliCached(num_terms, wp); Float result = log(z, wp) - ldexp(Float(1) / z, -1); Float z_inv = Float(1) / z; z_inv.truncateToApprox(wp); Float z_inv2 = z_inv * z_inv; z_inv2.truncateToApprox(wp); Float z_power = z_inv2; // z^{-2}, z^{-4}, ... // Divergence detection: stop once past the minimum of the Stirling asymptotic series (like lnGamma_core) int64_t prev_term_bits = (std::numeric_limits::max)(); for (int k = 1; k <= num_terms; k++) { Float coeff = bernoulli[k] / static_cast(2 * k); Float term = coeff * z_power; int64_t curr_term_bits = term.isZero() ? (std::numeric_limits::min)() : term.exponent() + static_cast(term.mantissa().bitLength()); if (k >= 3 && curr_term_bits >= prev_term_bits) break; result = result - term; result.truncateToApprox(wp); prev_term_bits = curr_term_bits; if (k >= 3 && term.isZero()) break; if (k >= 3) { auto result_bits = result.exponent() + static_cast(result.mantissa().bitLength()); if (result_bits - curr_term_bits > wp_bits + 5) break; } z_power = z_power * z_inv2; z_power.truncateToApprox(wp); } result = result - shift_sum; finalizeResult(result, eff_x, precision); return result; } Float digamma(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) { if (x.isNegative()) return Float::nan(); return Float::positiveInfinity(); } if (x.isZero()) return Float::nan(); // Non-positive integer: pole if (x.isNegative() && x.isInteger()) return Float::nan(); return digamma_core(Float(x), x.effectiveBits(), precision); } Float digamma(Float&& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) { if (x.isNegative()) return Float::nan(); return Float::positiveInfinity(); } if (x.isZero()) return Float::nan(); // Non-positive integer: pole if (x.isNegative() && x.isInteger()) return Float::nan(); int eff = x.effectiveBits(); return digamma_core(std::move(x), eff, precision); } // trigamma(x) — trigamma function ψ₁(x) = d²/dx² ln Γ(x) // Asymptotic expansion: ψ₁(z) ~ 1/z + 1/(2z²) + Σ B_{2k}/(z^{2k+1}) static Float trigamma_core(Float x, int eff_x, int precision) { int wp = precision + 20; const int wp_bits = Float::precisionToBits(wp); // Convergence check in bits // Negative argument: reflection formula ψ₁(1-x) + ψ₁(x) = π²/sin²(πx) if (x.isNegative()) { Float one_minus_x = Float(1) - x; Float psi1_1mx = trigamma(one_minus_x, wp); Float pi_val = Float::pi(wp); Float sin_pix = sinPi(x, wp); Float pi2_over_sin2 = (pi_val * pi_val) / (sin_pix * sin_pix); Float result = pi2_over_sin2 - psi1_1mx; finalizeResult(result, eff_x, precision); return result; } // Argument shift: ψ₁(x+1) = ψ₁(x) - 1/x² → ψ₁(x) = ψ₁(x+m) + Σ 1/(x+i)² // BUGFIX (2026-05-30): the efe6db2c fix for lnGamma/digamma had not been applied to trigamma. // (1) shift_target = wp/8 was a misapplied bit formula, capping at ~74 digits // → wp·0.45+8. (2) Float(1)/z etc. for exact inputs fell to default_precision // → make z non-exact. (3) num_terms = wp/(2·log2 z) was the wrong formula // → π·z + 10 + divergence detection. (4) the convergence check wp+5 mixed bits vs digits → wp_bits+5. Float z = std::move(x); z.truncateToApprox(wp); z.setEffectiveBits(wp_bits); const double shift_target = std::max(20.0, wp * 0.45 + 8.0); int m = 0; Float shift_sum(0); double z_approx = z.toDouble(); if (z_approx < shift_target) { m = static_cast(shift_target - z_approx) + 1; for (int i = 0; i < m; i++) { Float zi = z + Float(i); zi.truncateToApprox(wp); Float zi_inv = Float(1) / zi; shift_sum = shift_sum + zi_inv * zi_inv; } z = z + Float(m); z.truncateToApprox(wp); } // Asymptotic expansion: ψ₁(z) ~ 1/z + 1/(2z²) + Σ_{k=1}^{N} B_{2k}/z^{2k+1} // Optimal truncation k* ≈ π·z (same reason as lnGamma_core/digamma_core). constexpr double PI_VAL = 3.141592653589793238462643383279502884; int num_terms = static_cast(PI_VAL * z.toDouble()) + 10; if (num_terms < 5) num_terms = 5; int num_terms_cap = static_cast(PI_VAL * shift_target * 1.2 + 100.0); if (num_terms_cap < 2000) num_terms_cap = 2000; if (num_terms > num_terms_cap) num_terms = num_terms_cap; const auto& bernoulli = getBernoulliCached(num_terms, wp); Float z_inv = Float(1) / z; z_inv.truncateToApprox(wp); Float z_inv2 = z_inv * z_inv; z_inv2.truncateToApprox(wp); Float result = z_inv + ldexp(z_inv2, -1); Float z_power = z_inv2 * z_inv; // z^{-3}, z^{-5}, ... // Divergence detection: stop just before passing the minimum of the asymptotic series. int64_t prev_term_bits = (std::numeric_limits::max)(); for (int k = 1; k <= num_terms; k++) { Float term = bernoulli[k] * z_power; int64_t curr_term_bits = term.isZero() ? (std::numeric_limits::min)() : term.exponent() + static_cast(term.mantissa().bitLength()); if (k >= 3 && curr_term_bits >= prev_term_bits) break; result = result + term; result.truncateToApprox(wp); prev_term_bits = curr_term_bits; if (k >= 3 && term.isZero()) break; if (k >= 3) { auto result_bits = result.exponent() + static_cast(result.mantissa().bitLength()); if (result_bits - curr_term_bits > wp_bits + 5) break; } z_power = z_power * z_inv2; z_power.truncateToApprox(wp); } result = result + shift_sum; finalizeResult(result, eff_x, precision); return result; } Float trigamma(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) { if (x.isNegative()) return Float::nan(); return Float(0); // ψ₁(∞) = 0 } if (x.isZero()) return Float::nan(); // Non-positive integer: pole if (x.isNegative() && x.isInteger()) return Float::nan(); return trigamma_core(Float(x), x.effectiveBits(), precision); } Float trigamma(Float&& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) { if (x.isNegative()) return Float::nan(); return Float(0); // ψ₁(∞) = 0 } if (x.isZero()) return Float::nan(); // Non-positive integer: pole if (x.isNegative() && x.isInteger()) return Float::nan(); int eff = x.effectiveBits(); return trigamma_core(std::move(x), eff, precision); } // polygamma(n, x) — polygamma function ψ^(n)(x) = d^{n+1}/dx^{n+1} ln Γ(x) // n=0: digamma, n=1: trigamma (delegate to existing implementations) // n≥2: asymptotic expansion + argument shift // ψ^(n)(z) = (-1)^{n+1} [(n-1)!/z^n + n!/(2z^{n+1}) // + Σ_{k=1}^{K} B_{2k} · (2k+n-1)!/((2k)!) / z^{2k+n}] Float polygamma(int n, const Float& x, int precision) { Float::PrecisionScope _ps(precision + 64); // Compute exact÷exact inside the function at working precision if (n < 0) { throw std::invalid_argument("polygamma: n must be non-negative"); } if (n == 0) return digamma(x, precision); if (n == 1) return trigamma(x, precision); // n >= 2 int eff_x = x.effectiveBits(); if (x.isNaN()) return Float::nan(); if (x.isInfinity()) { if (x.isNegative()) return Float::nan(); return Float(0); // ψ^(n)(+∞) = 0 (n ≥ 1) } if (x.isZero()) return Float::nan(); if (x.isNegative() && x.isInteger()) return Float::nan(); int wp = precision + 20; const int wp_bits = Float::precisionToBits(wp); // Convergence check in bits // Compute n! and (n-1)! Float n_fact(1); for (int i = 2; i <= n; i++) n_fact = n_fact * i; Float n_minus_1_fact = n_fact / n; // Argument shift: ψ^(n)(z) = ψ^(n)(z+m) + (-1)^{n+1}·n!·Σ 1/(z+i)^{n+1} // BUGFIX (2026-05-30): fixed the same kind of unit-mixing as trigamma/digamma. // (1) shift_target = wp·0.35 was too small for the asymptotic series' convergence (driving the // optimal-truncation error ~ e^{-2πz} (k*≈πz) down to wp_bits bits requires z≳0.4·wp) // → align with trigamma to wp·0.45+8. (2) num_terms = wp/(2·log2 z) was a // misuse of a bit formula assuming a geometric series → π·z + 10 + divergence detection. // (3) the convergence check wp+5 mixed bits vs digits → wp_bits+5. Float z = x; z.truncateToApprox(wp); z.setEffectiveBits(wp_bits); Float shift_sum(0); double shift_target = std::max(20.0, wp * 0.45 + 8.0); double z_approx = z.toDouble(); if (z_approx < shift_target) { int m = static_cast(std::ceil(shift_target - z_approx)) + 1; for (int i = 0; i < m; i++) { Float zi = z + Float(i); zi.truncateToApprox(wp); // Compute zi^{n+1} by repeated multiplication Float zi_pow = zi; for (int j = 1; j <= n; j++) { zi_pow = zi_pow * zi; zi_pow.truncateToApprox(wp); } shift_sum = shift_sum + Float(1) / zi_pow; } z = z + Float(m); z.truncateToApprox(wp); } // Asymptotic expansion: optimal truncation k* ≈ π·z (same reason as trigamma/digamma/lnGamma). constexpr double PI_VAL = 3.141592653589793238462643383279502884; int num_terms = static_cast(PI_VAL * z.toDouble()) + 10; if (num_terms < 5) num_terms = 5; int num_terms_cap = static_cast(PI_VAL * shift_target * 1.2 + 100.0); if (num_terms_cap < 2000) num_terms_cap = 2000; if (num_terms > num_terms_cap) num_terms = num_terms_cap; auto bernoulli = computeBernoulliNumbers(num_terms, wp); Float z_inv = Float(1) / z; z_inv.truncateToApprox(wp); Float z_inv2 = z_inv * z_inv; z_inv2.truncateToApprox(wp); // z^{-n} Float z_inv_n(1); for (int i = 0; i < n; i++) { z_inv_n = z_inv_n * z_inv; z_inv_n.truncateToApprox(wp); } // First two terms: (n-1)!/z^n + n!/(2z^{n+1}) Float A = n_minus_1_fact * z_inv_n + ldexp(n_fact * z_inv_n * z_inv, -1); A.truncateToApprox(wp); // Σ B_{2k} · R(n,k) / z^{2k+n} // R(n,k) = (2k+n-1)!/(2k)! = Π_{j=1}^{n-1} (2k+j) Float z_power = z_inv_n * z_inv2; // z^{-(n+2)} int64_t prev_term_bits = (std::numeric_limits::max)(); for (int k = 1; k <= num_terms; k++) { Float R(1); for (int j = 1; j <= n - 1; j++) { R = R * (2 * k + j); } Float term = bernoulli[k] * R * z_power; int64_t curr_term_bits = term.isZero() ? (std::numeric_limits::min)() : term.exponent() + static_cast(term.mantissa().bitLength()); // Divergence detection: stop just before passing the smallest term of the asymptotic series. if (k >= 3 && curr_term_bits >= prev_term_bits) break; A = A + term; A.truncateToApprox(wp); prev_term_bits = curr_term_bits; if (k >= 3 && term.isZero()) break; if (k >= 3) { auto A_bits = A.exponent() + static_cast(A.mantissa().bitLength()); if (A_bits - curr_term_bits > wp_bits + 5) break; } z_power = z_power * z_inv2; z_power.truncateToApprox(wp); } // Sign: (-1)^{n+1} Float result = (n % 2 == 0) ? -A : A; // Shift correction: (-1)^{n+1} · n! · shift_sum if (!shift_sum.isZero()) { Float correction = n_fact * shift_sum; if (n % 2 == 0) correction = -correction; result = result + correction; } finalizeResult(result, eff_x, precision); return result; } Float polygamma(int n, Float&& x, int precision) { Float::PrecisionScope _ps(precision + 64); // Compute exact÷exact inside the function at working precision if (n < 0) { throw std::invalid_argument("polygamma: n must be non-negative"); } if (n == 0) return digamma(std::move(x), precision); if (n == 1) return trigamma(std::move(x), precision); // n >= 2 int eff_x = x.effectiveBits(); if (x.isNaN()) return Float::nan(); if (x.isInfinity()) { if (x.isNegative()) return Float::nan(); return Float(0); // ψ^(n)(+∞) = 0 (n ≥ 1) } if (x.isZero()) return Float::nan(); if (x.isNegative() && x.isInteger()) return Float::nan(); int wp = precision + 20; const int wp_bits = Float::precisionToBits(wp); // Convergence check in bits // Compute n! and (n-1)! Float n_fact(1); for (int i = 2; i <= n; i++) n_fact = n_fact * i; Float n_minus_1_fact = n_fact / n; // Argument shift: ψ^(n)(z) = ψ^(n)(z+m) + (-1)^{n+1}·n!·Σ 1/(z+i)^{n+1} // BUGFIX (2026-05-30): fixed the same kind of unit-mixing as trigamma/digamma // (identical to the const Float& version; see its comments for details). Float z = std::move(x); z.truncateToApprox(wp); z.setEffectiveBits(wp_bits); Float shift_sum(0); double shift_target = std::max(20.0, wp * 0.45 + 8.0); double z_approx = z.toDouble(); if (z_approx < shift_target) { int m = static_cast(std::ceil(shift_target - z_approx)) + 1; for (int i = 0; i < m; i++) { Float zi = z + Float(i); zi.truncateToApprox(wp); // Compute zi^{n+1} by repeated multiplication Float zi_pow = zi; for (int j = 1; j <= n; j++) { zi_pow = zi_pow * zi; zi_pow.truncateToApprox(wp); } shift_sum = shift_sum + Float(1) / zi_pow; } z = z + Float(m); z.truncateToApprox(wp); } // Asymptotic expansion: optimal truncation k* ≈ π·z (same reason as trigamma/digamma/lnGamma). constexpr double PI_VAL = 3.141592653589793238462643383279502884; int num_terms = static_cast(PI_VAL * z.toDouble()) + 10; if (num_terms < 5) num_terms = 5; int num_terms_cap = static_cast(PI_VAL * shift_target * 1.2 + 100.0); if (num_terms_cap < 2000) num_terms_cap = 2000; if (num_terms > num_terms_cap) num_terms = num_terms_cap; auto bernoulli = computeBernoulliNumbers(num_terms, wp); Float z_inv = Float(1) / z; z_inv.truncateToApprox(wp); Float z_inv2 = z_inv * z_inv; z_inv2.truncateToApprox(wp); // z^{-n} Float z_inv_n(1); for (int i = 0; i < n; i++) { z_inv_n = z_inv_n * z_inv; z_inv_n.truncateToApprox(wp); } // First two terms: (n-1)!/z^n + n!/(2z^{n+1}) Float A = n_minus_1_fact * z_inv_n + ldexp(n_fact * z_inv_n * z_inv, -1); A.truncateToApprox(wp); // Σ B_{2k} · R(n,k) / z^{2k+n} // R(n,k) = (2k+n-1)!/(2k)! = Π_{j=1}^{n-1} (2k+j) Float z_power = z_inv_n * z_inv2; // z^{-(n+2)} int64_t prev_term_bits = (std::numeric_limits::max)(); for (int k = 1; k <= num_terms; k++) { Float R(1); for (int j = 1; j <= n - 1; j++) { R = R * (2 * k + j); } Float term = bernoulli[k] * R * z_power; int64_t curr_term_bits = term.isZero() ? (std::numeric_limits::min)() : term.exponent() + static_cast(term.mantissa().bitLength()); // Divergence detection: stop just before passing the smallest term of the asymptotic series. if (k >= 3 && curr_term_bits >= prev_term_bits) break; A = A + term; A.truncateToApprox(wp); prev_term_bits = curr_term_bits; if (k >= 3 && term.isZero()) break; if (k >= 3) { auto A_bits = A.exponent() + static_cast(A.mantissa().bitLength()); if (A_bits - curr_term_bits > wp_bits + 5) break; } z_power = z_power * z_inv2; z_power.truncateToApprox(wp); } // Sign: (-1)^{n+1} Float result = (n % 2 == 0) ? -A : A; // Shift correction: (-1)^{n+1} · n! · shift_sum if (!shift_sum.isZero()) { Float correction = n_fact * shift_sum; if (n % 2 == 0) correction = -correction; result = result + correction; } finalizeResult(result, eff_x, precision); return result; } //============================================================================= // Incomplete gamma functions //============================================================================= // gammaP — regularized lower incomplete gamma function P(a,x) = γ(a,x)/Γ(a) // x < a+1: Taylor series // P(a,x) = exp(-x + a·ln(x) - lnΓ(a)) · Σ_{n≥0} x^n / (a·(a+1)···(a+n)) // x ≥ a+1: 1 - Q(a,x) (CF is faster) Float gammaP(const Float& a, const Float& x, int precision) { if (a.isNaN() || x.isNaN()) return Float::nan(); if (x.isZero()) return Float(0); // P(a,0) = 0 if (x.isNegative()) return Float::nan(); // x ≥ 0 only if (!a.isPositive()) return Float::nan(); // a > 0 only if (x.isInfinity()) return Float(1); // P(a,+∞) = 1 int eff = std::min(a.effectiveBits(), x.effectiveBits()); int wp = precision + 20; double a_d = a.toDouble(); double x_d = x.toDouble(); if (x_d < a_d + 1.0) { // Taylor series // BUGFIX (2026-05-30): for exact inputs (a=3,x=2 etc.), exact/exact divisions like // Float(1)/a_wp fall to default_precision. Make the working values non-exact. const int wp_bits = Float::precisionToBits(wp); Float a_wp = a; a_wp.truncateToApprox(wp); a_wp.setEffectiveBits(wp_bits); Float x_wp = x; x_wp.truncateToApprox(wp); x_wp.setEffectiveBits(wp_bits); Float front = exp(-x_wp + a_wp * log(x_wp, wp) - lnGamma(a_wp, wp), wp); // S = Σ x^n / Π_{k=0}^{n} (a+k) // term_0 = 1/a, term_n = term_{n-1} · x / (a+n) Float term = Float(1) / a_wp; Float sum = term; for (int n = 1; n < 10000; n++) { term = term * x_wp / (a_wp + Float(n)); term.truncateToApprox(wp); sum = sum + term; sum.truncateToApprox(wp); if (n >= 3 && term.isZero()) break; if (n >= 3) { int64_t t_bits = term.exponent() + static_cast(term.mantissa().bitLength()); int64_t s_bits = sum.exponent() + static_cast(sum.mantissa().bitLength()); if (s_bits - t_bits > Float::precisionToBits(wp) + 5) break; } } Float result = front * sum; finalizeResult(result, eff, precision); return result; } else { // x ≥ a+1: compute Q via CF and return 1 - Q Float q = gammaQ(a, x, wp); Float result = Float(1) - q; finalizeResult(result, eff, precision); return result; } } Float gammaP(Float&& a, Float&& x, int precision) { if (a.isNaN() || x.isNaN()) return Float::nan(); if (x.isZero()) return Float(0); // P(a,0) = 0 if (x.isNegative()) return Float::nan(); // x ≥ 0 only if (!a.isPositive()) return Float::nan(); // a > 0 only if (x.isInfinity()) return Float(1); // P(a,+∞) = 1 int eff = std::min(a.effectiveBits(), x.effectiveBits()); int wp = precision + 20; double a_d = a.toDouble(); double x_d = x.toDouble(); if (x_d < a_d + 1.0) { // Taylor series const int wp_bits = Float::precisionToBits(wp); // avoid exact/exact poison Float a_wp = std::move(a); a_wp.truncateToApprox(wp); a_wp.setEffectiveBits(wp_bits); Float x_wp = std::move(x); x_wp.truncateToApprox(wp); x_wp.setEffectiveBits(wp_bits); Float front = exp(-x_wp + a_wp * log(x_wp, wp) - lnGamma(a_wp, wp), wp); Float term = Float(1) / a_wp; Float sum = term; for (int n = 1; n < 10000; n++) { term = term * x_wp / (a_wp + Float(n)); term.truncateToApprox(wp); sum = sum + term; sum.truncateToApprox(wp); if (n >= 3 && term.isZero()) break; if (n >= 3) { int64_t t_bits = term.exponent() + static_cast(term.mantissa().bitLength()); int64_t s_bits = sum.exponent() + static_cast(sum.mantissa().bitLength()); if (s_bits - t_bits > Float::precisionToBits(wp) + 5) break; } } Float result = front * sum; finalizeResult(result, eff, precision); return result; } else { Float q = gammaQ(std::move(a), std::move(x), wp); Float result = Float(1) - q; finalizeResult(result, eff, precision); return result; } } // gammaQ — regularized upper incomplete gamma function Q(a,x) = Γ(a,x)/Γ(a) // x ≥ a+1: Legendre continued fraction (Modified Lentz) // Q(a,x) = exp(-x + a·ln(x) - lnΓ(a)) / f // f = (x+1-a) + K_{n≥1} [-n(n-a) / (x+2n+1-a)] // x < a+1: 1 - P(a,x) (Taylor is faster) Float gammaQ(const Float& a, const Float& x, int precision) { if (a.isNaN() || x.isNaN()) return Float::nan(); if (x.isZero()) return Float(1); // Q(a,0) = 1 if (x.isNegative()) return Float::nan(); if (!a.isPositive()) return Float::nan(); if (x.isInfinity()) return Float(0); // Q(a,+∞) = 0 int eff = std::min(a.effectiveBits(), x.effectiveBits()); int wp = precision + 20; double a_d = a.toDouble(); double x_d = x.toDouble(); if (x_d >= a_d + 1.0) { // Legendre CF // BUGFIX (2026-05-30): avoid exact/exact division poison for exact inputs. const int wp_bits = Float::precisionToBits(wp); Float a_wp = a; a_wp.truncateToApprox(wp); a_wp.setEffectiveBits(wp_bits); Float x_wp = x; x_wp.truncateToApprox(wp); x_wp.setEffectiveBits(wp_bits); Float front = exp(-x_wp + a_wp * log(x_wp, wp) - lnGamma(a_wp, wp), wp); // Modified Lentz Float tiny(1, -wp * 4); // 2^{-4·wp} (to avoid zero) Float b0 = x_wp + Float(1) - a_wp; Float f = b0.isZero() ? tiny : b0; f.truncateToApprox(wp); Float C = f; Float D(0); for (int n = 1; n < 10000; n++) { Float an = Float(-n) * (Float(n) - a_wp); Float bn = x_wp + Float(2 * n + 1) - a_wp; D = bn + an * D; D.truncateToApprox(wp); if (D.isZero()) D = tiny; D = Float(1) / D; D.truncateToApprox(wp); C = bn + an / C; C.truncateToApprox(wp); if (C.isZero()) C = tiny; Float delta = C * D; f = f * delta; f.truncateToApprox(wp); Float diff = delta - Float(1); if (diff.isZero()) break; if (n >= 3) { int64_t diff_bits = diff.exponent() + static_cast(diff.mantissa().bitLength()); if (diff_bits < -(Float::precisionToBits(wp) + 5)) break; } } Float result = front / f; finalizeResult(result, eff, precision); return result; } else { // x < a+1: compute P via Taylor and return 1 - P Float p = gammaP(a, x, wp); Float result = Float(1) - p; finalizeResult(result, eff, precision); return result; } } Float gammaQ(Float&& a, Float&& x, int precision) { if (a.isNaN() || x.isNaN()) return Float::nan(); if (x.isZero()) return Float(1); // Q(a,0) = 1 if (x.isNegative()) return Float::nan(); if (!a.isPositive()) return Float::nan(); if (x.isInfinity()) return Float(0); // Q(a,+∞) = 0 int eff = std::min(a.effectiveBits(), x.effectiveBits()); int wp = precision + 20; double a_d = a.toDouble(); double x_d = x.toDouble(); if (x_d >= a_d + 1.0) { // Legendre CF const int wp_bits = Float::precisionToBits(wp); // avoid exact/exact poison Float a_wp = std::move(a); a_wp.truncateToApprox(wp); a_wp.setEffectiveBits(wp_bits); Float x_wp = std::move(x); x_wp.truncateToApprox(wp); x_wp.setEffectiveBits(wp_bits); Float front = exp(-x_wp + a_wp * log(x_wp, wp) - lnGamma(a_wp, wp), wp); // Modified Lentz Float tiny(1, -wp * 4); Float b0 = x_wp + Float(1) - a_wp; Float f = b0.isZero() ? tiny : b0; f.truncateToApprox(wp); Float C = f; Float D(0); for (int n = 1; n < 10000; n++) { Float an = Float(-n) * (Float(n) - a_wp); Float bn = x_wp + Float(2 * n + 1) - a_wp; D = bn + an * D; D.truncateToApprox(wp); if (D.isZero()) D = tiny; D = Float(1) / D; D.truncateToApprox(wp); C = bn + an / C; C.truncateToApprox(wp); if (C.isZero()) C = tiny; Float delta = C * D; f = f * delta; f.truncateToApprox(wp); Float diff = delta - Float(1); if (diff.isZero()) break; if (n >= 3) { int64_t diff_bits = diff.exponent() + static_cast(diff.mantissa().bitLength()); if (diff_bits < -(Float::precisionToBits(wp) + 5)) break; } } Float result = front / f; finalizeResult(result, eff, precision); return result; } else { Float p = gammaP(std::move(a), std::move(x), wp); Float result = Float(1) - p; finalizeResult(result, eff, precision); return result; } } // gammaLower — lower incomplete gamma γ(a,x) = P(a,x)·Γ(a) Float gammaLower(const Float& a, const Float& x, int precision) { if (a.isNaN() || x.isNaN()) return Float::nan(); if (x.isZero()) return Float(0); if (x.isNegative()) return Float::nan(); if (!a.isPositive()) return Float::nan(); int eff = std::min(a.effectiveBits(), x.effectiveBits()); int wp = precision + 15; Float p = gammaP(a, x, wp); Float g = gamma(a, wp); Float result = p * g; finalizeResult(result, eff, precision); return result; } Float gammaLower(Float&& a, Float&& x, int precision) { if (a.isNaN() || x.isNaN()) return Float::nan(); if (x.isZero()) return Float(0); if (x.isNegative()) return Float::nan(); if (!a.isPositive()) return Float::nan(); int eff = std::min(a.effectiveBits(), x.effectiveBits()); int wp = precision + 15; Float p = gammaP(a, x, wp); Float g = gamma(std::move(a), wp); Float result = p * g; finalizeResult(result, eff, precision); return result; } // gammaUpper — upper incomplete gamma Γ(a,x) = Q(a,x)·Γ(a) Float gammaUpper(const Float& a, const Float& x, int precision) { if (a.isNaN() || x.isNaN()) return Float::nan(); if (x.isZero()) return gamma(a, precision); // Γ(a,0) = Γ(a) if (x.isNegative()) return Float::nan(); if (!a.isPositive()) return Float::nan(); if (x.isInfinity()) return Float(0); int eff = std::min(a.effectiveBits(), x.effectiveBits()); int wp = precision + 15; Float q = gammaQ(a, x, wp); Float g = gamma(a, wp); Float result = q * g; finalizeResult(result, eff, precision); return result; } Float gammaUpper(Float&& a, Float&& x, int precision) { if (a.isNaN() || x.isNaN()) return Float::nan(); if (x.isZero()) return gamma(std::move(a), precision); // Γ(a,0) = Γ(a) if (x.isNegative()) return Float::nan(); if (!a.isPositive()) return Float::nan(); if (x.isInfinity()) return Float(0); int eff = std::min(a.effectiveBits(), x.effectiveBits()); int wp = precision + 15; Float q = gammaQ(a, x, wp); Float g = gamma(std::move(a), wp); Float result = q * g; finalizeResult(result, eff, precision); return result; } //============================================================================= // Incomplete beta function //============================================================================= // betaRegularized — regularized incomplete beta function I_x(a,b) // Continued-fraction expansion (Modified Lentz) + symmetry // I_x(a,b) = front / (1 + d₁/(1 + d₂/(1 + ...))) // d_{2m+1} = -(a+m)(a+b+m)x / ((a+2m)(a+2m+1)) // d_{2m} = m(b-m)x / ((a+2m-1)(a+2m)) // When x ≥ (a+1)/(a+b+2): invert via I_x(a,b) = 1 - I_{1-x}(b,a) Float betaRegularized(const Float& x, const Float& a, const Float& b, int precision) { if (x.isNaN() || a.isNaN() || b.isNaN()) return Float::nan(); if (x.isZero()) return Float(0); // I_0(a,b) = 0 Float one(1); if (x == one) return Float(1); // I_1(a,b) = 1 if (x.isNegative() || x > one) return Float::nan(); // x ∈ [0,1] if (!a.isPositive() || !b.isPositive()) return Float::nan(); int eff = std::min({x.effectiveBits(), a.effectiveBits(), b.effectiveBits()}); int wp = precision + 20; Float x_wp = x; x_wp.truncateToApprox(wp); Float a_wp = a; a_wp.truncateToApprox(wp); Float b_wp = b; b_wp.truncateToApprox(wp); // Symmetry-based inversion decision: invert if x ≥ (a+1)/(a+b+2) double x_d = x.toDouble(); double a_d = a.toDouble(); double b_d = b.toDouble(); bool flip = x_d >= (a_d + 1.0) / (a_d + b_d + 2.0); Float xx, aa, bb; if (flip) { xx = Float(1) - x_wp; xx.truncateToApprox(wp); aa = b_wp; bb = a_wp; } else { xx = x_wp; aa = a_wp; bb = b_wp; } // front = x^a · (1-x)^b / (a · B(a,b)) // = exp(a·ln(x) + b·ln(1-x) + lnΓ(a+b) - lnΓ(a) - lnΓ(b)) / a Float ln_front = aa * log(xx, wp) + bb * log(Float(1) - xx, wp) + lnGamma(aa + bb, wp) - lnGamma(aa, wp) - lnGamma(bb, wp); Float front = exp(ln_front, wp) / aa; front.truncateToApprox(wp); // CF: evaluate 1 + d_1/(1 + d_2/(1 + ...)) via Modified Lentz Float tiny(1, -wp * 4); Float f(1); Float C(1); Float D(0); for (int n = 1; n < 10000; n++) { Float d; if (n % 2 == 1) { // d_{2m+1}: m = (n-1)/2 int m = (n - 1) / 2; Float am = aa + Float(m); Float abm = aa + bb + Float(m); Float denom = (aa + Float(2 * m)) * (aa + Float(2 * m + 1)); d = -(am * abm * xx) / denom; } else { // d_{2m}: m = n/2 int m = n / 2; Float fm(m); Float bm = bb - Float(m); Float denom = (aa + Float(2 * m - 1)) * (aa + Float(2 * m)); d = (fm * bm * xx) / denom; } d.truncateToApprox(wp); D = Float(1) + d * D; D.truncateToApprox(wp); if (D.isZero()) D = tiny; D = Float(1) / D; D.truncateToApprox(wp); C = Float(1) + d / C; C.truncateToApprox(wp); if (C.isZero()) C = tiny; Float delta = C * D; f = f * delta; f.truncateToApprox(wp); if (n >= 3) { Float diff = delta - Float(1); if (diff.isZero()) break; int64_t diff_bits = diff.exponent() + static_cast(diff.mantissa().bitLength()); if (diff_bits < -(Float::precisionToBits(wp) + 5)) break; } } Float result = front / f; if (flip) { result = Float(1) - result; } finalizeResult(result, eff, precision); return result; } Float betaRegularized(Float&& x, Float&& a, Float&& b, int precision) { if (x.isNaN() || a.isNaN() || b.isNaN()) return Float::nan(); if (x.isZero()) return Float(0); // I_0(a,b) = 0 Float one(1); if (x == one) return Float(1); // I_1(a,b) = 1 if (x.isNegative() || x > one) return Float::nan(); // x ∈ [0,1] if (!a.isPositive() || !b.isPositive()) return Float::nan(); int eff = std::min({x.effectiveBits(), a.effectiveBits(), b.effectiveBits()}); int wp = precision + 20; Float x_wp = std::move(x); x_wp.truncateToApprox(wp); Float a_wp = std::move(a); a_wp.truncateToApprox(wp); Float b_wp = std::move(b); b_wp.truncateToApprox(wp); // Symmetry-based inversion decision: invert if x ≥ (a+1)/(a+b+2) double x_d = x_wp.toDouble(); double a_d = a_wp.toDouble(); double b_d = b_wp.toDouble(); bool flip = x_d >= (a_d + 1.0) / (a_d + b_d + 2.0); Float xx, aa, bb; if (flip) { xx = Float(1) - x_wp; xx.truncateToApprox(wp); aa = b_wp; bb = a_wp; } else { xx = x_wp; aa = a_wp; bb = b_wp; } // front = x^a · (1-x)^b / (a · B(a,b)) Float ln_front = aa * log(xx, wp) + bb * log(Float(1) - xx, wp) + lnGamma(aa + bb, wp) - lnGamma(aa, wp) - lnGamma(bb, wp); Float front = exp(ln_front, wp) / aa; front.truncateToApprox(wp); // CF: Modified Lentz Float tiny(1, -wp * 4); Float f(1); Float C(1); Float D(0); for (int n = 1; n < 10000; n++) { Float d; if (n % 2 == 1) { int m = (n - 1) / 2; Float am = aa + Float(m); Float abm = aa + bb + Float(m); Float denom = (aa + Float(2 * m)) * (aa + Float(2 * m + 1)); d = -(am * abm * xx) / denom; } else { int m = n / 2; Float fm(m); Float bm = bb - Float(m); Float denom = (aa + Float(2 * m - 1)) * (aa + Float(2 * m)); d = (fm * bm * xx) / denom; } d.truncateToApprox(wp); D = Float(1) + d * D; D.truncateToApprox(wp); if (D.isZero()) D = tiny; D = Float(1) / D; D.truncateToApprox(wp); C = Float(1) + d / C; C.truncateToApprox(wp); if (C.isZero()) C = tiny; Float delta = C * D; f = f * delta; f.truncateToApprox(wp); if (n >= 3) { Float diff = delta - Float(1); if (diff.isZero()) break; int64_t diff_bits = diff.exponent() + static_cast(diff.mantissa().bitLength()); if (diff_bits < -(Float::precisionToBits(wp) + 5)) break; } } Float result = front / f; if (flip) { result = Float(1) - result; } finalizeResult(result, eff, precision); return result; } //============================================================================= // Elliptic integrals — Carlson symmetric forms //============================================================================= // R_C(x, y) = (1/2)∫₀^∞ dt / [(t+y)√(t+x)] // Quadratically convergent iteration via the duplication theorem static Float carlsonRC_core(Float xw, Float yw, int eff, int precision) { Float::PrecisionScope _ps(precision + 64); // Compute exact÷exact inside the function at working precision int wp = precision + 20; xw.truncateToApprox(wp); yw.truncateToApprox(wp); for (int iter = 0; iter < 200; iter++) { Float lam = ldexp(sqrt(xw * yw, wp), 1) + yw; xw = ldexp(xw + lam, -2); xw.truncateToApprox(wp); yw = ldexp(yw + lam, -2); yw.truncateToApprox(wp); Float A = (xw + yw + yw) / 3; Float s = (yw - A) / A; if (s.isZero()) break; int64_t s_bits = s.exponent() + static_cast(s.mantissa().bitLength()); if (s_bits < -(Float::precisionToBits(wp) + 5)) break; } Float A = (xw + yw + yw) / 3; Float s = (yw - A) / A; // 1 + (3/10)s² + (1/7)s³ + (3/8)s⁴ + (9/22)s⁵ Float poly = Float(1) + s * s * (Float(3) / 10 + s * (Float(1) / 7 + s * (Float(3) / 8 + s * 9 / 22))); Float result = poly / sqrt(A, wp); finalizeResult(result, eff, precision); return result; } Float carlsonRC(const Float& x, const Float& y, int precision) { if (x.isNaN() || y.isNaN()) return Float::nan(); if (x.isNegative() || !y.isPositive()) return Float::nan(); int eff = std::min(x.effectiveBits(), y.effectiveBits()); return carlsonRC_core(Float(x), Float(y), eff, precision); } Float carlsonRC(Float&& x, Float&& y, int precision) { if (x.isNaN() || y.isNaN()) return Float::nan(); if (x.isNegative() || !y.isPositive()) return Float::nan(); int eff = std::min(x.effectiveBits(), y.effectiveBits()); return carlsonRC_core(std::move(x), std::move(y), eff, precision); } // R_F(x, y, z) = (1/2)∫₀^∞ dt / √[(t+x)(t+y)(t+z)] // x, y, z ≥ 0, at most one is 0 static Float carlsonRF_core(Float xw, Float yw, Float zw, int eff, int precision) { int wp = precision + 20; xw.truncateToApprox(wp); yw.truncateToApprox(wp); zw.truncateToApprox(wp); for (int iter = 0; iter < 200; iter++) { Float sqx = sqrt(xw, wp); Float sqy = sqrt(yw, wp); Float sqz = sqrt(zw, wp); Float lam = sqx * sqy + sqy * sqz + sqz * sqx; xw = ldexp(xw + lam, -2); xw.truncateToApprox(wp); yw = ldexp(yw + lam, -2); yw.truncateToApprox(wp); zw = ldexp(zw + lam, -2); zw.truncateToApprox(wp); Float A = (xw + yw + zw) / 3; Float dx = (A - xw) / A; Float dy = (A - yw) / A; Float dz = (A - zw) / A; int64_t dx_b = dx.isZero() ? INT64_MIN : dx.exponent() + static_cast(dx.mantissa().bitLength()); int64_t dy_b = dy.isZero() ? INT64_MIN : dy.exponent() + static_cast(dy.mantissa().bitLength()); int64_t dz_b = dz.isZero() ? INT64_MIN : dz.exponent() + static_cast(dz.mantissa().bitLength()); if (std::max({dx_b, dy_b, dz_b}) < -(Float::precisionToBits(wp) + 5)) break; } Float A = (xw + yw + zw) / 3; Float X = (A - xw) / A; Float Y = (A - yw) / A; Float Z = -(X + Y); Float E2 = X * Y - Z * Z; Float E3 = X * Y * Z; Float poly = Float(1) - E2 / 10 + E3 / 14 + E2 * E2 / 24 - 3 * E2 * E3 / 44; Float result = poly / sqrt(A, wp); finalizeResult(result, eff, precision); return result; } Float carlsonRF(const Float& x, const Float& y, const Float& z, int precision) { if (x.isNaN() || y.isNaN() || z.isNaN()) return Float::nan(); if (x.isNegative() || y.isNegative() || z.isNegative()) return Float::nan(); int eff = std::min({x.effectiveBits(), y.effectiveBits(), z.effectiveBits()}); return carlsonRF_core(Float(x), Float(y), Float(z), eff, precision); } Float carlsonRF(Float&& x, Float&& y, Float&& z, int precision) { if (x.isNaN() || y.isNaN() || z.isNaN()) return Float::nan(); if (x.isNegative() || y.isNegative() || z.isNegative()) return Float::nan(); int eff = std::min({x.effectiveBits(), y.effectiveBits(), z.effectiveBits()}); return carlsonRF_core(std::move(x), std::move(y), std::move(z), eff, precision); } // R_D(x, y, z) = (3/2)∫₀^∞ dt / [(t+z)^{3/2}√((t+x)(t+y))] // x, y ≥ 0 (at most one is 0), z > 0 static Float carlsonRD_core(Float xw, Float yw, Float zw, int eff, int precision) { Float::PrecisionScope _ps(precision + 64); // Compute exact÷exact inside the function at working precision int wp = precision + 20; xw.truncateToApprox(wp); yw.truncateToApprox(wp); zw.truncateToApprox(wp); Float sigma(0); Float fac(1); for (int iter = 0; iter < 200; iter++) { Float sqx = sqrt(xw, wp); Float sqy = sqrt(yw, wp); Float sqz = sqrt(zw, wp); Float lam = sqx * sqy + sqy * sqz + sqz * sqx; sigma = sigma + fac / (sqz * (zw + lam)); sigma.truncateToApprox(wp); fac = ldexp(fac, -2); xw = ldexp(xw + lam, -2); xw.truncateToApprox(wp); yw = ldexp(yw + lam, -2); yw.truncateToApprox(wp); zw = ldexp(zw + lam, -2); zw.truncateToApprox(wp); Float A = (xw + yw + 3 * zw) / 5; Float dx = (A - xw) / A; Float dy = (A - yw) / A; Float dz = (A - zw) / A; int64_t dx_b = dx.isZero() ? INT64_MIN : dx.exponent() + static_cast(dx.mantissa().bitLength()); int64_t dy_b = dy.isZero() ? INT64_MIN : dy.exponent() + static_cast(dy.mantissa().bitLength()); int64_t dz_b = dz.isZero() ? INT64_MIN : dz.exponent() + static_cast(dz.mantissa().bitLength()); if (std::max({dx_b, dy_b, dz_b}) < -(Float::precisionToBits(wp) + 5)) break; } Float A = (xw + yw + 3 * zw) / 5; Float dx = (A - xw) / A; Float dy = (A - yw) / A; Float dz = (A - zw) / A; Float ea = dx * dy; Float eb = dz * dz; Float ec = ea - eb; Float ed = ea - 6 * eb; Float ee = ed + ec + ec; // C₁=3/14, C₂=1/6, C₃=9/22, C₄=3/26, C₅=9/88, C₆=9/52 Float poly = Float(1) + ed * (Float(-3) / 14 + Float(9) / 88 * ed - Float(9) / 52 * dz * ee) + dz * (Float(1) / 6 * ee + dz * (Float(-9) / 22 * ec + dz * 3 / 26 * ea)); Float result = 3 * sigma + fac * poly / (A * sqrt(A, wp)); finalizeResult(result, eff, precision); return result; } Float carlsonRD(const Float& x, const Float& y, const Float& z, int precision) { if (x.isNaN() || y.isNaN() || z.isNaN()) return Float::nan(); if (x.isNegative() || y.isNegative() || !z.isPositive()) return Float::nan(); int eff = std::min({x.effectiveBits(), y.effectiveBits(), z.effectiveBits()}); return carlsonRD_core(Float(x), Float(y), Float(z), eff, precision); } Float carlsonRD(Float&& x, Float&& y, Float&& z, int precision) { if (x.isNaN() || y.isNaN() || z.isNaN()) return Float::nan(); if (x.isNegative() || y.isNegative() || !z.isPositive()) return Float::nan(); int eff = std::min({x.effectiveBits(), y.effectiveBits(), z.effectiveBits()}); return carlsonRD_core(std::move(x), std::move(y), std::move(z), eff, precision); } // R_J(x, y, z, p) = (3/2)∫₀^∞ dt / [(t+p)√((t+x)(t+y)(t+z))] // x, y, z ≥ 0 (at most one is 0), p > 0 static Float carlsonRJ_core(Float xw, Float yw, Float zw, Float pw, int eff, int precision) { Float::PrecisionScope _ps(precision + 64); // Compute exact÷exact inside the function at working precision int wp = precision + 20; xw.truncateToApprox(wp); yw.truncateToApprox(wp); zw.truncateToApprox(wp); pw.truncateToApprox(wp); Float sigma(0); Float fac(1); for (int iter = 0; iter < 200; iter++) { Float sqx = sqrt(xw, wp); Float sqy = sqrt(yw, wp); Float sqz = sqrt(zw, wp); Float sqp = sqrt(pw, wp); Float lam = sqx * sqy + sqy * sqz + sqz * sqx; Float alpha = pw * (sqx + sqy + sqz) + sqx * sqy * sqz; alpha = alpha * alpha; alpha.truncateToApprox(wp); Float beta = pw * (pw + lam) * (pw + lam); beta.truncateToApprox(wp); sigma = sigma + fac * carlsonRC(alpha, beta, wp); sigma.truncateToApprox(wp); fac = ldexp(fac, -2); xw = ldexp(xw + lam, -2); xw.truncateToApprox(wp); yw = ldexp(yw + lam, -2); yw.truncateToApprox(wp); zw = ldexp(zw + lam, -2); zw.truncateToApprox(wp); pw = ldexp(pw + lam, -2); pw.truncateToApprox(wp); Float A = (xw + yw + zw + pw + pw) / 5; Float dx = (A - xw) / A; Float dy = (A - yw) / A; Float dz = (A - zw) / A; Float dp = (A - pw) / A; int64_t dx_b = dx.isZero() ? INT64_MIN : dx.exponent() + static_cast(dx.mantissa().bitLength()); int64_t dy_b = dy.isZero() ? INT64_MIN : dy.exponent() + static_cast(dy.mantissa().bitLength()); int64_t dz_b = dz.isZero() ? INT64_MIN : dz.exponent() + static_cast(dz.mantissa().bitLength()); int64_t dp_b = dp.isZero() ? INT64_MIN : dp.exponent() + static_cast(dp.mantissa().bitLength()); if (std::max({dx_b, dy_b, dz_b, dp_b}) < -(Float::precisionToBits(wp) + 5)) break; } Float A = (xw + yw + zw + pw + pw) / 5; Float dx = (A - xw) / A; Float dy = (A - yw) / A; Float dz = (A - zw) / A; Float dp = (A - pw) / A; Float ea = dx * (dy + dz) + dy * dz; Float eb = dx * dy * dz; Float ec = dp * dp; Float ed = ea - 3 * ec; Float ee = eb + ldexp(dp * (ea - ec), 1); // C₁=3/14, C₂=1/3, C₃=3/22, C₄=3/26 Float poly = Float(1) + ed * (Float(-3) / 14 + Float(9) / 88 * ed - Float(9) / 52 * ee) + eb * (Float(1) / 6 + dp * (Float(-6) / 22 + dp * 3 / 26)) + dp * ea * (Float(1) / 3 - dp * 3 / 22) - Float(1) / 3 * dp * ec; Float result = 3 * sigma + fac * poly / (A * sqrt(A, wp)); finalizeResult(result, eff, precision); return result; } Float carlsonRJ(const Float& x, const Float& y, const Float& z, const Float& p, int precision) { if (x.isNaN() || y.isNaN() || z.isNaN() || p.isNaN()) return Float::nan(); if (x.isNegative() || y.isNegative() || z.isNegative() || !p.isPositive()) return Float::nan(); int eff = std::min({x.effectiveBits(), y.effectiveBits(), z.effectiveBits(), p.effectiveBits()}); return carlsonRJ_core(Float(x), Float(y), Float(z), Float(p), eff, precision); } Float carlsonRJ(Float&& x, Float&& y, Float&& z, Float&& p, int precision) { if (x.isNaN() || y.isNaN() || z.isNaN() || p.isNaN()) return Float::nan(); if (x.isNegative() || y.isNegative() || z.isNegative() || !p.isPositive()) return Float::nan(); int eff = std::min({x.effectiveBits(), y.effectiveBits(), z.effectiveBits(), p.effectiveBits()}); return carlsonRJ_core(std::move(x), std::move(y), std::move(z), std::move(p), eff, precision); } //============================================================================= // Elliptic integrals — Legendre form //============================================================================= // K(k) = R_F(0, 1-k², 1) — complete elliptic integral of the first kind Float ellipticK(const Float& k, int precision) { if (k.isNaN()) return Float::nan(); int eff_k = k.effectiveBits(); int wp = precision + 15; Float k2 = k * k; k2.truncateToApprox(wp); if (k2 == Float(1)) return Float::positiveInfinity(); // K(1) = ∞ Float result = carlsonRF(Float(0), Float(1) - k2, Float(1), wp); finalizeResult(result, eff_k, precision); return result; } Float ellipticK(Float&& k, int precision) { if (k.isNaN()) return Float::nan(); int eff_k = k.effectiveBits(); int wp = precision + 15; Float k2 = k * k; k2.truncateToApprox(wp); if (k2 == Float(1)) return Float::positiveInfinity(); Float result = carlsonRF(Float(0), Float(1) - std::move(k2), Float(1), wp); finalizeResult(result, eff_k, precision); return result; } // E(k) = R_F(0, 1-k², 1) - (k²/3)·R_D(0, 1-k², 1) — complete elliptic integral of the second kind Float ellipticE(const Float& k, int precision) { Float::PrecisionScope _ps(precision + 64); // Compute exact÷exact inside the function at working precision if (k.isNaN()) return Float::nan(); { Float k2t = k * k; if (k2t == Float(1)) return Float(1); // E(±1) = 1 } int eff_k = k.effectiveBits(); int wp = precision + 15; Float k2 = k * k; k2.truncateToApprox(wp); Float kp2 = Float(1) - k2; kp2.truncateToApprox(wp); Float rf = carlsonRF(Float(0), kp2, Float(1), wp); Float rd = carlsonRD(Float(0), kp2, Float(1), wp); Float result = rf - k2 * rd / 3; finalizeResult(result, eff_k, precision); return result; } Float ellipticE(Float&& k, int precision) { Float::PrecisionScope _ps(precision + 64); // Compute exact÷exact inside the function at working precision if (k.isNaN()) return Float::nan(); { Float k2t = k * k; if (k2t == Float(1)) return Float(1); } int eff_k = k.effectiveBits(); int wp = precision + 15; Float k2 = k * k; k2.truncateToApprox(wp); Float kp2 = Float(1) - k2; kp2.truncateToApprox(wp); Float rf = carlsonRF(Float(0), kp2, Float(1), wp); Float rd = carlsonRD(Float(0), std::move(kp2), Float(1), wp); Float result = rf - std::move(k2) * rd / 3; finalizeResult(result, eff_k, precision); return result; } // Π(n, k) = R_F(0, 1-k², 1) + (n/3)·R_J(0, 1-k², 1, 1-n) — complete elliptic integral of the third kind Float ellipticPi(const Float& n, const Float& k, int precision) { if (n.isNaN() || k.isNaN()) return Float::nan(); int eff = std::min(n.effectiveBits(), k.effectiveBits()); int wp = precision + 15; Float k2 = k * k; k2.truncateToApprox(wp); Float kp2 = Float(1) - k2; kp2.truncateToApprox(wp); Float rf = carlsonRF(Float(0), kp2, Float(1), wp); Float rj = carlsonRJ(Float(0), kp2, Float(1), Float(1) - n, wp); Float result = rf + n * rj / 3; finalizeResult(result, eff, precision); return result; } Float ellipticPi(Float&& n, Float&& k, int precision) { if (n.isNaN() || k.isNaN()) return Float::nan(); int eff = std::min(n.effectiveBits(), k.effectiveBits()); int wp = precision + 15; Float k2 = k * k; k2.truncateToApprox(wp); Float kp2 = Float(1) - k2; kp2.truncateToApprox(wp); Float rf = carlsonRF(Float(0), kp2, Float(1), wp); Float rj = carlsonRJ(Float(0), std::move(kp2), Float(1), Float(1) - n, wp); Float result = rf + std::move(n) * rj / 3; finalizeResult(result, eff, precision); return result; } // F(φ, k) = sin(φ)·R_F(cos²φ, 1-k²sin²φ, 1) — incomplete elliptic integral of the first kind Float ellipticF(const Float& phi, const Float& k, int precision) { if (phi.isNaN() || k.isNaN()) return Float::nan(); int eff = std::min(phi.effectiveBits(), k.effectiveBits()); int wp = precision + 15; Float s = sin(phi, wp); Float c = cos(phi, wp); Float k2 = k * k; k2.truncateToApprox(wp); Float result = s * carlsonRF(c * c, Float(1) - k2 * s * s, Float(1), wp); finalizeResult(result, eff, precision); return result; } Float ellipticF(Float&& phi, Float&& k, int precision) { if (phi.isNaN() || k.isNaN()) return Float::nan(); int eff = std::min(phi.effectiveBits(), k.effectiveBits()); int wp = precision + 15; Float s = sin(phi, wp); Float c = cos(std::move(phi), wp); Float k2 = k * k; k2.truncateToApprox(wp); Float result = s * carlsonRF(c * c, Float(1) - std::move(k2) * s * s, Float(1), wp); finalizeResult(result, eff, precision); return result; } // E(φ, k) = sin(φ)·R_F(...) - (k²/3)sin³φ·R_D(...) — incomplete elliptic integral of the second kind Float ellipticE(const Float& phi, const Float& k, int precision) { if (phi.isNaN() || k.isNaN()) return Float::nan(); int eff = std::min(phi.effectiveBits(), k.effectiveBits()); int wp = precision + 15; Float s = sin(phi, wp); Float c = cos(phi, wp); Float k2 = k * k; k2.truncateToApprox(wp); Float c2 = c * c; c2.truncateToApprox(wp); Float s2 = s * s; s2.truncateToApprox(wp); Float w = Float(1) - k2 * s2; w.truncateToApprox(wp); Float rf = carlsonRF(c2, w, Float(1), wp); Float rd = carlsonRD(c2, w, Float(1), wp); Float result = s * rf - k2 * s * s2 * rd / 3; finalizeResult(result, eff, precision); return result; } Float ellipticE(Float&& phi, Float&& k, int precision) { if (phi.isNaN() || k.isNaN()) return Float::nan(); int eff = std::min(phi.effectiveBits(), k.effectiveBits()); int wp = precision + 15; Float s = sin(phi, wp); Float c = cos(std::move(phi), wp); Float k2 = k * k; k2.truncateToApprox(wp); Float c2 = c * c; c2.truncateToApprox(wp); Float s2 = s * s; s2.truncateToApprox(wp); Float w = Float(1) - k2 * s2; w.truncateToApprox(wp); Float rf = carlsonRF(c2, w, Float(1), wp); Float rd = carlsonRD(std::move(c2), std::move(w), Float(1), wp); Float result = s * rf - std::move(k2) * s * s2 * rd / 3; finalizeResult(result, eff, precision); return result; } // Π(n, φ, k) — incomplete elliptic integral of the third kind Float ellipticPi(const Float& n, const Float& phi, const Float& k, int precision) { if (n.isNaN() || phi.isNaN() || k.isNaN()) return Float::nan(); int eff = std::min({n.effectiveBits(), phi.effectiveBits(), k.effectiveBits()}); int wp = precision + 15; Float s = sin(phi, wp); Float c = cos(phi, wp); Float k2 = k * k; k2.truncateToApprox(wp); Float s2 = s * s; s2.truncateToApprox(wp); Float c2 = c * c; c2.truncateToApprox(wp); Float w = Float(1) - k2 * s2; w.truncateToApprox(wp); Float rf = carlsonRF(c2, w, Float(1), wp); Float rj = carlsonRJ(c2, w, Float(1), Float(1) - n * s2, wp); Float result = s * rf + n * s * s2 * rj / 3; finalizeResult(result, eff, precision); return result; } Float ellipticPi(Float&& n, Float&& phi, Float&& k, int precision) { if (n.isNaN() || phi.isNaN() || k.isNaN()) return Float::nan(); int eff = std::min({n.effectiveBits(), phi.effectiveBits(), k.effectiveBits()}); int wp = precision + 15; Float s = sin(phi, wp); Float c = cos(std::move(phi), wp); Float k2 = k * k; k2.truncateToApprox(wp); Float s2 = s * s; s2.truncateToApprox(wp); Float c2 = c * c; c2.truncateToApprox(wp); Float w = Float(1) - k2 * s2; w.truncateToApprox(wp); Float rf = carlsonRF(c2, w, Float(1), wp); Float rj = carlsonRJ(std::move(c2), std::move(w), Float(1), Float(1) - n * s2, wp); Float result = s * rf + std::move(n) * s * s2 * rj / 3; finalizeResult(result, eff, precision); return result; } //============================================================================= // Jacobi elliptic functions — descending AGM Landen transformation //============================================================================= // Common helper: compute the amplitude am(u, k) = φ₀ via AGM + inverse transformation static Float jacobiAmplitude(const Float& u, const Float& k, int wp) { // k = 0: am(u, 0) = u if (k.isZero()) { Float r = u; r.truncateToApprox(wp); return r; } // k = 1: am(u, 1) = gd(u) = 2·arctan(tanh(u/2)) if (k == Float(1)) { Float r = ldexp(atan(tanh(ldexp(u, -1), wp), wp), 1); r.truncateToApprox(wp); return r; } Float kp = sqrt(Float(1) - k * k, wp); // k' = √(1-k²) // AGM forward iteration: a₀=1, b₀=k', c₀=k std::vector a_seq, c_seq; Float a(1), b = kp, c = k; a.truncateToApprox(wp); b.truncateToApprox(wp); c.truncateToApprox(wp); a_seq.push_back(a); c_seq.push_back(c); for (int n = 0; n < 200; n++) { Float a_new = ldexp(a + b, -1); a_new.truncateToApprox(wp); Float c_new = ldexp(a - b, -1); c_new.truncateToApprox(wp); Float b_new = sqrt(a * b, wp); a = a_new; b = b_new; c = c_new; a_seq.push_back(a); c_seq.push_back(c); if (c.isZero()) break; int64_t c_bits = c.exponent() + static_cast(c.mantissa().bitLength()); if (c_bits < -(Float::precisionToBits(wp) + 5)) break; } int N = static_cast(a_seq.size()) - 1; // φ_N = 2^N · a_N · u Float phi = u * a_seq[N]; for (int i = 0; i < N; i++) { phi = ldexp(phi, 1); } phi.truncateToApprox(wp); // Inverse transformation: φ_{n-1} = (φ_n + arcsin(c_n/a_n · sin(φ_n))) / 2 for (int n = N; n >= 1; n--) { Float sinphi = sin(phi, wp); Float arg = c_seq[n] * sinphi / a_seq[n]; arg.truncateToApprox(wp); phi = ldexp(phi + asin(arg, wp), -1); phi.truncateToApprox(wp); } return phi; } Float jacobiSn(const Float& u, const Float& k, int precision) { if (u.isNaN() || k.isNaN()) return Float::nan(); if (u.isZero()) return Float(0); int eff = std::min(u.effectiveBits(), k.effectiveBits()); int wp = precision + 20; Float phi = jacobiAmplitude(u, k, wp); Float result = sin(phi, wp); finalizeResult(result, eff, precision); return result; } Float jacobiSn(Float&& u, Float&& k, int precision) { if (u.isNaN() || k.isNaN()) return Float::nan(); if (u.isZero()) return Float(0); int eff = std::min(u.effectiveBits(), k.effectiveBits()); int wp = precision + 20; Float phi = jacobiAmplitude(u, std::move(k), wp); Float result = sin(std::move(phi), wp); finalizeResult(result, eff, precision); return result; } Float jacobiCn(const Float& u, const Float& k, int precision) { if (u.isNaN() || k.isNaN()) return Float::nan(); if (u.isZero()) return Float(1); int eff = std::min(u.effectiveBits(), k.effectiveBits()); int wp = precision + 20; Float phi = jacobiAmplitude(u, k, wp); Float result = cos(phi, wp); finalizeResult(result, eff, precision); return result; } Float jacobiCn(Float&& u, Float&& k, int precision) { if (u.isNaN() || k.isNaN()) return Float::nan(); if (u.isZero()) return Float(1); int eff = std::min(u.effectiveBits(), k.effectiveBits()); int wp = precision + 20; Float phi = jacobiAmplitude(u, std::move(k), wp); Float result = cos(std::move(phi), wp); finalizeResult(result, eff, precision); return result; } Float jacobiDn(const Float& u, const Float& k, int precision) { if (u.isNaN() || k.isNaN()) return Float::nan(); if (u.isZero()) return Float(1); int eff = std::min(u.effectiveBits(), k.effectiveBits()); int wp = precision + 20; Float phi = jacobiAmplitude(u, k, wp); Float sn = sin(phi, wp); Float k2 = k * k; k2.truncateToApprox(wp); Float result = sqrt(Float(1) - k2 * sn * sn, wp); finalizeResult(result, eff, precision); return result; } Float jacobiDn(Float&& u, Float&& k, int precision) { if (u.isNaN() || k.isNaN()) return Float::nan(); if (u.isZero()) return Float(1); int eff = std::min(u.effectiveBits(), k.effectiveBits()); int wp = precision + 20; Float phi = jacobiAmplitude(u, k, wp); Float sn = sin(std::move(phi), wp); Float k2 = k * k; k2.truncateToApprox(wp); Float result = sqrt(Float(1) - std::move(k2) * sn * sn, wp); finalizeResult(result, eff, precision); return result; } //============================================================================= // Zeta function ζ(s) — Borwein algorithm //============================================================================= // Borwein's (1995) accelerated alternating series: // ζ(s) = [1/(1-2^{1-s})] · [(-1)/(d_n)] · Σ_{k=0}^{n-1} (-1)^k (d_k - d_n) / (k+1)^s // where d_k = n · Σ_{i=0}^{k} (n+i-1)! · 4^i / ((n-i)! · (2i)!) Float zeta(const Float& s, int precision) { Float::PrecisionScope _ps(precision + 64); // Compute exact÷exact inside the function at working precision if (s.isNaN()) return Float::nan(); if (s.isInfinity()) { if (s.isNegative()) return Float::nan(); return Float(1); // ζ(+∞) = 1 } int eff_s = s.effectiveBits(); int wp = precision + 40; // s = 1 is a pole Float one(1); if (s == one) return Float::nan(); // Even negative integer: ζ(-2n) = 0 (trivial zero) if (s.isNegative() && s.isInteger()) { double sv = s.toDouble(); int si = static_cast(sv); if (si % 2 == 0 && si < 0) return Float(0); } // s < 0: reflection formula ζ(s) = 2^s · π^{s-1} · sin(πs/2) · Γ(1-s) · ζ(1-s) if (s.isNegative()) { Float one_minus_s = Float(1) - s; Float z_1ms = zeta(one_minus_s, wp); Float g_1ms = gamma(one_minus_s, wp); Float two_pow_s = pow(Float(2), s, wp); Float pi_pow_sm1 = pow(Float::pi(wp), s - Float(1), wp); Float sin_term = sinPi(ldexp(s, -1), wp); Float result = two_pow_s * pi_pow_sm1 * sin_term * g_1ms * z_1ms; finalizeResult(result, eff_s, precision); return result; } // s > 0, s ≠ 1: computation via the Dirichlet eta function with Euler acceleration // η(s) = (1-2^{1-s})·ζ(s) = Σ (-1)^{k-1}/k^s, computed via the Euler transform // Partial sums of η(s): S_K = Σ_{k=1}^{K} (-1)^{k-1}/k^s // Euler transform: E_n = Σ_{k=0}^{n} C(n,k)/2^n · S_{k+1} // Term count: the Euler transform error is ~2^{-n} (1 bit/term), so the term count is taken in bits. // BUGFIX (2026-05-30): n = wp·1.05 misused decimal digits as a bit term count, capping at ~0.3·P digits // → wp_bits·1.05 (unit-mixing pattern C). int n = static_cast(Float::precisionToBits(wp) * 1.05) + 5; // Euler transform while computing the partial sums // BUGFIX (2026-05-30): for exact inputs (s=3 etc.), Float(1)/pow(kp1,s) etc. fall to // exact/exact poison, so make s_copy non-exact. Float eta(0); Float s_copy = s; s_copy.truncateToApprox(wp); s_copy.setEffectiveBits(Float::precisionToBits(wp)); // S_k = partial sum, w_k = C(n,k)/2^n (Euler weight) Float partial_sum(0); Float weighted_sum(0); // w_0 = 1/2^n, w_{k+1} = w_k · (n-k)/(k+1) // 2^n is too large, so handle in log scale or compute directly with Float Float w = ldexp(Float(1), -n); // w_0 = 2^{-n} w.truncateToApprox(wp); w.setEffectiveBits(Float::precisionToBits(wp)); // avoid exact poison of /Float(k+1) for (int k = 0; k <= n; k++) { // Partial sum update: S_{k+1} = S_k + (-1)^k / (k+1)^s Float kp1 = Float(k + 1); Float term_k = Float(1) / pow(kp1, s_copy, wp); if (k % 2 == 0) { partial_sum = partial_sum + term_k; } else { partial_sum = partial_sum - term_k; } partial_sum.truncateToApprox(wp); // Weighted sum weighted_sum = weighted_sum + w * partial_sum; weighted_sum.truncateToApprox(wp); // Weight update: w_{k+1} = w_k · (n-k) / (k+1) if (k < n) { w = w * Float(n - k) / Float(k + 1); w.truncateToApprox(wp); } } eta = weighted_sum; // ζ(s) = η(s) / (1 - 2^{1-s}) Float two_pow_1ms = pow(Float(2), Float(1) - s_copy, wp); Float denom = Float(1) - two_pow_1ms; if (denom.isZero()) { // Case s = 1 (already checked, but just in case) return Float::nan(); } Float result = eta / denom; finalizeResult(result, eff_s, precision); return result; } Float zeta(Float&& s, int precision) { return zeta(static_cast(s), precision); } //============================================================================= // Hurwitz zeta function ζ(s, a) — Euler-Maclaurin formula //============================================================================= // ζ(s, a) = Σ_{k=0}^{N-1} (a+k)^{-s} + (a+N)^{1-s}/(s-1) + (a+N)^{-s}/2 // + Σ_{j=1}^{M} B_{2j}/(2j)! · s(s+1)···(s+2j-2) · (a+N)^{-(s+2j-1)} // // The Bernoulli numbers B_{2j} are computed recursively at Float precision: // B(2m) = -1/(2m+1) · [1 + (2m+1)·(-1/2) + Σ_{j=1}^{m-1} C(2m+1,2j)·B(2j)] Float hurwitzZeta(const Float& s, const Float& a, int precision) { if (s.isNaN() || a.isNaN()) return Float::nan(); if (a.isZero() || a.isNegative()) return Float::nan(); // a > 0 is required int eff = std::min(s.effectiveBits(), a.effectiveBits()); // s = 1 is a pole Float one(1); if (s == one) return Float::nan(); // a = 1 → delegate to Riemann ζ(s) if (a == one) return zeta(s, precision); int wp = precision + 40; const int wp_bits = Float::precisionToBits(wp); // BUGFIX (2026-05-30): for exact inputs (s=3,a=0.5), pow/division become exact/exact poison. Float sw = s; sw.truncateToApprox(wp); sw.setEffectiveBits(wp_bits); Float aw = a; aw.truncateToApprox(wp); aw.setEffectiveBits(wp_bits); // Argument shift: choose N so that a+N becomes large enough. // The minimum attainable Euler-Maclaurin error is ~e^{-2π(a+N)}. To drive this below 10^{-wp}, // a+N ≳ wp/(2π·log10 e) ≈ 0.366·wp is required. BUGFIX (2026-05-30): // the old wp/3 (=0.333·wp) was slightly too small → wp·0.4+5 for some margin. int target_aN = static_cast(wp * 0.4) + 5; int N = std::max(1, target_aN - static_cast(aw.toDouble())); // Direct sum: Σ_{k=0}^{N-1} (a+k)^{-s} Float direct_sum(0); for (int k = 0; k < N; k++) { Float term = pow(aw + Float(k), -sw, wp); term.truncateToApprox(wp); direct_sum = direct_sum + term; direct_sum.truncateToApprox(wp); } Float aN = aw + Float(N); aN.truncateToApprox(wp); // Integral term: (a+N)^{1-s} / (s-1) Float integral_term = pow(aN, Float(1) - sw, wp) / (sw - Float(1)); integral_term.truncateToApprox(wp); // Midpoint correction: (1/2)·(a+N)^{-s} Float midpoint = ldexp(Float(1) / pow(aN, sw, wp), -1); midpoint.truncateToApprox(wp); Float sum = direct_sum + integral_term + midpoint; sum.truncateToApprox(wp); // Number of Bernoulli correction terms: optimal truncation M* ≈ π·(a+N). Since convergence/divergence // detection stops early, provide the optimal point as an upper bound. BUGFIX (2026-05-30): the old wp/5+5 // truncated far short of the optimal point (~π·target_aN), capping at ~0.3·P digits // (unit-mixing family: the term count was too small relative to the bit budget wp_bits). constexpr double PI_VAL = 3.141592653589793238462643383279502884; int M = static_cast(PI_VAL * static_cast(target_aN)) + 10; // Bernoulli numbers B_{2k} (k=1..M). Computed with the same validated routine as polygamma/trigamma. // BUGFIX (2026-05-30): the former inline recurrence broke beyond B_10 due to Float/int division // of the binomial coefficients (divScalarF only integer-divides the mantissa without extending precision) // and insufficient cancellation guard, capping hurwitzZeta at ~26 digits. Resolved by delegating to // computeBernoulliNumbers (result[k]=B_{2k}). bern[k] = B_{2k} (k=0..M). auto bern = computeBernoulliNumbers(M, wp); // Euler-Maclaurin Bernoulli correction Float rising = sw; // Rising factorial: s, s(s+1)(s+2), ... Float aN_inv = Float(1) / aN; aN_inv.truncateToApprox(wp); Float aN_pow = pow(aN, -sw, wp) * aN_inv; // (a+N)^{-(s+1)} aN_pow.truncateToApprox(wp); int64_t prev_ct = (std::numeric_limits::max)(); for (int j = 0; j < M; j++) { // Compute (2(j+1))! Float fact(1); for (int i = 1; i <= 2 * (j + 1); i++) { fact = fact * i; } fact.truncateToApprox(wp); Float correction = bern[j + 1] / fact * rising * aN_pow; // B_{2(j+1)} correction.truncateToApprox(wp); int64_t ct = correction.isZero() ? (std::numeric_limits::min)() : correction.exponent() + static_cast(correction.mantissa().bitLength()); // Divergence detection: stop just before passing the smallest term of the asymptotic series (correction starts increasing). if (j >= 3 && ct > prev_ct) break; sum = sum + correction; sum.truncateToApprox(wp); prev_ct = ct; // Convergence check if (j >= 3 && !correction.isZero()) { auto st = sum.exponent() + static_cast(sum.mantissa().bitLength()); if (st - ct > Float::precisionToBits(wp) + 5) break; } // Rising factorial: multiply by (s+2j-1)(s+2j) rising = rising * (sw + Float(2 * j + 1)) * (sw + Float(2 * (j + 1))); rising.truncateToApprox(wp); // (a+N) power: multiply by (a+N)^{-2} aN_pow = aN_pow * aN_inv * aN_inv; aN_pow.truncateToApprox(wp); } finalizeResult(sum, eff, precision); return sum; } Float hurwitzZeta(Float&& s, Float&& a, int precision) { return hurwitzZeta(static_cast(s), static_cast(a), precision); } //============================================================================= // Dirichlet η function: η(s) = (1 - 2^{1-s}) · ζ(s) //============================================================================= Float dirichletEta(const Float& s, int precision) { if (s.isNaN()) return Float::nan(); // η(1) = ln(2) (avoids the pole of ζ(1)) Float one(1); if (s == one) { return log(Float(2), precision); } int eff_s = s.effectiveBits(); int wp = precision + 20; Float sw = s; sw.truncateToApprox(wp); Float factor = Float(1) - pow(Float(2), Float(1) - sw, wp); factor.truncateToApprox(wp); Float z = zeta(sw, wp); Float result = factor * z; finalizeResult(result, eff_s, precision); return result; } Float dirichletEta(Float&& s, int precision) { if (s.isNaN()) return Float::nan(); Float one(1); if (s == one) { return log(Float(2), precision); } int eff_s = s.effectiveBits(); int wp = precision + 20; s.truncateToApprox(wp); Float factor = Float(1) - pow(Float(2), Float(1) - s, wp); factor.truncateToApprox(wp); Float z = zeta(std::move(s), wp); Float result = std::move(factor) * z; finalizeResult(result, eff_s, precision); return result; } //============================================================================= // Exponential integral Ei(x) and logarithmic integral li(x) //============================================================================= // Ei(x) = -PV∫_{-x}^{∞} e^{-t}/t dt = γ + ln|x| + Σ_{n=1}^{∞} x^n/(n·n!) // Convergent series for x > 0 // Body: takes x by value (assumes already moved) static Float expint_core(Float x, int eff_x, int precision) { Float::PrecisionScope _ps(precision + 64); // Compute exact÷exact inside the function at working precision int wp = precision + 20; // If x stays exact (effective_bits = INT_MAX), the later term*x/n becomes an operation between // two INT_MAX values, triggering a defaultPrecision (~53 digit) fallback. // Pin effective_bits explicitly to wp via setResultPrecision. x.setResultPrecision(wp); // Ei(x) = γ + ln|x| + Σ_{n=1}^{∞} x^n / (n · n!) Float euler_gamma = Float::euler(wp); Float ln_abs_x = log(abs(x), wp); Float sum(0); Float term = x; // n=1: x^1 / (1·1!) = x sum = term; // Convergence threshold: wp is in decimal digits, so convert to bits for comparison. // The old code compared wp+5 directly against the bit difference, terminating early at ~46 digits. const int64_t convergence_bits = static_cast(Float::precisionToBits(wp)) + 5; for (int n = 2; n < 10 * wp; n++) { term = term * x / n; term.truncateToApprox(wp); Float contribution = term / n; sum = sum + contribution; sum.truncateToApprox(wp); // Convergence check if (n >= 5 && contribution.isZero()) break; if (n >= 5) { auto c_bits = contribution.exponent() + static_cast(contribution.mantissa().bitLength()); auto s_bits = sum.exponent() + static_cast(sum.mantissa().bitLength()); if (s_bits - c_bits > convergence_bits) break; } } Float result = euler_gamma + ln_abs_x + sum; finalizeResult(result, eff_x, precision); return result; } Float expint(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) { if (x.isNegative()) return Float(0); return Float::positiveInfinity(); } if (x.isZero()) return Float::negativeInfinity(); return expint_core(Float(x), x.effectiveBits(), precision); } Float expint(Float&& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) { if (x.isNegative()) return Float(0); return Float::positiveInfinity(); } if (x.isZero()) return Float::negativeInfinity(); int eff = x.effectiveBits(); return expint_core(std::move(x), eff, precision); } // li(x) = Ei(ln(x)) — logarithmic integral Float li(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) return Float::positiveInfinity(); if (x.isZero() || x.isNegative()) return Float::nan(); // li(1) = -∞ (Ei(0) = -∞) if (x == Float(1)) return Float::negativeInfinity(); int eff_x = x.effectiveBits(); int wp = precision + 10; Float ln_x = log(x, wp); Float result = expint(ln_x, wp); finalizeResult(result, eff_x, precision); return result; } Float li(Float&& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) return Float::positiveInfinity(); if (x.isZero() || x.isNegative()) return Float::nan(); if (x == Float(1)) return Float::negativeInfinity(); int eff_x = x.effectiveBits(); int wp = precision + 10; Float ln_x = log(std::move(x), wp); Float result = expint(std::move(ln_x), wp); finalizeResult(result, eff_x, precision); return result; } //============================================================================= // Dilogarithm Li₂(x) = -∫₀ˣ ln(1-t)/t dt = Σ_{n=1}^{∞} x^n/n² //============================================================================= // Body: takes x by value (assumes already moved) static Float dilog_core(Float x, int eff_x, int precision) { int wp = precision + 20; // If x stays exact (effective_bits = INT_MAX), later operations trigger a // defaultPrecision fallback, so pin effective to wp x.setResultPrecision(wp); // Special values if (x.isZero()) return Float(0); // Li₂(1) = π²/6 if (x == Float(1)) { Float pi = Float::pi(wp); Float result = pi * pi / 6; finalizeResult(result, eff_x, precision); return result; } // Li₂(-1) = -π²/12 if (x == Float(-1)) { Float pi = Float::pi(wp); Float result = -(pi * pi) / 12; finalizeResult(result, eff_x, precision); return result; } double x_approx = std::abs(x.toDouble()); // x < -1: inversion formula Li₂(x) = -Li₂(1/x) - π²/6 - ½·ln²(-x). // 1/x ∈ (-1,0) can be computed via the existing convergent route, and log is real since -x > 1 > 0. // (x > 1 makes Li₂ complex-valued, outside the scope of the real dilog, so it is not handled here. // Previously |x|>1 passed through and the Taylor series diverged, returning a huge = wrong value.) if (x_approx > 1.0 && x.isNegative()) { Float inv_x = Float(1) / x; // ∈ (-1, 0) inv_x.setResultPrecision(wp); Float li2_inv = dilog(inv_x, wp); Float pi = Float::pi(wp); Float neg_x = -x; // > 1 Float ln_negx = log(neg_x, wp); Float result = -li2_inv - pi * pi / 6 - ldexp(ln_negx * ln_negx, -1); finalizeResult(result, eff_x, precision); return result; } // |x| > 0.5: transformation Li₂(x) = -Li₂(1-x) + π²/6 - ln(x)·ln(1-x) if (x_approx > 0.5 && x_approx <= 1.0) { Float one_minus_x = Float(1) - x; one_minus_x.truncateToApprox(wp); Float li2_1mx = dilog(one_minus_x, wp); Float pi = Float::pi(wp); Float ln_x = log(x, wp); Float ln_1mx = log(one_minus_x, wp); Float result = -li2_1mx + pi * pi / 6 - ln_x * ln_1mx; finalizeResult(result, eff_x, precision); return result; } // |x| <= 0.5: direct Taylor series Li₂(x) = Σ_{n=1}^{∞} x^n / n² Float sum(0); Float x_power = x; // x^1 const int64_t convergence_bits = static_cast(Float::precisionToBits(wp)) + 5; for (int n = 1; n < 10 * wp; n++) { Float term = x_power / (static_cast(n) * n); sum = sum + term; sum.truncateToApprox(wp); // Convergence check (wp is in decimal digits, converted to bits for comparison) if (n >= 5 && term.isZero()) break; if (n >= 5) { auto t_bits = term.exponent() + static_cast(term.mantissa().bitLength()); auto s_bits = sum.exponent() + static_cast(sum.mantissa().bitLength()); if (s_bits - t_bits > convergence_bits) break; } x_power = x_power * x; x_power.truncateToApprox(wp); } finalizeResult(sum, eff_x, precision); return sum; } Float dilog(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); return dilog_core(Float(x), x.effectiveBits(), precision); } Float dilog(Float&& x, int precision) { if (x.isNaN()) return Float::nan(); int eff = x.effectiveBits(); return dilog_core(std::move(x), eff, precision); } //============================================================================= // Bessel functions J_n(x), Y_n(x) //============================================================================= // J_n(x) = Σ_{m=0}^{∞} (-1)^m / (m! · (m+n)!) · (x/2)^{2m+n} // Y_n(x) = [J_n(x)·cos(nπ) - J_{-n}(x)] / sin(nπ) (a limit when n is an integer) Float besselJ(int n, const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) return Float(0); // J_n(±∞) → 0 (oscillatory decay) int eff_x = x.effectiveBits(); int wp = precision + 20; const int wp_bits = Float::precisionToBits(wp); Float xw = x; xw.truncateToApprox(wp); xw.setEffectiveBits(wp_bits); // avoid exact/exact poison of exact inputs // J_n(0) = δ_{n,0} if (x.isZero()) { return (n == 0) ? Float(1) : Float(0); } // Negative order: J_{-n}(x) = (-1)^n · J_n(x) bool negate = false; int abs_n = n; if (n < 0) { abs_n = -n; if (abs_n % 2 != 0) negate = true; } // Taylor series: J_n(x) = (x/2)^n · Σ_{m=0}^{∞} (-1)^m · (x/2)^{2m} / (m! · (m+n)!) Float x_half = ldexp(xw, -1); x_half.truncateToApprox(wp); Float x_half_sq = x_half * x_half; x_half_sq.truncateToApprox(wp); Float neg_x_half_sq = -x_half_sq; // Compute (x/2)^n Float x_half_n(1); for (int i = 0; i < abs_n; i++) { FloatOps::mul(x_half_n, x_half, x_half_n); } x_half_n.truncateToApprox(wp); // Series: Σ (-1)^m · (x/2)^{2m} / (m! · (m+n)!) // term_0 = 1/n! Float term(1); term.setEffectiveBits(wp_bits); // avoid exact/exact poison of 1/n! for (int i = 1; i <= abs_n; i++) { FloatOps::div(term, Float(i), term); } term.truncateToApprox(wp); Float sum = term; for (int m = 1; m < 10 * wp; m++) { // term_m = term_{m-1} · (-(x/2)²) / (m · (m+n)) FloatOps::mul(term, neg_x_half_sq, term); FloatOps::div(term, Float(static_cast(m) * (m + abs_n)), term); term.truncateToApprox(wp); FloatOps::add(sum, term, sum); sum.truncateToApprox(wp); if (m >= 3 && term.isZero()) break; if (m >= 3) { auto t_bits = term.exponent() + static_cast(term.mantissa().bitLength()); auto s_bits = sum.exponent() + static_cast(sum.mantissa().bitLength()); if (s_bits - t_bits > Float::precisionToBits(wp) + 5) break; } } Float result = x_half_n * sum; if (negate) result = -result; finalizeResult(result, eff_x, precision); return result; } Float besselJ(int n, Float&& x, int precision) { return besselJ(n, static_cast(x), precision); } Float besselY(int n, const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) return Float(0); // Y_n(±∞) → 0 if (x.isZero()) return Float::negativeInfinity(); // Y_n(0) = -∞ if (x.isNegative()) return Float::nan(); // Y_n(x<0) is not real int eff_x = x.effectiveBits(); int wp = precision + 30; // BUGFIX (2026-05-30): for exact inputs (x=1), exact/exact divisions like (x/2) powers, 1/n!, // and harmonic numbers 1/j fall to default_precision. Make the working values and unit constants non-exact. const int wp_bits = Float::precisionToBits(wp); Float xw = x; xw.truncateToApprox(wp); xw.setEffectiveBits(wp_bits); Float one_w(1); one_w.setEffectiveBits(wp_bits); // Non-exact dividend for 1/j, 1/n!, etc. int abs_n = (n >= 0) ? n : -n; // Exact formula (Abramowitz & Stegun 9.1.11): // Y_n(x) = (2/π)·J_n(x)·(γ + ln(x/2)) // - (1/π)·Σ_{k=0}^{n-1} (n-k-1)!/k! · (x/2)^{2k-n} // - (1/π)·Σ_{k=0}^{∞} (-1)^k·(ψ(k+1)+ψ(k+n+1))/(k!·(k+n)!) · (x/2)^{2k+n} // ψ(m+1) = -γ + H_m (H_m = Σ 1/j) // → combining: // Y_n(x) = (2/π)·J_n(x)·ln(x/2) // - (1/π)·(x/2)^{-n}·Σ_{k=0}^{n-1} (n-k-1)!/k! · (x²/4)^k // - (1/π)·(x/2)^n·Σ_{k=0}^{∞} (-1)^k·(H_k+H_{k+n})/(k!·(k+n)!)·(x²/4)^k Float pi_val = Float::pi(wp); Float euler_gamma = Float::euler(wp); Float two_over_pi = Float(2) / pi_val; Float one_over_pi = Float(1) / pi_val; Float x_half = ldexp(xw, -1); x_half.truncateToApprox(wp); Float ln_x_half = log(x_half, wp); Float x_half_sq = x_half * x_half; x_half_sq.truncateToApprox(wp); Float jn = besselJ(abs_n, xw, wp); // Part A: (2/π) · J_n(x) · (γ + ln(x/2)) [DLMF 10.8.1] Float result = two_over_pi * jn * (euler_gamma + ln_x_half); result.truncateToApprox(wp); // Part B: -(1/π)·(x/2)^{-n}·Σ_{k=0}^{n-1} (n-k-1)!/k! · (x²/4)^k if (abs_n > 0) { Float x_half_neg_n(1); for (int i = 0; i < abs_n; i++) { FloatOps::div(x_half_neg_n, x_half, x_half_neg_n); } x_half_neg_n.truncateToApprox(wp); // k=0 term: (n-1)!/0! = (n-1)! Float bk_coeff(1); bk_coeff.setEffectiveBits(wp_bits); // avoid exact poison of the recurrence /(k(n-k)) for (int i = 1; i < abs_n; i++) { FloatOps::mul(bk_coeff, Float(i), bk_coeff); } bk_coeff.truncateToApprox(wp); Float partB = bk_coeff; for (int k = 1; k < abs_n; k++) { // Recurrence: coeff_k = coeff_{k-1} · (x²/4) / (k · (n-k)) FloatOps::mul(bk_coeff, x_half_sq, bk_coeff); FloatOps::div(bk_coeff, Float(static_cast(k) * (abs_n - k)), bk_coeff); bk_coeff.truncateToApprox(wp); FloatOps::add(partB, bk_coeff, partB); partB.truncateToApprox(wp); } result = result - one_over_pi * x_half_neg_n * partB; result.truncateToApprox(wp); } // Part C: -(1/π)·(x/2)^n·Σ_{k=0}^{∞} (-1)^k·(H_k+H_{k+n})/(k!·(k+n)!)·(x²/4)^k Float x_half_n(1); for (int i = 0; i < abs_n; i++) { FloatOps::mul(x_half_n, x_half, x_half_n); } x_half_n.truncateToApprox(wp); Float H_k(0); // H_0 = 0 Float H_kn(0); // H_n = Σ_{j=1}^{n} 1/j Float h_tmp(0); for (int j = 1; j <= abs_n; j++) { FloatOps::div(one_w, Float(j), h_tmp); FloatOps::add(H_kn, h_tmp, H_kn); } H_kn.truncateToApprox(wp); // k=0: coeff = 1/(0!·n!) = 1/n!, harmonic sum = H_0 + H_n = H_n Float ck_coeff(1); // 1/(k!·(k+n)!) — managed by recurrence, including the sign (-1)^k ck_coeff.setEffectiveBits(wp_bits); // avoid exact/exact poison of 1/n! for (int i = 1; i <= abs_n; i++) { FloatOps::div(ck_coeff, Float(i), ck_coeff); } ck_coeff.truncateToApprox(wp); Float neg_x_half_sq = -x_half_sq; Float partC = ck_coeff * (H_k + H_kn); partC.truncateToApprox(wp); Float term(0); for (int k = 1; k < 10 * wp; k++) { FloatOps::div(one_w, Float(k), h_tmp); FloatOps::add(H_k, h_tmp, H_k); FloatOps::div(one_w, Float(k + abs_n), h_tmp); FloatOps::add(H_kn, h_tmp, H_kn); // Recurrence: ck_coeff_{k} = ck_coeff_{k-1} · (-(x²/4)) / (k · (k+n)) FloatOps::mul(ck_coeff, neg_x_half_sq, ck_coeff); FloatOps::div(ck_coeff, Float(static_cast(k) * (k + abs_n)), ck_coeff); ck_coeff.truncateToApprox(wp); FloatOps::mul(ck_coeff, H_k + H_kn, term); term.truncateToApprox(wp); FloatOps::add(partC, term, partC); partC.truncateToApprox(wp); if (k >= 5 && term.isZero()) break; if (k >= 5) { auto t_bits = term.exponent() + static_cast(term.mantissa().bitLength()); auto s_bits = partC.exponent() + static_cast(partC.mantissa().bitLength()); if (s_bits - t_bits > Float::precisionToBits(wp) + 5) break; } } result = result - one_over_pi * x_half_n * partC; // Negative order: Y_{-n}(x) = (-1)^n · Y_n(x) if (n < 0 && abs_n % 2 != 0) result = -result; finalizeResult(result, eff_x, precision); return result; } Float besselY(int n, Float&& x, int precision) { return besselY(n, static_cast(x), precision); } //============================================================================= // Modified Bessel function of the first kind I_n(x) //============================================================================= // I_n(x) = Σ_{m=0}^{∞} (x/2)^{2m+n} / (m! · (m+n)!) // Same series as J_n but without the alternating sign (-1)^m (all terms positive) Float besselI(int n, const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) { if (x.isNegative()) { return (std::abs(n) % 2 == 0) ? Float::positiveInfinity() : Float::negativeInfinity(); } return Float::positiveInfinity(); // I_n(+∞) = +∞ } int eff_x = x.effectiveBits(); int wp = precision + 20; const int wp_bits = Float::precisionToBits(wp); Float xw = x; xw.truncateToApprox(wp); xw.setEffectiveBits(wp_bits); // avoid exact/exact poison of exact inputs // I_n(0) = δ_{n,0} if (x.isZero()) { return (n == 0) ? Float(1) : Float(0); } // Negative order: I_{-n}(x) = I_n(x) (integer order) int abs_n = (n >= 0) ? n : -n; // I_n(-x) = (-1)^n · I_n(x) bool negate = false; if (xw.isNegative()) { xw = -xw; if (abs_n % 2 != 0) negate = true; } // Taylor series: I_n(x) = (x/2)^n · Σ_{m=0}^{∞} (x/2)^{2m} / (m! · (m+n)!) Float x_half = ldexp(xw, -1); x_half.truncateToApprox(wp); Float x_half_sq = x_half * x_half; x_half_sq.truncateToApprox(wp); // (x/2)^n Float x_half_n(1); for (int i = 0; i < abs_n; i++) { FloatOps::mul(x_half_n, x_half, x_half_n); } x_half_n.truncateToApprox(wp); // term_0 = 1/n! Float term(1); term.setEffectiveBits(wp_bits); // avoid exact/exact poison of 1/n! for (int i = 1; i <= abs_n; i++) { FloatOps::div(term, Float(i), term); } term.truncateToApprox(wp); Float sum = term; for (int m = 1; m < 10 * wp; m++) { // term_m = term_{m-1} · (x/2)² / (m · (m+n)) — no sign flip FloatOps::mul(term, x_half_sq, term); FloatOps::div(term, Float(static_cast(m) * (m + abs_n)), term); term.truncateToApprox(wp); FloatOps::add(sum, term, sum); sum.truncateToApprox(wp); if (m >= 3 && term.isZero()) break; if (m >= 3) { auto t_bits = term.exponent() + static_cast(term.mantissa().bitLength()); auto s_bits = sum.exponent() + static_cast(sum.mantissa().bitLength()); if (s_bits - t_bits > Float::precisionToBits(wp) + 5) break; } } Float result = x_half_n * sum; if (negate) result = -result; finalizeResult(result, eff_x, precision); return result; } Float besselI(int n, Float&& x, int precision) { return besselI(n, static_cast(x), precision); } //============================================================================= // Modified Bessel function of the second kind K_n(x) //============================================================================= // A&S 9.6.11 / DLMF 10.31.1 (integer order): // K_n(x) = (1/2)(x/2)^{-n} · Σ_{k=0}^{n-1} (-1)^k · (n-k-1)!/k! · (x²/4)^k // + (-1)^{n+1} · ln(x/2) · I_n(x) // + (-1)^n · (1/2)(x/2)^n · Σ_{k=0}^{∞} [ψ(k+1)+ψ(k+n+1)] / (k!·(k+n)!) · (x²/4)^k // ψ(m+1) = -γ + H_m Float besselK(int n, const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) { if (x.isNegative()) return Float::nan(); return Float(0); // K_n(+∞) = 0 (exponential decay) } if (x.isZero()) return Float::positiveInfinity(); // K_n(0) = +∞ if (x.isNegative()) return Float::nan(); // K_n(x<0) is not real int eff_x = x.effectiveBits(); int wp = precision + 30; // BUGFIX (2026-05-30): avoid exact/exact poison of (x/2) powers, 1/n!, and harmonic numbers 1/j // for exact inputs (same form as besselY). const int wp_bits = Float::precisionToBits(wp); Float xw = x; xw.truncateToApprox(wp); xw.setEffectiveBits(wp_bits); Float one_w(1); one_w.setEffectiveBits(wp_bits); // K_{-n}(x) = K_n(x) (integer order) int abs_n = (n >= 0) ? n : -n; Float euler_gamma = Float::euler(wp); Float x_half = ldexp(xw, -1); x_half.truncateToApprox(wp); Float ln_x_half = log(x_half, wp); Float x_half_sq = x_half * x_half; x_half_sq.truncateToApprox(wp); Float in = besselI(abs_n, xw, wp); // Part A: (-1)^{n+1} · I_n(x) · ln(x/2) Float partA = in * ln_x_half; if (abs_n % 2 == 0) partA = -partA; // (-1)^{n+1} partA.truncateToApprox(wp); // Part B: (1/2)·(x/2)^{-n} · Σ_{k=0}^{n-1} (-1)^k · (n-k-1)!/k! · (x²/4)^k Float partB(0); if (abs_n > 0) { Float x_half_neg_n(1); for (int i = 0; i < abs_n; i++) { FloatOps::div(x_half_neg_n, x_half, x_half_neg_n); } x_half_neg_n.truncateToApprox(wp); // k=0: (n-1)!/0! = (n-1)! Float bk_coeff(1); bk_coeff.setEffectiveBits(wp_bits); // avoid exact poison of the recurrence /(k(n-k)) Float neg_x_half_sq_k = -x_half_sq; for (int i = 1; i < abs_n; i++) { FloatOps::mul(bk_coeff, Float(i), bk_coeff); } bk_coeff.truncateToApprox(wp); Float sumB = bk_coeff; for (int k = 1; k < abs_n; k++) { // Recurrence: coeff_k = coeff_{k-1} · (-(x²/4)) / (k · (n-k)) FloatOps::mul(bk_coeff, neg_x_half_sq_k, bk_coeff); FloatOps::div(bk_coeff, Float(static_cast(k) * (abs_n - k)), bk_coeff); bk_coeff.truncateToApprox(wp); FloatOps::add(sumB, bk_coeff, sumB); sumB.truncateToApprox(wp); } partB = ldexp(x_half_neg_n * sumB, -1); partB.truncateToApprox(wp); } // Part C: (-1)^n · (1/2)·(x/2)^n · Σ_{k=0}^{∞} [ψ(k+1)+ψ(k+n+1)] / (k!·(k+n)!) · (x²/4)^k // ψ(k+1) = -γ + H_k, ψ(k+n+1) = -γ + H_{k+n} // ψ(k+1) + ψ(k+n+1) = -2γ + H_k + H_{k+n} Float x_half_n(1); for (int i = 0; i < abs_n; i++) { FloatOps::mul(x_half_n, x_half, x_half_n); } x_half_n.truncateToApprox(wp); Float H_k(0); // H_0 = 0 Float H_kn(0); // H_n = Σ_{j=1}^{n} 1/j Float h_tmp(0); for (int j = 1; j <= abs_n; j++) { FloatOps::div(one_w, Float(j), h_tmp); FloatOps::add(H_kn, h_tmp, H_kn); } H_kn.truncateToApprox(wp); // k=0: 1/(0!·n!) = 1/n! Float ck_coeff(1); ck_coeff.setEffectiveBits(wp_bits); // avoid exact/exact poison of 1/n! for (int i = 1; i <= abs_n; i++) { FloatOps::div(ck_coeff, Float(i), ck_coeff); } ck_coeff.truncateToApprox(wp); Float two_gamma = ldexp(euler_gamma, 1); Float partC = ck_coeff * (H_k + H_kn - two_gamma); partC.truncateToApprox(wp); Float term(0); for (int k = 1; k < 10 * wp; k++) { FloatOps::div(one_w, Float(k), h_tmp); FloatOps::add(H_k, h_tmp, H_k); FloatOps::div(one_w, Float(k + abs_n), h_tmp); FloatOps::add(H_kn, h_tmp, H_kn); // Recurrence: ck_coeff_{k} = ck_coeff_{k-1} · (x²/4) / (k · (k+n)) FloatOps::mul(ck_coeff, x_half_sq, ck_coeff); FloatOps::div(ck_coeff, Float(static_cast(k) * (k + abs_n)), ck_coeff); ck_coeff.truncateToApprox(wp); FloatOps::mul(ck_coeff, H_k + H_kn - two_gamma, term); term.truncateToApprox(wp); FloatOps::add(partC, term, partC); partC.truncateToApprox(wp); if (k >= 5 && term.isZero()) break; if (k >= 5) { auto t_bits = term.exponent() + static_cast(term.mantissa().bitLength()); auto s_bits = partC.exponent() + static_cast(partC.mantissa().bitLength()); if (s_bits - t_bits > Float::precisionToBits(wp) + 5) break; } } Float sign_n = (abs_n % 2 == 0) ? Float(1) : Float(-1); partC = sign_n * ldexp(x_half_n * partC, -1); partC.truncateToApprox(wp); Float result = partA + partB + partC; finalizeResult(result, eff_x, precision); return result; } Float besselK(int n, Float&& x, int precision) { return besselK(n, static_cast(x), precision); } //============================================================================= // Spherical Bessel function of the first kind j_n(x) //============================================================================= // j_0(x) = sin(x)/x // j_1(x) = sin(x)/x² - cos(x)/x // j_{k+1}(x) = (2k+1)/x · j_k(x) - j_{k-1}(x) (forward recurrence) Float sphericalBesselJ(int n, const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (n < 0) return Float::nan(); int eff_x = x.effectiveBits(); int wp = precision + 20 + 2 * n; Float xw = x; xw.truncateToApprox(wp); // j_n(0): j_0(0) = 1, j_n(0) = 0 (n > 0) if (x.isZero()) { return (n == 0) ? Float(1) : Float(0); } // x → ±∞: j_n(x) → 0 (oscillatory decay) if (x.isInfinity()) return Float(0); Float sin_x, cos_x; sinCos(xw, sin_x, cos_x, wp); // j_0(x) = sin(x)/x Float j0 = sin_x / xw; j0.truncateToApprox(wp); if (n == 0) { finalizeResult(j0, eff_x, precision); return j0; } // j_1(x) = sin(x)/x² - cos(x)/x Float j1 = sin_x / (xw * xw) - cos_x / xw; j1.truncateToApprox(wp); if (n == 1) { finalizeResult(j1, eff_x, precision); return j1; } // Forward recurrence: j_{k+1} = (2k+1)/x · j_k - j_{k-1} Float j_prev = j0; Float j_curr = j1; for (int k = 1; k < n; k++) { Float j_next = Float(2 * k + 1) / xw * j_curr - j_prev; j_next.truncateToApprox(wp); j_prev = j_curr; j_curr = j_next; } finalizeResult(j_curr, eff_x, precision); return j_curr; } Float sphericalBesselJ(int n, Float&& x, int precision) { return sphericalBesselJ(n, static_cast(x), precision); } //============================================================================= // Spherical Bessel function of the second kind y_n(x) //============================================================================= // y_0(x) = -cos(x)/x // y_1(x) = -cos(x)/x² - sin(x)/x // y_{k+1}(x) = (2k+1)/x · y_k(x) - y_{k-1}(x) (forward recurrence) Float sphericalBesselY(int n, const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (n < 0) return Float::nan(); if (x.isZero()) return Float::negativeInfinity(); // y_n(0) = -∞ if (x.isNegative()) return Float::nan(); if (x.isInfinity()) return Float(0); // y_n(+∞) → 0 int eff_x = x.effectiveBits(); int wp = precision + 20 + 2 * n; Float xw = x; xw.truncateToApprox(wp); Float sin_x, cos_x; sinCos(xw, sin_x, cos_x, wp); // y_0(x) = -cos(x)/x Float y0 = -cos_x / xw; y0.truncateToApprox(wp); if (n == 0) { finalizeResult(y0, eff_x, precision); return y0; } // y_1(x) = -cos(x)/x² - sin(x)/x Float y1 = -cos_x / (xw * xw) - sin_x / xw; y1.truncateToApprox(wp); if (n == 1) { finalizeResult(y1, eff_x, precision); return y1; } // Forward recurrence: y_{k+1} = (2k+1)/x · y_k - y_{k-1} Float y_prev = y0; Float y_curr = y1; for (int k = 1; k < n; k++) { Float y_next = Float(2 * k + 1) / xw * y_curr - y_prev; y_next.truncateToApprox(wp); y_prev = y_curr; y_curr = y_next; } finalizeResult(y_curr, eff_x, precision); return y_curr; } Float sphericalBesselY(int n, Float&& x, int precision) { return sphericalBesselY(n, static_cast(x), precision); } //============================================================================= // Airy functions Ai(x), Bi(x) //============================================================================= // Ai(x) = c1·f(x) - c2·g(x) // Bi(x) = √3·[c1·f(x) + c2·g(x)] // f(x) = Σ_{k=0}^{∞} 3^k · x^{3k} / (3k)! · Γ(k+1/3)/(Γ(1/3)) ← via recurrence // In practice, computed directly with a Taylor series: // Ai(x) = a·Σ_{k=0}^{∞} c_k·x^k where c_0 = 1, c_1 = x, c_{k+3} = c_k / ((k+2)(k+3))·x³ // Maclaurin series: // Ai(x) = Ai(0)·f(x) + Ai'(0)·g(x) // f(x) = 1 + x³/6 + x⁶/180 + ... = Σ of terms reciprocal-like to (3k)! // g(x) = x + x⁴/12 + x⁷/504 + ... // Ai(0) = 1/(3^{2/3}·Γ(2/3)), Ai'(0) = -1/(3^{1/3}·Γ(1/3)) //------------------------------------------------------------------------- // Asymptotic expansion (DLMF/AS 10.4.59 / 10.4.63) //------------------------------------------------------------------------- // Common coefficients: c_0 = 1, c_s/c_{s-1} = (6s-1)(6s-5) / (72s) // x → +∞: Ai(x) ~ exp(-ζ) / (2√π · x^{1/4}) · Σ (-1)^s c_s / ζ^s // x → -∞: Ai(-y) ~ (1/(√π y^{1/4})) [sin(θ) S_even - cos(θ) S_odd] // ζ = (2/3) |x|^{3/2}, θ = ζ + π/4 // S_even = Σ (-1)^k c_{2k} / ζ^{2k}, S_odd = Σ (-1)^k c_{2k+1} / ζ^{2k+1} // // Activation condition (codex consult 2026-05-06): // x > 0: ζ > 0.40 p_bits + 16 (= |x|^{3/2} > 0.6 p + 24) // x < 0: ζ > 0.467 p_bits + 21 (≈ 0.50 p + 24 is safe) // Achieved precision ~ 2.885·ζ bits // Stopping condition: smallest-term truncation (stop once the term starts increasing) // Phase precision (x<0 only): wp_phase_bits = p_bits + 48 + msb(ζ) namespace { // From ζ, return the estimated truncation s before |term_s| starts increasing // (smallest-term position ≈ 2ζ, with safety margin) inline int airyAsymptoticMaxIter(double zeta_d) { if (zeta_d <= 0.0) return 100; long long n = static_cast(2.0 * zeta_d) + 32; if (n > 100000) n = 100000; return static_cast(n); } // Asymptotic expansion for x > 0, large ζ = (2/3) x^{3/2} // Returns Ai(x) keeping a working precision of precision + 30 digits Float airyAi_asymptotic_pos(const Float& xw, int precision) { int wp = precision + 30; Float sqrt_x = sqrt(xw, wp); Float x14 = sqrt(sqrt_x, wp); // x^{1/4} // ζ = (2 x sqrt(x)) / 3 Float zeta = Float(2) * xw * sqrt_x / Float(3); zeta.truncateToApprox(wp); Float zeta_inv = Float(1) / zeta; zeta_inv.truncateToApprox(wp); // Series: Σ (-1)^s c_s / ζ^s, smallest-term truncation Float sum(1); // c_0 = 1 Float term(1); Float prev_abs(1); int max_iter = airyAsymptoticMaxIter(zeta.toDouble()); for (int s = 1; s < max_iter; ++s) { int64_t num = static_cast(6 * s - 1) * static_cast(6 * s - 5); int64_t den = static_cast(72) * s; FloatOps::mul(term, zeta_inv, term); FloatOps::mul(term, Float(num), term); FloatOps::div(term, Float(den), term); term.truncateToApprox(wp); term = -term; // (-1)^s factor (per-iteration sign flip) Float abs_term = term; if (abs_term.isNegative()) abs_term = -abs_term; if (s >= 2 && abs_term > prev_abs) break; // smallest-term truncation prev_abs = abs_term; FloatOps::add(sum, term, sum); sum.truncateToApprox(wp); } // prefactor = exp(-ζ) / (2 √π · x^{1/4}) Float pi = Float::pi(wp); Float sqrt_pi = sqrt(pi, wp); Float exp_neg_zeta = exp(-zeta, wp); Float prefactor = exp_neg_zeta / (Float(2) * sqrt_pi * x14); prefactor.truncateToApprox(wp); return prefactor * sum; } // For x < 0 (neg_x is the negative input), large y = -neg_x > 0 Float airyAi_asymptotic_neg(const Float& neg_x, int precision) { int p_bits = Float::precisionToBits(precision); int wp = precision + 30; Float y = -neg_x; y.truncateToApprox(wp); Float sqrt_y = sqrt(y, wp); Float y14 = sqrt(sqrt_y, wp); Float zeta = Float(2) * y * sqrt_y / Float(3); zeta.truncateToApprox(wp); // Phase precision: needs extra by the MSB of ζ (so the phase is not lost inside sinCos) int64_t zeta_msb = zeta.exponent() + static_cast(zeta.mantissa().bitLength()); if (zeta_msb < 0) zeta_msb = 0; int wp_phase_bits = p_bits + 48 + static_cast(zeta_msb); int wp_phase = Float::bitsToPrecision(wp_phase_bits) + 4; if (wp_phase < wp) wp_phase = wp; // Recompute θ = ζ + π/4 at high precision Float y_phase = -neg_x; y_phase.truncateToApprox(wp_phase); Float sqrt_y_phase = sqrt(y_phase, wp_phase); Float zeta_hi = Float(2) * y_phase * sqrt_y_phase / Float(3); zeta_hi.truncateToApprox(wp_phase); Float pi_over_4 = Float::pi(wp_phase) / Float(4); pi_over_4.truncateToApprox(wp_phase); Float theta = zeta_hi + pi_over_4; theta.truncateToApprox(wp_phase); Float sin_theta, cos_theta; sinCos(theta, sin_theta, cos_theta, wp_phase); sin_theta.truncateToApprox(wp); cos_theta.truncateToApprox(wp); Float zeta_inv = Float(1) / zeta; zeta_inv.truncateToApprox(wp); // Series: term_s = c_s / ζ^s (unsigned), distributed to subseries with (-1)^k addition Float term(1); Float s_even(1); Float s_odd(0); Float prev_abs(1); int max_iter = airyAsymptoticMaxIter(zeta.toDouble()); for (int s = 1; s < max_iter; ++s) { int64_t num = static_cast(6 * s - 1) * static_cast(6 * s - 5); int64_t den = static_cast(72) * s; FloatOps::mul(term, zeta_inv, term); FloatOps::mul(term, Float(num), term); FloatOps::div(term, Float(den), term); term.truncateToApprox(wp); Float abs_term = term; if (abs_term.isNegative()) abs_term = -abs_term; if (s >= 2 && abs_term > prev_abs) break; prev_abs = abs_term; // s even → add (-1)^(s/2)·term to S_even // s odd → add (-1)^((s-1)/2)·term to S_odd if ((s & 1) == 0) { int k = s >> 1; Float t = (k & 1) ? -term : term; FloatOps::add(s_even, t, s_even); s_even.truncateToApprox(wp); } else { int k = (s - 1) >> 1; Float t = (k & 1) ? -term : term; FloatOps::add(s_odd, t, s_odd); s_odd.truncateToApprox(wp); } } Float pi = Float::pi(wp); Float sqrt_pi = sqrt(pi, wp); Float prefactor = Float(1) / (sqrt_pi * y14); prefactor.truncateToApprox(wp); return prefactor * (sin_theta * s_even - cos_theta * s_odd); } // Decide whether to activate the asymptotic expansion: use it if ζ > ζ_cut // ζ_cut = 0.40·p_bits + 16 (x>0) or 0.50·p_bits + 24 (x<0) // Two-stage decision: reject the certain band via the msb estimate, computing the actual value with toDouble() only in the boundary band. bool airyAi_should_use_asymptotic(const Float& xw, int precision) { if (xw.isZero()) return false; int p_bits = Float::precisionToBits(precision); bool neg = xw.isNegative(); // Approximate log2 of |x|: exponent + bitLength - 1 int64_t msb = xw.exponent() + static_cast(xw.mantissa().bitLength()) - 1; if (msb <= 0) return false; // |x| < 1 is almost certainly Maclaurin double log2_zeta_est = 1.5 * static_cast(msb) - 0.585; // log2((2/3)|x|^{1.5}) double zeta_cut = neg ? (0.50 * p_bits + 24.0) : (0.40 * p_bits + 16.0); double log2_zeta_cut = std::log2(zeta_cut + 1e-9); // Certain band: the msb estimate error is at most ~1.5 bits, so ±2.0 is safe if (log2_zeta_est > log2_zeta_cut + 2.0) return true; if (log2_zeta_est < log2_zeta_cut - 2.0) return false; // Boundary band: if |x| fits in the double range, compute ζ directly double x_d = std::abs(xw.toDouble()); if (std::isfinite(x_d) && x_d > 0.0) { double zeta_d = (2.0 / 3.0) * x_d * std::sqrt(x_d); return zeta_d > zeta_cut; } // toDouble overflows for huge values: outside the certain band with large msb, lean toward yes return true; } // Estimate the cancellation digits on the Maclaurin route and return the required working precision. // The largest term of f(x), g(x) ~ exp(2|x|^{3/2}/3) = exp(ζ) → loses 2ζ/ln(10) digits // (the factor 2 compensates the cancellation of both f(x) and g(x)) int airyMaclaurinDynWp(const Float& xw, int precision) { int base = precision + 30; int64_t msb = xw.exponent() + static_cast(xw.mantissa().bitLength()) - 1; if (msb <= 0) return base; // |x| < 1: no cancellation double x_d = std::abs(xw.toDouble()); double zeta_d; if (std::isfinite(x_d) && x_d > 0.0) { zeta_d = (2.0 / 3.0) * x_d * std::sqrt(x_d); } else { // Huge |x| should go to the asymptotic route, but estimate via msb just in case double log2_zeta = 1.5 * static_cast(msb) - 0.585; zeta_d = std::pow(2.0, log2_zeta); } // K · ζ / ln(10) digits + safety // In theory K=2 (cancellation of ai0·f and aip0·g = 2ζ/ln(10)) should suffice, but // internal precision propagation of gamma/pow + Maclaurin accumulated error require K=4 in practice. int extra = static_cast(std::ceil(4.0 * zeta_d / 2.302585092994046)) + 8; return base + extra; } // thread_local cache of Ai(0), Ai'(0) (keyed by precision) // ai0 = 1 / (3^{2/3} · Γ(2/3)) // aip0 = -1 / (3^{1/3} · Γ(1/3)) // Eliminates recomputing gamma/pow every time (codex suggestion 2026-05-06). struct AiryConstantsCache { int wp = -1; Float ai0; Float aip0; }; static const AiryConstantsCache& airyConstants(int wp) { thread_local AiryConstantsCache cache; if (cache.wp == wp) return cache; Float::PrecisionScope _ps(wp); // Compute exact÷exact of the constants one/two/three at wp digits (old: setResultPrecision each) Float three(3); Float two(2); Float one(1); Float two_thirds = two / three; Float one_third = one / three; cache.ai0 = one / (pow(three, two_thirds, wp) * gamma(two_thirds, wp)); cache.aip0 = -one / (pow(three, one_third, wp) * gamma(one_third, wp)); cache.ai0.truncateToApprox(wp); cache.aip0.truncateToApprox(wp); cache.wp = wp; return cache; } } // anonymous namespace Float airyAi(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) return Float(0); // Ai(±∞) → 0 int eff_x = x.effectiveBits(); Float xw = x; xw.truncateToApprox(precision + 30); // Activate the asymptotic expansion: only for large |x| and ζ > ζ_cut. Leave the Maclaurin route unchanged if (airyAi_should_use_asymptotic(xw, precision)) { Float result = xw.isNegative() ? airyAi_asymptotic_neg(xw, precision) : airyAi_asymptotic_pos(xw, precision); finalizeResult(result, eff_x, precision); return result; } // Maclaurin route: the cancellation of f(x), g(x) loses ζ/ln(10) digits, so expand wp int wp = airyMaclaurinDynWp(xw, precision); xw = x; xw.truncateToApprox(wp); // Ai(0), Ai'(0) via the thread_local cache (keyed by precision) const AiryConstantsCache& constants = airyConstants(wp); const Float& ai0 = constants.ai0; const Float& aip0 = constants.aip0; if (x.isZero()) { Float result = ai0; finalizeResult(result, eff_x, precision); return result; } // f(x) = Σ_{k=0}^{∞} a_k, where a_0 = 1, a_{k+1} = a_k · x³ / ((3k+2)(3k+3)) // g(x) = Σ_{k=0}^{∞} b_k, where b_0 = x, b_{k+1} = b_k · x³ / ((3k+3)(3k+4)) Float x3 = xw * xw * xw; x3.truncateToApprox(wp); Float f_sum(1); f_sum.setResultPrecision(wp); // a_0 = 1 Float g_sum = xw; Float f_term(1); f_term.setResultPrecision(wp); Float g_term = xw; // Pre-allocate the divisor Floats and reuse them in place (eliminates per-iteration allocation) Float fdiv; fdiv.setResultPrecision(wp); Float gdiv; gdiv.setResultPrecision(wp); int wp_bits = Float::precisionToBits(wp); // Adaptive precision: at wp ≥ ADAPTIVE_THRESHOLD, reduce the truncation digit count as the terms shrink, // cutting the cost of subsequent muls. At small wp the adaptive overhead exceeds the savings, so // use the legacy (fixed wp truncate) path. // At 5000d: 47x → 5.3x (8.8x improvement); at 1000d: 13-17x → 4.2x (3-4x improvement). // Disable adaptive at small wp (≤ 200d) to avoid the 19d/38d regression. constexpr int ADAPTIVE_THRESHOLD = 200; // digits constexpr int MIN_TRUNC_DIGITS = 20; const bool use_adaptive = (wp >= ADAPTIVE_THRESHOLD); for (int k = 0; k < 10 * wp; k++) { int64_t fd = static_cast(3 * k + 2) * (3 * k + 3); int64_t gd = static_cast(3 * k + 3) * (3 * k + 4); fdiv = Float(fd); fdiv.setResultPrecision(wp); gdiv = Float(gd); gdiv.setResultPrecision(wp); FloatOps::mul(f_term, x3, f_term); FloatOps::div(f_term, fdiv, f_term); if (!use_adaptive) f_term.truncateToApprox(wp); FloatOps::add(f_sum, f_term, f_sum); f_sum.truncateToApprox(wp); FloatOps::mul(g_term, x3, g_term); FloatOps::div(g_term, gdiv, g_term); if (!use_adaptive) g_term.truncateToApprox(wp); FloatOps::add(g_sum, g_term, g_sum); g_sum.truncateToApprox(wp); int64_t f_drop = 0, g_drop = 0; if (use_adaptive) { // Adaptive truncate: shorten f_term, g_term according to their shrinking magnitude int64_t ft_mag = f_term.exponent() + static_cast(f_term.mantissa().bitLength()); int64_t fs_mag = f_sum.exponent() + static_cast(f_sum.mantissa().bitLength()); f_drop = std::max((int64_t)0, fs_mag - ft_mag); int f_bits = static_cast(std::max((int64_t)64, (int64_t)wp_bits + 5 - f_drop)); int f_digits = std::max(MIN_TRUNC_DIGITS, static_cast((f_bits - 8) / 3.32192809488736) + 2); f_term.truncateToApprox(std::min(wp, f_digits)); int64_t gt_mag = g_term.exponent() + static_cast(g_term.mantissa().bitLength()); int64_t gs_mag = g_sum.exponent() + static_cast(g_sum.mantissa().bitLength()); g_drop = std::max((int64_t)0, gs_mag - gt_mag); int g_bits = static_cast(std::max((int64_t)64, (int64_t)wp_bits + 5 - g_drop)); int g_digits = std::max(MIN_TRUNC_DIGITS, static_cast((g_bits - 8) / 3.32192809488736) + 2); g_term.truncateToApprox(std::min(wp, g_digits)); } else { int64_t ft_mag = f_term.exponent() + static_cast(f_term.mantissa().bitLength()); int64_t fs_mag = f_sum.exponent() + static_cast(f_sum.mantissa().bitLength()); f_drop = std::max((int64_t)0, fs_mag - ft_mag); int64_t gt_mag = g_term.exponent() + static_cast(g_term.mantissa().bitLength()); int64_t gs_mag = g_sum.exponent() + static_cast(g_sum.mantissa().bitLength()); g_drop = std::max((int64_t)0, gs_mag - gt_mag); } if (k >= 3) { bool f_conv = f_term.isZero() || (f_drop > wp_bits + 5); bool g_conv = g_term.isZero() || (g_drop > wp_bits + 5); if (f_conv && g_conv) break; } } Float result = ai0 * f_sum + aip0 * g_sum; finalizeResult(result, eff_x, precision); return result; } Float airyAi(Float&& x, int precision) { return airyAi(static_cast(x), precision); } Float airyBi(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) { if (x.isNegative()) return Float(0); // Bi(-∞) → 0 return Float::positiveInfinity(); // Bi(+∞) → +∞ } int eff_x = x.effectiveBits(); Float xw = x; xw.truncateToApprox(precision + 30); // Maclaurin: cancellation correction int wp = airyMaclaurinDynWp(xw, precision); xw = x; xw.truncateToApprox(wp); // Bi(0) = 1 / (3^{1/6} · Γ(2/3)) // Bi'(0) = 3^{1/6} / Γ(1/3) Float::PrecisionScope _ps(wp); // Compute exact÷exact of the constants at wp digits (old: setResultPrecision each) Float three(3); Float two(2); Float one(1); Float six(6); Float one_sixth = one / six; Float two_thirds = two / three; Float one_third = one / three; Float bi0 = one / (pow(three, one_sixth, wp) * gamma(two_thirds, wp)); Float bip0 = pow(three, one_sixth, wp) / gamma(one_third, wp); bi0.truncateToApprox(wp); bip0.truncateToApprox(wp); if (x.isZero()) { finalizeResult(bi0, eff_x, precision); return bi0; } // Same f(x), g(x) series Float x3 = xw * xw * xw; x3.truncateToApprox(wp); Float f_sum(1); f_sum.setResultPrecision(wp); Float g_sum = xw; Float f_term(1); f_term.setResultPrecision(wp); Float g_term = xw; for (int k = 0; k < 10 * wp; k++) { Float fdiv(static_cast(3 * k + 2) * (3 * k + 3)); fdiv.setResultPrecision(wp); FloatOps::mul(f_term, x3, f_term); FloatOps::div(f_term, fdiv, f_term); f_term.truncateToApprox(wp); FloatOps::add(f_sum, f_term, f_sum); f_sum.truncateToApprox(wp); Float gdiv(static_cast(3 * k + 3) * (3 * k + 4)); gdiv.setResultPrecision(wp); FloatOps::mul(g_term, x3, g_term); FloatOps::div(g_term, gdiv, g_term); g_term.truncateToApprox(wp); FloatOps::add(g_sum, g_term, g_sum); g_sum.truncateToApprox(wp); if (k >= 3) { bool f_conv = f_term.isZero(); bool g_conv = g_term.isZero(); if (!f_conv) { auto ft = f_term.exponent() + static_cast(f_term.mantissa().bitLength()); auto fs = f_sum.exponent() + static_cast(f_sum.mantissa().bitLength()); f_conv = (fs - ft > Float::precisionToBits(wp) + 5); } if (!g_conv) { auto gt = g_term.exponent() + static_cast(g_term.mantissa().bitLength()); auto gs = g_sum.exponent() + static_cast(g_sum.mantissa().bitLength()); g_conv = (gs - gt > Float::precisionToBits(wp) + 5); } if (f_conv && g_conv) break; } } Float result = bi0 * f_sum + bip0 * g_sum; finalizeResult(result, eff_x, precision); return result; } Float airyBi(Float&& x, int precision) { return airyBi(static_cast(x), precision); } //============================================================================= // Derivatives of the Airy functions Ai'(x), Bi'(x) //============================================================================= // Term-wise differentiation of the Maclaurin series: // f(x) = Σ c_k·x^{3k}, f'(x) = Σ_{k≥1} 3k·c_k·x^{3k-1} // g(x) = Σ d_k·x^{3k+1}, g'(x) = Σ_{k≥0} (3k+1)·d_k·x^{3k} // // After updating f_term, g_term inside the loop: // fp_contrib = 3(k+1) · f_term / x // gp_contrib = (3k+4) · g_term / x // // Ai'(x) = Ai(0)·f'(x) + Ai'(0)·g'(x) // Bi'(x) = Bi(0)·f'(x) + Bi'(0)·g'(x) Float airyAiPrime(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) return Float(0); // Ai'(±∞) → 0 int eff_x = x.effectiveBits(); Float xw = x; xw.truncateToApprox(precision + 30); // Maclaurin: cancellation correction (the derivatives are dominated by the same ζ) int wp = airyMaclaurinDynWp(xw, precision); xw = x; xw.truncateToApprox(wp); Float::PrecisionScope _ps(wp); // Compute exact÷exact of the constants at wp digits (old: setResultPrecision each) Float three(3); Float two(2); Float one(1); Float two_thirds = two / three; Float one_third = one / three; Float ai0 = one / (pow(three, two_thirds, wp) * gamma(two_thirds, wp)); Float aip0 = -one / (pow(three, one_third, wp) * gamma(one_third, wp)); ai0.truncateToApprox(wp); aip0.truncateToApprox(wp); // Ai'(0) = aip0 (f'(0) = 0, g'(0) = 1) if (x.isZero()) { finalizeResult(aip0, eff_x, precision); return aip0; } Float x3 = xw * xw * xw; x3.truncateToApprox(wp); Float inv_x = one / xw; inv_x.truncateToApprox(wp); Float f_term(1); f_term.setResultPrecision(wp); Float g_term = xw; Float fp_sum(0); fp_sum.setResultPrecision(wp); // f'(0) = 0 Float gp_sum(1); gp_sum.setResultPrecision(wp); // g'(0) = 1 Float fp_contrib(0), gp_contrib(0); for (int k = 0; k < 10 * wp; k++) { Float fdiv(static_cast(3 * k + 2) * (3 * k + 3)); fdiv.setResultPrecision(wp); FloatOps::mul(f_term, x3, f_term); FloatOps::div(f_term, fdiv, f_term); f_term.truncateToApprox(wp); Float gdiv(static_cast(3 * k + 3) * (3 * k + 4)); gdiv.setResultPrecision(wp); FloatOps::mul(g_term, x3, g_term); FloatOps::div(g_term, gdiv, g_term); g_term.truncateToApprox(wp); Float f_mul(3 * (k + 1)); f_mul.setResultPrecision(wp); Float g_mul(3 * k + 4); g_mul.setResultPrecision(wp); FloatOps::mul(f_term, inv_x, fp_contrib); FloatOps::mul(fp_contrib, f_mul, fp_contrib); fp_contrib.truncateToApprox(wp); FloatOps::mul(g_term, inv_x, gp_contrib); FloatOps::mul(gp_contrib, g_mul, gp_contrib); gp_contrib.truncateToApprox(wp); FloatOps::add(fp_sum, fp_contrib, fp_sum); fp_sum.truncateToApprox(wp); FloatOps::add(gp_sum, gp_contrib, gp_sum); gp_sum.truncateToApprox(wp); if (k >= 3) { bool fp_conv = fp_contrib.isZero(); bool gp_conv = gp_contrib.isZero(); if (!fp_conv) { auto ft = fp_contrib.exponent() + static_cast(fp_contrib.mantissa().bitLength()); auto fs = fp_sum.exponent() + static_cast(fp_sum.mantissa().bitLength()); fp_conv = (fs - ft > Float::precisionToBits(wp) + 5); } if (!gp_conv) { auto gt = gp_contrib.exponent() + static_cast(gp_contrib.mantissa().bitLength()); auto gs = gp_sum.exponent() + static_cast(gp_sum.mantissa().bitLength()); gp_conv = (gs - gt > Float::precisionToBits(wp) + 5); } if (fp_conv && gp_conv) break; } } Float result = ai0 * fp_sum + aip0 * gp_sum; finalizeResult(result, eff_x, precision); return result; } Float airyAiPrime(Float&& x, int precision) { return airyAiPrime(static_cast(x), precision); } Float airyBiPrime(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isInfinity()) { if (x.isNegative()) return Float(0); // Bi'(-∞) → 0 return Float::positiveInfinity(); // Bi'(+∞) → +∞ } int eff_x = x.effectiveBits(); Float xw = x; xw.truncateToApprox(precision + 30); // Maclaurin: cancellation correction int wp = airyMaclaurinDynWp(xw, precision); xw = x; xw.truncateToApprox(wp); Float::PrecisionScope _ps(wp); // Compute exact÷exact of the constants at wp digits (old: setResultPrecision each) Float three(3); Float two(2); Float one(1); Float six(6); Float one_sixth = one / six; Float two_thirds = two / three; Float one_third = one / three; Float bi0 = one / (pow(three, one_sixth, wp) * gamma(two_thirds, wp)); Float bip0 = pow(three, one_sixth, wp) / gamma(one_third, wp); bi0.truncateToApprox(wp); bip0.truncateToApprox(wp); // Bi'(0) = bip0 if (x.isZero()) { finalizeResult(bip0, eff_x, precision); return bip0; } Float x3 = xw * xw * xw; x3.truncateToApprox(wp); Float inv_x = one / xw; inv_x.truncateToApprox(wp); Float f_term(1); f_term.setResultPrecision(wp); Float g_term = xw; Float fp_sum(0); fp_sum.setResultPrecision(wp); Float gp_sum(1); gp_sum.setResultPrecision(wp); Float fp_contrib(0), gp_contrib(0); for (int k = 0; k < 10 * wp; k++) { Float fdiv(static_cast(3 * k + 2) * (3 * k + 3)); fdiv.setResultPrecision(wp); FloatOps::mul(f_term, x3, f_term); FloatOps::div(f_term, fdiv, f_term); f_term.truncateToApprox(wp); Float gdiv(static_cast(3 * k + 3) * (3 * k + 4)); gdiv.setResultPrecision(wp); FloatOps::mul(g_term, x3, g_term); FloatOps::div(g_term, gdiv, g_term); g_term.truncateToApprox(wp); Float f_mul(3 * (k + 1)); f_mul.setResultPrecision(wp); Float g_mul(3 * k + 4); g_mul.setResultPrecision(wp); FloatOps::mul(f_term, inv_x, fp_contrib); FloatOps::mul(fp_contrib, f_mul, fp_contrib); fp_contrib.truncateToApprox(wp); FloatOps::mul(g_term, inv_x, gp_contrib); FloatOps::mul(gp_contrib, g_mul, gp_contrib); gp_contrib.truncateToApprox(wp); FloatOps::add(fp_sum, fp_contrib, fp_sum); fp_sum.truncateToApprox(wp); FloatOps::add(gp_sum, gp_contrib, gp_sum); gp_sum.truncateToApprox(wp); if (k >= 3) { bool fp_conv = fp_contrib.isZero(); bool gp_conv = gp_contrib.isZero(); if (!fp_conv) { auto ft = fp_contrib.exponent() + static_cast(fp_contrib.mantissa().bitLength()); auto fs = fp_sum.exponent() + static_cast(fp_sum.mantissa().bitLength()); fp_conv = (fs - ft > Float::precisionToBits(wp) + 5); } if (!gp_conv) { auto gt = gp_contrib.exponent() + static_cast(gp_contrib.mantissa().bitLength()); auto gs = gp_sum.exponent() + static_cast(gp_sum.mantissa().bitLength()); gp_conv = (gs - gt > Float::precisionToBits(wp) + 5); } if (fp_conv && gp_conv) break; } } Float result = bi0 * fp_sum + bip0 * gp_sum; finalizeResult(result, eff_x, precision); return result; } Float airyBiPrime(Float&& x, int precision) { return airyBiPrime(static_cast(x), precision); } //============================================================================= // Confluent hypergeometric limit function ₀F₁(; b; z) //============================================================================= // ₀F₁(; b; z) = Σ_{k=0}^∞ z^k / ((b)_k · k!) // term_{k+1} / term_k = z / ((b+k) · (k+1)) // Converges for all z Float hyperg0F1(const Float& b, const Float& z, int precision) { if (b.isNaN() || z.isNaN()) return Float::nan(); if (z.isZero()) return Float(1); // b a non-positive integer → pole if (!b.isPositive() && b.isInteger()) return Float::nan(); int eff = std::min(b.effectiveBits(), z.effectiveBits()); int wp = precision + 20; Float::PrecisionScope _ps(wp); // Compute exact÷exact via FloatOps::div at wp digits (contextBits) Float bw = b; bw.truncateToApprox(wp); Float zw = z; zw.truncateToApprox(wp); Float term(1); Float sum(1); Float tmp(0); for (int k = 0; k < 10 * wp; k++) { FloatOps::mul(term, zw, term); FloatOps::add(bw, Float(k), tmp); FloatOps::mul(tmp, Float(k + 1), tmp); FloatOps::div(term, tmp, term); term.truncateToApprox(wp); FloatOps::add(sum, term, sum); sum.truncateToApprox(wp); if (k >= 3 && term.isZero()) break; if (k >= 3) { auto t_bits = term.exponent() + static_cast(term.mantissa().bitLength()); auto s_bits = sum.exponent() + static_cast(sum.mantissa().bitLength()); if (s_bits - t_bits > Float::precisionToBits(wp) + 5) break; } } finalizeResult(sum, eff, precision); return sum; } Float hyperg0F1(Float&& b, Float&& z, int precision) { return hyperg0F1(static_cast(b), static_cast(z), precision); } //============================================================================= // Confluent hypergeometric function ₁F₁(a; b; z) (Kummer M) //============================================================================= // M(a, b, z) = Σ_{k=0}^∞ (a)_k z^k / ((b)_k · k!) // term_{k+1} / term_k = (a+k) · z / ((b+k) · (k+1)) // // Kummer transformation (when z < 0): M(a,b,z) = e^z · M(b-a, b, -z) // Avoids the cancellation of the alternating series and accelerates convergence Float confHyperg(const Float& a, const Float& b, const Float& z, int precision) { Float::PrecisionScope _ps(precision + 64); // Compute exact÷exact inside the function at working precision if (a.isNaN() || b.isNaN() || z.isNaN()) return Float::nan(); if (z.isZero()) return Float(1); // b a non-positive integer → pole if (!b.isPositive() && b.isInteger()) return Float::nan(); // a = 0 → 1 if (a.isZero()) return Float(1); int eff = std::min({a.effectiveBits(), b.effectiveBits(), z.effectiveBits()}); int wp = precision + 30; Float aw = a; aw.truncateToApprox(wp); Float bw = b; bw.truncateToApprox(wp); Float zw = z; zw.truncateToApprox(wp); // a = b → e^z if (aw == bw) { Float result = exp(zw, wp); finalizeResult(result, eff, precision); return result; } // Kummer transformation: when z < 0, M(a,b,z) = e^z · M(b-a, b, -z) // Since -z > 0, the recursive call does not enter the Kummer path if (zw.isNegative()) { Float ez = exp(zw, wp); Float inner = confHyperg(bw - aw, bw, -zw, wp); inner.truncateToApprox(wp); Float result = ez * inner; finalizeResult(result, eff, precision); return result; } // a a non-positive integer → polynomial (finite sum) bool is_poly = false; int poly_terms = 0; if (aw.isNegative() && aw.isInteger()) { is_poly = true; poly_terms = -static_cast(aw.toDouble()) + 1; } // Taylor series Float term(1); Float sum(1); int max_iter = is_poly ? poly_terms : 10 * wp; for (int k = 0; k < max_iter; k++) { term = term * (aw + Float(k)) * zw / ((bw + Float(k)) * (k + 1)); term.truncateToApprox(wp); sum = sum + term; sum.truncateToApprox(wp); if (!is_poly && k >= 3 && term.isZero()) break; if (!is_poly && k >= 3) { auto t_bits = term.exponent() + static_cast(term.mantissa().bitLength()); auto s_bits = sum.exponent() + static_cast(sum.mantissa().bitLength()); if (s_bits - t_bits > Float::precisionToBits(wp) + 5) break; } } finalizeResult(sum, eff, precision); return sum; } Float confHyperg(Float&& a, Float&& b, Float&& z, int precision) { return confHyperg(static_cast(a), static_cast(b), static_cast(z), precision); } //============================================================================= // Gauss hypergeometric function ₂F₁(a, b; c; z) //============================================================================= // ₂F₁(a, b; c; z) = Σ_{k=0}^∞ (a)_k (b)_k z^k / ((c)_k · k!) // Converges for |z| < 1. Domain strategy: // |z| < 0.5 → direct Taylor // z < -0.5 → Pfaff transformation: (1-z)^{-a} · ₂F₁(a, c-b; c; z/(z-1)) // 0.5 ≤ z < 1 → DLMF 15.8.1 connection formula (evaluate at 1-z) // polynomial case → always direct Taylor Float hyperg(const Float& a, const Float& b, const Float& c, const Float& z, int precision) { if (a.isNaN() || b.isNaN() || c.isNaN() || z.isNaN()) return Float::nan(); if (z.isZero()) return Float(1); if (a.isZero() || b.isZero()) return Float(1); int eff = std::min({a.effectiveBits(), b.effectiveBits(), c.effectiveBits(), z.effectiveBits()}); int wp = precision + 40; // For dyadic exact inputs (0.5/1.5/2.5 etc.), FloatOps::div poisons as exact/exact on the direct Taylor route, // so set a scope exceeding the working precision of every branch (wp=+40, +80 near integers). Float::PrecisionScope _ps(precision + 80); Float aw = a; aw.truncateToApprox(wp); Float bw = b; bw.truncateToApprox(wp); Float cw = c; cw.truncateToApprox(wp); Float zw = z; zw.truncateToApprox(wp); // c a non-positive integer → pole unless a or b terminates first if (!cw.isPositive() && cw.isInteger()) { int ci = static_cast(cw.toDouble()); bool a_ok = aw.isNegative() && aw.isInteger() && static_cast(aw.toDouble()) >= ci; bool b_ok = bw.isNegative() && bw.isInteger() && static_cast(bw.toDouble()) >= ci; if (!a_ok && !b_ok) return Float::nan(); } // Detect the polynomial case bool is_poly = false; int poly_terms = 0; if (aw.isNegative() && aw.isInteger()) { is_poly = true; poly_terms = -static_cast(aw.toDouble()) + 1; } else if (bw.isNegative() && bw.isInteger()) { is_poly = true; poly_terms = -static_cast(bw.toDouble()) + 1; } double z_val = zw.toDouble(); double z_abs = std::abs(z_val); // |z| >= 1 and non-polynomial → diverges if (!is_poly && z_abs >= 1.0) return Float::nan(); // --- Pfaff transformation: z < -0.5 --- // ₂F₁(a,b;c;z) = (1-z)^{-a} · ₂F₁(a, c-b; c; z/(z-1)) // z/(z-1) ∈ (0, 1/3], so Taylor converges fast if (!is_poly && z_val < -0.5) { Float one_minus_z = Float(1) - zw; one_minus_z.truncateToApprox(wp); Float z_mapped = zw / (zw - Float(1)); z_mapped.truncateToApprox(wp); Float prefix = pow(one_minus_z, -aw, wp); prefix.truncateToApprox(wp); Float inner = hyperg(aw, cw - bw, cw, z_mapped, wp); inner.truncateToApprox(wp); Float result = prefix * inner; finalizeResult(result, eff, precision); return result; } // --- DLMF 15.8.1 connection formula: 0.5 ≤ z < 1 --- // ₂F₁(a,b;c;z) = Γ(c)Γ(c-a-b) / [Γ(c-a)Γ(c-b)] · ₂F₁(a,b;a+b-c+1;1-z) // + (1-z)^{c-a-b} · Γ(c)Γ(a+b-c) / [Γ(a)Γ(b)] · ₂F₁(c-a,c-b;c-a-b+1;1-z) // Since 1-z ∈ (0, 0.5), both ₂F₁ converge via Taylor. // ★ z=0.5 is strictly excluded (with >=, w=1-z=0.5 maps to itself and causes infinite recursion; fixed 2026-06-02). // z=0.5 converges via direct Taylor (ratio→0.5), so leave it to the fall-through below. if (!is_poly && z_val > 0.5) { Float w = Float(1) - zw; // w ∈ (0, 0.5] w.truncateToApprox(wp); Float cab = cw - aw - bw; cab.truncateToApprox(wp); // If c-a-b is near an integer, use direct Taylor (due to precision loss) double cab_val = cab.toDouble(); double cab_frac = cab_val - std::floor(cab_val); if (cab_frac < 0.01 || cab_frac > 0.99) { // Near integer: the connection formula is unstable, so use direct Taylor with extra guard bits wp = precision + 80; aw.truncateToApprox(wp); bw.truncateToApprox(wp); cw.truncateToApprox(wp); zw = z; zw.truncateToApprox(wp); Float term(1); Float sum(1); for (int k = 0; k < 10 * wp; k++) { term = term * (aw + Float(k)) * (bw + Float(k)) * zw / ((cw + Float(k)) * (k + 1)); term.truncateToApprox(wp); sum = sum + term; sum.truncateToApprox(wp); if (k >= 3 && term.isZero()) break; if (k >= 3) { auto t_bits = term.exponent() + static_cast(term.mantissa().bitLength()); auto s_bits = sum.exponent() + static_cast(sum.mantissa().bitLength()); if (s_bits - t_bits > Float::precisionToBits(wp) + 5) break; } } finalizeResult(sum, eff, precision); return sum; } // Compute the two terms of the connection formula Float gc = gamma(cw, wp); Float g_cab = gamma(cab, wp); Float g_neg_cab = gamma(-cab, wp); // Note: Γ(a+b-c) is Γ(-(c-a-b)), not Γ(-cab) // Term 1: Γ(c)·Γ(c-a-b) / [Γ(c-a)·Γ(c-b)] · ₂F₁(a, b; a+b-c+1; 1-z) Float coeff1 = gc * g_cab / (gamma(cw - aw, wp) * gamma(cw - bw, wp)); coeff1.truncateToApprox(wp); Float f1 = hyperg(aw, bw, aw + bw - cw + Float(1), w, wp); f1.truncateToApprox(wp); // Term 2: (1-z)^{c-a-b} · Γ(c)·Γ(a+b-c) / [Γ(a)·Γ(b)] · ₂F₁(c-a, c-b; c-a-b+1; 1-z) Float ab_minus_c = -(cab); // a+b-c Float coeff2 = gc * gamma(ab_minus_c, wp) / (gamma(aw, wp) * gamma(bw, wp)); coeff2.truncateToApprox(wp); Float f2 = hyperg(cw - aw, cw - bw, cab + Float(1), w, wp); f2.truncateToApprox(wp); Float w_power = pow(w, cab, wp); w_power.truncateToApprox(wp); Float result = coeff1 * f1 + coeff2 * w_power * f2; finalizeResult(result, eff, precision); return result; } // --- Direct Taylor series: |z| < 0.5 or polynomial --- Float term(1); Float sum(1); Float tmp(0); int max_iter = is_poly ? poly_terms : 10 * wp; for (int k = 0; k < max_iter; k++) { // term *= (a+k)*(b+k)*z / ((c+k)*(k+1)) Float fk(k); FloatOps::add(aw, fk, tmp); FloatOps::mul(term, tmp, term); FloatOps::add(bw, fk, tmp); FloatOps::mul(term, tmp, term); FloatOps::mul(term, zw, term); FloatOps::add(cw, fk, tmp); FloatOps::mul(tmp, Float(k + 1), tmp); FloatOps::div(term, tmp, term); term.truncateToApprox(wp); FloatOps::add(sum, term, sum); sum.truncateToApprox(wp); if (!is_poly && k >= 3 && term.isZero()) break; if (!is_poly && k >= 3) { auto t_bits = term.exponent() + static_cast(term.mantissa().bitLength()); auto s_bits = sum.exponent() + static_cast(sum.mantissa().bitLength()); if (s_bits - t_bits > Float::precisionToBits(wp) + 5) break; } } finalizeResult(sum, eff, precision); return sum; } Float hyperg(Float&& a, Float&& b, Float&& c, Float&& z, int precision) { return hyperg(static_cast(a), static_cast(b), static_cast(c), static_cast(z), precision); } //============================================================================= // Legendre polynomials P_n(x) //============================================================================= // Bonnet recurrence: (n+1)P_{n+1} = (2n+1)·x·P_n - n·P_{n-1} Float legendreP(int n, const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (n < 0) return Float::nan(); if (n == 0) return Float(1); int eff_x = x.effectiveBits(); if (n == 1) { Float r = x; finalizeResult(r, eff_x, precision); return r; } int wp = precision + 10; Float xw = x; xw.truncateToApprox(wp); Float p_prev(1); // P_0 Float p_curr = xw; // P_1 for (int k = 1; k < n; k++) { Float p_next = ((2 * k + 1) * xw * p_curr - k * p_prev) / (k + 1); p_next.truncateToApprox(wp); p_prev = p_curr; p_curr = p_next; } finalizeResult(p_curr, eff_x, precision); return p_curr; } Float legendreP(int n, Float&& x, int precision) { return legendreP(n, static_cast(x), precision); } //============================================================================= // Associated Legendre functions P_n^m(x) //============================================================================= // Without the Condon-Shortley phase factor // P_m^m = (2m-1)!! · (1-x²)^{m/2} // P_{m+1}^m = x·(2m+1)·P_m^m // (k-m+1)P_{k+1}^m = (2k+1)·x·P_k^m - (k+m)·P_{k-1}^m Float assocLegendreP(int n, int m, const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (n < 0 || m < 0) return Float::nan(); if (m > n) return Float(0); if (m == 0) return legendreP(n, x, precision); int eff_x = x.effectiveBits(); int wp = precision + 15; Float xw = x; xw.truncateToApprox(wp); // (1 - x²)^{1/2} Float sin2 = Float(1) - xw * xw; sin2.truncateToApprox(wp); if (sin2.isNegative()) sin2 = Float(0); Float sin_factor = sqrt(sin2, wp); // P_m^m = (2m-1)!! · (1-x²)^{m/2} Float pmm(1); for (int i = 1; i <= m; i++) { pmm = pmm * (2 * i - 1) * sin_factor; pmm.truncateToApprox(wp); } if (n == m) { finalizeResult(pmm, eff_x, precision); return pmm; } // P_{m+1}^m = x·(2m+1)·P_m^m Float pm1m = xw * (2 * m + 1) * pmm; pm1m.truncateToApprox(wp); if (n == m + 1) { finalizeResult(pm1m, eff_x, precision); return pm1m; } // Recurrence Float p_prev = pmm; Float p_curr = pm1m; for (int k = m + 1; k < n; k++) { Float p_next = ((2 * k + 1) * xw * p_curr - (k + m) * p_prev) / (k - m + 1); p_next.truncateToApprox(wp); p_prev = p_curr; p_curr = p_next; } finalizeResult(p_curr, eff_x, precision); return p_curr; } Float assocLegendreP(int n, int m, Float&& x, int precision) { return assocLegendreP(n, m, static_cast(x), precision); } //============================================================================= // Hermite polynomials H_n(x) — physicists' version //============================================================================= // H_0(x) = 1, H_1(x) = 2x // H_{n+1}(x) = 2x·H_n(x) - 2n·H_{n-1}(x) Float hermite(int n, const Float& x, int precision) { if (n < 0) return Float::nan(); if (n == 0) return Float::one(precision); int wp = precision + 10; Float two(2); Float h_prev = Float::one(wp); // H_0 Float h_curr = two * x; // H_1 h_curr.truncateToApprox(wp); for (int k = 1; k < n; ++k) { Float h_next = two * x * h_curr - two * k * h_prev; h_next.truncateToApprox(wp); h_prev = std::move(h_curr); h_curr = std::move(h_next); } finalizeResult(h_curr, x.effectiveBits(), precision); return h_curr; } Float hermite(int n, Float&& x, int precision) { return hermite(n, static_cast(x), precision); } //============================================================================= // Laguerre polynomials L_n(x) //============================================================================= // L_0(x) = 1, L_1(x) = 1 - x // (n+1)·L_{n+1}(x) = (2n+1-x)·L_n(x) - n·L_{n-1}(x) Float laguerre(int n, const Float& x, int precision) { if (n < 0) return Float::nan(); if (n == 0) return Float::one(precision); int wp = precision + 10; const int wp_bits = Float::precisionToBits(wp); // BUGFIX (2026-05-30): the recurrence's /(k+1) falls to default_precision via exact/exact // division for exact inputs (e.g. x=0.5). Make the working values non-exact so the // division is performed at wp precision (same idiom as AUDIT_FLOAT_UNIT_MIXING step4 / the erfcx family). Float l_prev = Float::one(wp); // L_0 l_prev.setEffectiveBits(wp_bits); Float l_curr = Float::one(wp) - x; // L_1 l_curr.truncateToApprox(wp); l_curr.setEffectiveBits(wp_bits); for (int k = 1; k < n; ++k) { Float l_next = (Float(2 * k + 1) - x) * l_curr - k * l_prev; // Float/Float division (extends to working precision). Float/int (divScalarF) only // integer-divides the mantissa without extending precision, breaking for exact inputs. l_next = l_next / Float(k + 1); l_next.truncateToApprox(wp); l_prev = std::move(l_curr); l_curr = std::move(l_next); } finalizeResult(l_curr, x.effectiveBits(), precision); return l_curr; } Float laguerre(int n, Float&& x, int precision) { return laguerre(n, static_cast(x), precision); } //============================================================================= // Associated Laguerre polynomials L_n^m(x) //============================================================================= // L_0^m(x) = 1, L_1^m(x) = 1 + m - x // (n+1)·L_{n+1}^m(x) = (2n+1+m-x)·L_n^m(x) - (n+m)·L_{n-1}^m(x) Float assocLaguerre(int n, int m, const Float& x, int precision) { if (n < 0) return Float::nan(); if (n == 0) return Float::one(precision); int wp = precision + 10; const int wp_bits = Float::precisionToBits(wp); // BUGFIX (2026-05-30): avoid exact/exact poison of /(k+1) (same form as laguerre). Float l_prev = Float::one(wp); // L_0^m l_prev.setEffectiveBits(wp_bits); Float l_curr = Float(1 + m) - x; // L_1^m l_curr.truncateToApprox(wp); l_curr.setEffectiveBits(wp_bits); for (int k = 1; k < n; ++k) { Float l_next = (Float(2 * k + 1 + m) - x) * l_curr - (k + m) * l_prev; l_next = l_next / Float(k + 1); // Float/Float (precision extension); Float/int does not work l_next.truncateToApprox(wp); l_prev = std::move(l_curr); l_curr = std::move(l_next); } finalizeResult(l_curr, x.effectiveBits(), precision); return l_curr; } Float assocLaguerre(int n, int m, Float&& x, int precision) { return assocLaguerre(n, m, static_cast(x), precision); } //============================================================================= // Lambert W₀(x) — principal branch //============================================================================= // W(x)·e^{W(x)} = x, W₀ ≥ -1 // Halley iteration: cubic convergence // Body: takes x by value (assumes already moved) static Float lambertW0_core(Float x, int eff_x, int precision) { int wp = precision + 20; x.truncateToApprox(wp); // x < -1/e → out of domain Float neg_inv_e = Float(-1) / exp(Float(1), wp); neg_inv_e.truncateToApprox(wp); if (x < neg_inv_e) return Float::nan(); // Initial estimate in double double xd = x.toDouble(); double wd; if (xd < -0.3) { double p = std::sqrt(2.0 * (2.718281828459045 * xd + 1.0)); wd = -1.0 + p - p * p / 3.0 + 11.0 * p * p * p / 72.0; } else if (xd <= 3.0) { if (xd <= 0.5) { wd = xd * (1.0 - xd); } else { double lnx1 = std::log(xd + 1.0); wd = 0.665 * (1.0 + 0.0195 * lnx1) * lnx1 + 0.04; } } else { double lnx = std::log(xd); wd = lnx - std::log(lnx); } Float w(wd); w.truncateToApprox(wp); // Halley iteration for (int i = 0; i < 10 * (wp / 50 + 1); i++) { Float ew = exp(w, wp); Float wew = w * ew; wew.truncateToApprox(wp); Float f = wew - x; f.truncateToApprox(wp); if (f.isZero()) break; // Convergence check auto f_bits = f.exponent() + static_cast(f.mantissa().bitLength()); auto x_bits = x.isZero() ? int64_t(0) : x.exponent() + static_cast(x.mantissa().bitLength()); if (x_bits - f_bits > Float::precisionToBits(wp) + 5) break; Float wp1 = w + Float(1); wp1.truncateToApprox(wp); Float denom = ew * wp1 - (w + Float(2)) * f / ldexp(wp1, 1); denom.truncateToApprox(wp); if (denom.isZero()) break; w = w - f / denom; w.truncateToApprox(wp); // Halley convergence improves the effective precision of w each iteration. // Update eff so the effective_bits_ optimization of exp(w,wp) works correctly. w.setResultPrecision(wp); } finalizeResult(w, eff_x, precision); return w; } Float lambertW0(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isZero()) return Float(0); if (x.isInfinity() && x.isPositive()) return Float::positiveInfinity(); return lambertW0_core(Float(x), x.effectiveBits(), precision); } Float lambertW0(Float&& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isZero()) return Float(0); if (x.isInfinity() && x.isPositive()) return Float::positiveInfinity(); int eff = x.effectiveBits(); return lambertW0_core(std::move(x), eff, precision); } //============================================================================= // Lambert W₋₁(x) — secondary branch //============================================================================= // Defined for -1/e ≤ x < 0. W₋₁ ≤ -1. // Body: takes x by value (assumes already moved) static Float lambertWm1_core(Float x, int eff_x, int precision) { int wp = precision + 20; x.truncateToApprox(wp); // x < -1/e or x >= 0 → out of domain Float neg_inv_e = Float(-1) / exp(Float(1), wp); neg_inv_e.truncateToApprox(wp); if (x < neg_inv_e || !x.isNegative()) return Float::nan(); // Initial estimate in double double xd = x.toDouble(); double wd; if (xd > -0.1) { double p = std::sqrt(2.0 * (2.718281828459045 * xd + 1.0)); wd = -1.0 - p - p * p / 3.0 - 11.0 * p * p * p / 72.0; } else { double lnmx = std::log(-xd); wd = lnmx - std::log(-lnmx); } Float w(wd); w.truncateToApprox(wp); // Halley iteration for (int i = 0; i < 10 * (wp / 50 + 1); i++) { Float ew = exp(w, wp); Float wew = w * ew; wew.truncateToApprox(wp); Float f = wew - x; f.truncateToApprox(wp); if (f.isZero()) break; auto f_bits = f.exponent() + static_cast(f.mantissa().bitLength()); auto x_bits = x.exponent() + static_cast(x.mantissa().bitLength()); if (x_bits - f_bits > Float::precisionToBits(wp) + 5) break; Float wp1 = w + Float(1); wp1.truncateToApprox(wp); Float denom = ew * wp1 - (w + Float(2)) * f / ldexp(wp1, 1); denom.truncateToApprox(wp); if (denom.isZero()) break; w = w - f / denom; w.truncateToApprox(wp); w.setResultPrecision(wp); } finalizeResult(w, eff_x, precision); return w; } Float lambertWm1(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); return lambertWm1_core(Float(x), x.effectiveBits(), precision); } Float lambertWm1(Float&& x, int precision) { if (x.isNaN()) return Float::nan(); int eff = x.effectiveBits(); return lambertWm1_core(std::move(x), eff, precision); } //============================================================================= // Generalized exponential integral E_n(x) = ∫₁^∞ e^{-xt}/t^n dt //============================================================================= // E_1(x) = -Ei(-x) (x > 0) // Recurrence: k·E_{k+1}(x) = e^{-x} - x·E_k(x) // Continued fraction: for large x // Body: takes x by value (assumes already moved) // Compute E_1(x) via a continued fraction (fast convergence for x >= 1, no cancellation) // CF: E_1(x) = e^{-x} / (x + 1/(1 + 1/(x + 2/(1 + 2/(x + ...))))) // Modified Lentz method: converges via recurrences of a_i, b_i static Float expintN_cf(int n, const Float& x, int wp) { // Gauss CF: E_n(x) = e^{-x} * CF // b_0 = x + n, a_i = -i*(n-1+i), b_i = b_{i-1} + 2 Float tiny(1); tiny.truncateToApprox(wp); tiny = tiny >> (3 * wp); // ~2^{-3*wp} as tiny value Float b = x + Float(n); b.truncateToApprox(wp); Float c = Float(1) / tiny; Float d = Float(1) / b; Float h = d; for (int i = 1; i < 10 * wp; i++) { Float a = Float(-i) * Float(n - 1 + i); b = b + Float(2); b.truncateToApprox(wp); d = Float(1) / (a * d + b); d.truncateToApprox(wp); c = b + a / c; c.truncateToApprox(wp); Float delta = c * d; h = h * delta; h.truncateToApprox(wp); // Convergence check: |delta - 1| < 2^{-wp_bits} // BUGFIX (2026-05-30): -wp misused decimal digits as a bit exponent (unit mixing), // capping at ~0.3·P digits → -precisionToBits(wp). Float diff = delta - Float(1); if (i >= 3 && diff.isZero()) break; if (i >= 3) { auto d_bits = diff.exponent() + static_cast(diff.mantissa().bitLength()); if (d_bits < -Float::precisionToBits(wp)) break; } } Float result = exp(-x, wp) * h; result.truncateToApprox(wp); return result; } static Float expintN_core(int n, Float x, int eff_x, int precision) { Float::PrecisionScope _ps(precision + 64); // Compute exact÷exact inside the function at working precision int wp = precision + 40; // More guard bits (to counter alternating-series cancellation) x.truncateToApprox(wp); x.setEffectiveBits(Float::precisionToBits(wp)); // avoid exact/exact poison of exact inputs // E_0(x) = e^{-x}/x if (n == 0) { Float result = exp(-x, wp) / x; finalizeResult(result, eff_x, precision); return result; } // x > 20: obtain E_1 via continued fraction, derive n > 1 via the upward recurrence // (applying the CF directly to n > 1 loses precision, so going through E_1 is stable). // BUGFIX (2026-05-30): raised the threshold from x>=1 to x>20. The Gauss CF has an x-dependent // ceiling on attainable precision (x=1→~84 digits, x=3→~144 digits), failing a 200-digit request for // moderate x. The series E_1(x)=-γ-ln x+Σ converges fully with ~0.43·x digits cancellation (≤9 for // x≤20, within the +40 guard), so use the series for x≤20 and the CF only for x>20 (ceiling high enough). if (x > Float(20)) { Float e1 = expintN_cf(1, x, wp); if (n == 1) { finalizeResult(e1, eff_x, precision); return e1; } Float emx = exp(-x, wp); Float en = e1; for (int k = 1; k < n; k++) { en = (emx - x * en) / Float(k); // Float/Float (precision extension); Float/int does not work en.truncateToApprox(wp); } finalizeResult(en, eff_x, precision); return en; } // x <= 20: series E_1(x) = -γ - ln(x) + Σ_{k=1}^∞ (-1)^{k+1} x^k/(k·k!) Float e1; { Float euler_gamma = Float::euler(wp); Float ln_x = log(x, wp); Float sum = -euler_gamma - ln_x; Float term(1); for (int k = 1; k < 10 * wp; k++) { // Float/Float division (extends to working precision). Float/int (divScalarF) cannot extend // precision for small intermediate terms (occurring at x≳3) and breaks. term = term * (-x) / Float(k); term.truncateToApprox(wp); Float contribution = -term / Float(k); sum = sum + contribution; sum.truncateToApprox(wp); if (k >= 5 && contribution.isZero()) break; if (k >= 5) { auto c_bits = contribution.exponent() + static_cast(contribution.mantissa().bitLength()); auto s_bits = sum.exponent() + static_cast(sum.mantissa().bitLength()); if (s_bits - c_bits > Float::precisionToBits(wp) + 5) break; } } e1 = sum; } if (n == 1) { finalizeResult(e1, eff_x, precision); return e1; } // Upward recurrence: k·E_{k+1}(x) = e^{-x} - x·E_k(x) Float emx = exp(-x, wp); Float en = e1; for (int k = 1; k < n; k++) { en = (emx - x * en) / k; en.truncateToApprox(wp); } finalizeResult(en, eff_x, precision); return en; } Float expintN(int n, const Float& x, int precision) { Float::PrecisionScope _ps(precision); // Compute E_n(0)=1/(n-1) of the x=0 fast path at precision digits if (x.isNaN()) return Float::nan(); if (n < 0) return Float::nan(); if (x.isNegative()) return Float::nan(); if (x.isZero()) { if (n <= 1) return Float::positiveInfinity(); return Float(1) / (n - 1); } return expintN_core(n, Float(x), x.effectiveBits(), precision); } Float expintN(int n, Float&& x, int precision) { Float::PrecisionScope _ps(precision); // Compute E_n(0)=1/(n-1) of the x=0 fast path at precision digits if (x.isNaN()) return Float::nan(); if (n < 0) return Float::nan(); if (x.isNegative()) return Float::nan(); if (x.isZero()) { if (n <= 1) return Float::positiveInfinity(); return Float(1) / (n - 1); } int eff = x.effectiveBits(); return expintN_core(n, std::move(x), eff, precision); } //============================================================================= // Sine integral Si(x) = ∫₀ˣ sin(t)/t dt //============================================================================= // Si(x) = Σ_{k=0}^∞ (-1)^k · x^{2k+1} / ((2k+1)·(2k+1)!) // Odd function: Si(-x) = -Si(x) // Body: takes x by value (assumes already moved) static Float sinIntegral_core(Float x, int eff_x, int precision) { int wp = precision + 20; x.truncateToApprox(wp); x.setEffectiveBits(Float::precisionToBits(wp)); // avoid exact/exact poison of exact inputs // Odd function bool negate = x.isNegative(); if (negate) x = -x; Float x2 = x * x; x2.truncateToApprox(wp); Float neg_x2 = -x2; // term tracks x^{2k+1} / (2k+1)! // Actually: Si(x) = Σ (-1)^k x^{2k+1} / ((2k+1)(2k+1)!) // term = (-1)^k x^{2k+1} / (2k+1)!, contribution = term / (2k+1) // term_{k+1} = term_k · (-x²) / ((2k+2)(2k+3)) Float term = x; // k=0: x^1 / 1! Float sum = x; // k=0: x / 1 Float contribution(0); for (int k = 0; k < 10 * wp; k++) { FloatOps::mul(term, neg_x2, term); FloatOps::div(term, Float(static_cast(2 * k + 2) * (2 * k + 3)), term); term.truncateToApprox(wp); FloatOps::div(term, Float(2 * k + 3), contribution); FloatOps::add(sum, contribution, sum); sum.truncateToApprox(wp); if (k >= 3 && contribution.isZero()) break; if (k >= 3) { auto c_bits = contribution.exponent() + static_cast(contribution.mantissa().bitLength()); auto s_bits = sum.exponent() + static_cast(sum.mantissa().bitLength()); if (s_bits - c_bits > Float::precisionToBits(wp) + 5) break; } } if (negate) sum = -sum; finalizeResult(sum, eff_x, precision); return sum; } Float sinIntegral(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isZero()) return Float(0); return sinIntegral_core(Float(x), x.effectiveBits(), precision); } Float sinIntegral(Float&& x, int precision) { if (x.isNaN()) return Float::nan(); if (x.isZero()) return Float(0); int eff = x.effectiveBits(); return sinIntegral_core(std::move(x), eff, precision); } //============================================================================= // Cosine integral Ci(x) = γ + ln|x| + ∫₀ˣ (cos(t)-1)/t dt //============================================================================= // Ci(x) = γ + ln(x) + Σ_{k=1}^∞ (-1)^k · x^{2k} / (2k·(2k)!) // x > 0 only // Body: takes x by value (assumes already moved) static Float cosIntegral_core(Float x, int eff_x, int precision) { int wp = precision + 20; x.truncateToApprox(wp); x.setEffectiveBits(Float::precisionToBits(wp)); // avoid exact/exact poison of exact inputs Float euler_gamma = Float::euler(wp); Float ln_x = log(x, wp); Float sum = euler_gamma + ln_x; sum.truncateToApprox(wp); Float x2 = x * x; x2.truncateToApprox(wp); Float neg_x2 = -x2; // term tracks (-1)^k · x^{2k} / (2k)! // term_{k} = term_{k-1} · (-x²) / ((2k-1)·2k) // contribution = term / (2k) Float term(1); // k=0 initial value Float contribution(0); for (int k = 1; k < 10 * wp; k++) { FloatOps::mul(term, neg_x2, term); FloatOps::div(term, Float(static_cast(2 * k - 1) * (2 * k)), term); term.truncateToApprox(wp); FloatOps::div(term, Float(2 * k), contribution); FloatOps::add(sum, contribution, sum); sum.truncateToApprox(wp); if (k >= 3 && contribution.isZero()) break; if (k >= 3) { auto c_bits = contribution.exponent() + static_cast(contribution.mantissa().bitLength()); auto s_bits = sum.exponent() + static_cast(sum.mantissa().bitLength()); if (s_bits - c_bits > Float::precisionToBits(wp) + 5) break; } } finalizeResult(sum, eff_x, precision); return sum; } Float cosIntegral(const Float& x, int precision) { if (x.isNaN()) return Float::nan(); if (!x.isPositive()) { if (x.isZero()) return Float::negativeInfinity(); return Float::nan(); } return cosIntegral_core(Float(x), x.effectiveBits(), precision); } Float cosIntegral(Float&& x, int precision) { if (x.isNaN()) return Float::nan(); if (!x.isPositive()) { if (x.isZero()) return Float::negativeInfinity(); return Float::nan(); } int eff = x.effectiveBits(); return cosIntegral_core(std::move(x), eff, precision); } //============================================================================= // Random number generation //============================================================================= // Generate a uniform random number in [0, 1) at precision-bit precision Float randomFloat(int precision) { static thread_local std::mt19937_64 rng(std::random_device{}()); // Generate a precision-bit random mantissa int num_words = (precision + 63) / 64; std::vector words(num_words); for (int i = 0; i < num_words; i++) { words[i] = rng(); } // Mask off the extra bits int extra_bits = num_words * 64 - precision; if (extra_bits > 0 && num_words > 0) { words[num_words - 1] &= (uint64_t(-1) >> extra_bits); } // Convert to Int and multiply by 2^{-precision} → a value in [0, 1) Int mantissa = Int::fromRawWords( std::span(words.data(), words.size()), +1); if (mantissa.isZero()) return Float(0); // Float(mantissa, exponent): value = mantissa * 2^exponent // mantissa is an integer of at most precision bits, so // exponent = -precision yields [0, 1) Float result(mantissa, -static_cast(precision)); result.setResultPrecision(precision); return result; } // Uniform random number in [lo, hi) Float randomFloat(const Float& lo, const Float& hi, int precision) { Float r = randomFloat(precision); Float result = lo + r * (hi - lo); result.setResultPrecision(precision); return result; } Float randomFloat(Float&& lo, Float&& hi, int precision) { Float r = randomFloat(precision); Float range = std::move(hi) - lo; Float result = std::move(lo) + r * std::move(range); result.setResultPrecision(precision); return result; } //============================================================================= // normalRandom — normally distributed random number N(0,1), Box-Muller method //============================================================================= Float normalRandom(int precision) { // Box-Muller method: from u1, u2 ∈ (0, 1) // z = sqrt(-2·ln(u1)) · cos(2π·u2) int wp = precision + 10; // Generate u1 in (0, 1) (avoid 0: log(0) = -∞) Float u1; do { u1 = randomFloat(wp); } while (u1.isZero()); Float u2 = randomFloat(wp); // r = sqrt(-2 * ln(u1)) Float neg2ln = Float(-2) * log(u1, wp); Float r = sqrt(std::move(neg2ln), wp); // theta = 2π * u2 Float theta = ldexp(Float::pi(wp), 1) * u2; Float result = r * cos(theta, wp); result.setResultPrecision(precision); return result; } //============================================================================= // exponentialRandom — exponentially distributed random number Exp(1), inverse-transform method //============================================================================= Float exponentialRandom(int precision) { // Inverse-transform method: -ln(u), u ∈ (0, 1) int wp = precision + 10; Float u; do { u = randomFloat(wp); } while (u.isZero()); Float result = -log(u, wp); result.setResultPrecision(precision); return result; } //============================================================================= // canRound — rounding direction decision (compatible with MPFR mpfr_can_round) //============================================================================= bool canRound(const Float& x, int err_bits, RoundingMode rnd1, RoundingMode rnd2, int target_prec) { // Special values are always roundable if (x.isZero() || x.isNaN() || x.isInfinity()) return true; // Compute the target bit count int target_bits = Float::precisionToBits(target_prec); // Number of guard bits = accurate bit count - target bit count int guard_bits = err_bits - target_bits; if (guard_bits <= 0) return false; // insufficient precision // Logic equivalent to the internal canRoundCorrectly int bit_length = static_cast(x.mantissa().bitLength()); if (bit_length <= target_bits) return true; // no rounding needed // Number of guard bits usable for the decision (margin=2 accounting for computation error) constexpr int MARGIN = 2; int usable = guard_bits - MARGIN; if (usable < 1) return false; // Scan the guard bit region // shift = bit_length - target_bits: the number of bits below the rounding position int shift = bit_length - target_bits; const uint64_t* data = x.mantissa().data(); // Read the usable guard bits // Position: bit (shift - 1) is the most significant guard bit int hi_pos = shift - 1; int lo_pos = shift - usable; if (lo_pos < 0) lo_pos = 0; bool all_zero = true; bool all_one = true; size_t lo_word = static_cast(lo_pos) / 64; size_t hi_word = static_cast(hi_pos) / 64; for (size_t w = lo_word; w <= hi_word; ++w) { uint64_t word = data[w]; unsigned lo_bit = (w == lo_word) ? static_cast(lo_pos % 64) : 0; unsigned hi_bit = (w == hi_word) ? static_cast(hi_pos % 64) : 63; unsigned width = hi_bit - lo_bit + 1; uint64_t mask; if (width >= 64) { mask = ~uint64_t(0); } else { mask = ((uint64_t(1) << width) - 1) << lo_bit; } uint64_t bits = word & mask; if (bits != 0) all_zero = false; if (bits != mask) all_one = false; if (!all_zero && !all_one) return true; } // All 0 or all 1 → too close to the rounding boundary, direction unknown return false; } //============================================================================= // Float binary serialization //============================================================================= std::vector exportBinary(const Float& value) { // tag: 0x00=zero, 0x01=normal, 0x02=infinity, 0x03=nan if (value.isNaN()) { return {0x03}; } if (value.isInfinity()) { uint8_t sign = value.isNegative() ? 0xFF : 0x00; return {0x02, sign}; } if (value.isZero()) { return {0x00}; } // Normal value // [tag:1][sign:1][exponent:8 LE][eff:4 LE][req:4 LE][mantissa_binary...] uint8_t sign = value.isNegative() ? 0xFF : 0x00; int64_t exp = value.exponent(); int eff = value.effectiveBits(); int req = value.requestedBits(); // Serialize the mantissa auto mant_bin = IntIOUtils::exportBinary(value.mantissa()); std::vector result; result.reserve(1 + 1 + 8 + 4 + 4 + mant_bin.size()); result.push_back(0x01); // tag result.push_back(sign); // exponent LE for (int i = 0; i < 8; ++i) result.push_back(static_cast((static_cast(exp) >> (i * 8)) & 0xFF)); // effective_bits LE for (int i = 0; i < 4; ++i) result.push_back(static_cast((static_cast(eff) >> (i * 8)) & 0xFF)); // requested_bits LE for (int i = 0; i < 4; ++i) result.push_back(static_cast((static_cast(req) >> (i * 8)) & 0xFF)); // mantissa binary result.insert(result.end(), mant_bin.begin(), mant_bin.end()); return result; } Float importBinaryFloat(std::span data) { if (data.empty()) { throw std::invalid_argument("importBinaryFloat: empty data"); } uint8_t tag = data[0]; if (tag == 0x00) return Float::zero(); if (tag == 0x03) return Float::nan(); if (tag == 0x02) { if (data.size() < 2) throw std::invalid_argument("importBinaryFloat: truncated infinity"); return data[1] == 0xFF ? Float::negativeInfinity() : Float::positiveInfinity(); } if (tag != 0x01) throw std::invalid_argument("importBinaryFloat: unknown tag"); // Normal: [tag:1][sign:1][exp:8][eff:4][req:4][mantissa...] if (data.size() < 18) throw std::invalid_argument("importBinaryFloat: truncated normal value"); bool neg = (data[1] == 0xFF); int64_t exp = 0; for (int i = 0; i < 8; ++i) exp |= static_cast(data[2 + i]) << (i * 8); int eff = 0; for (int i = 0; i < 4; ++i) eff |= static_cast(data[10 + i]) << (i * 8); int req = 0; for (int i = 0; i < 4; ++i) req |= static_cast(data[14 + i]) << (i * 8); // mantissa auto mant_span = data.subspan(18); Int mantissa = IntIOUtils::importBinary(mant_span); Float result(std::move(mantissa), exp, neg); result.effective_bits_ = eff; result.requested_bits_ = req; return result; } } // namespace sangi