Uni20
Asynchronous tensor and runtime library.
Loading...
Searching...
No Matches
complex_log_gamma.hpp
Go to the documentation of this file.
1#pragma once
2
4#include "math.hpp"
5#if UNI20_HAS_MPREAL
7#endif
8#include <algorithm>
9#include <cstddef>
10#include <cstdint>
11#include <limits>
12#include <stdexcept>
13#include <vector>
14
15namespace uni20
16{
19{
20 std::size_t max_shifts = 4096;
21 std::size_t max_terms = 512;
22};
23
30template <class R> struct log_gamma_result
31{
35 std::size_t terms;
36 std::size_t shifts;
37};
38
39namespace detail
40{
41template <class R>
42concept LogGammaReal = native_math::NativeReal<R>
43#if UNI20_HAS_MPREAL
44 || std::same_as<R, mpreal>
45#endif
46 ;
47} // namespace detail
48
58template <detail::LogGammaReal R> class log_gamma_context {
59 public:
61
63 explicit log_gamma_context(log_gamma_options options = {})
64 requires(!has_runtime_precision_v<R>)
65 : log_gamma_context(native_precision<R>(), options)
66 {}
67
70 : precision_(p), work_(this->working_precision(p)), options_(options)
71 {
72 if (options.max_terms < 2) throw std::invalid_argument("log-Gamma needs at least two coefficient slots");
73 std::size_t count = 16;
74#if UNI20_HAS_MPREAL
75 if constexpr (std::same_as<R, mpreal>)
76 {
77 auto const wanted = static_cast<std::uint64_t>(work_.bit_count() / 6 + 8);
78 if (wanted > options.max_terms || wanted > (std::numeric_limits<std::size_t>::max() - 1) / 2)
79 throw std::invalid_argument("log-Gamma coefficient budget is too small for requested precision");
80 count = static_cast<std::size_t>(wanted);
81 coefficients_.reserve(count);
82 detail::exact_bernoulli_sequence sequence(2 * count);
83 for (std::size_t n = 0; n <= 2 * count; ++n)
84 {
85 auto b = sequence.next();
86 if (n >= 2 && n % 2 == 0)
87 coefficients_.push_back(this->number(b / (exact_rational{n} * exact_rational{n - 1})));
88 }
89 }
90 else
91#endif
92 {
93 count = std::min(count, options.max_terms);
94 auto table = detail::native_stirling_coefficients<R>();
95 coefficients_.assign(table.begin(), table.begin() + count);
96 }
97 log_coefficient_ = math::log(math::abs(coefficients_.back()));
98 log_target_ = math::log(numeric_limits<R>::epsilon(work_) / this->number(32));
99 small_angle_ = math::sqrt(numeric_limits<R>::epsilon(work_)) / this->number(2);
100 half_log_two_pi_ = math::log(this->number(2) * (pi<R> * this->number(1))) / this->number(2);
101 }
102
104 Precision precision() const { return precision_; }
105
108 {
109 complex_type z = this->argument(input);
110 accumulator sum(this->number(0));
111 auto const one = this->number(1), half = one / this->number(2);
112 std::size_t shifts = 0;
113 R log_tail = this->log_remainder(z);
114 while (log_tail > log_target_)
115 {
116 this->check_shift(shifts, z);
117 sum.add(-this->logarithm(z));
118 z += one;
119 ++shifts;
120 log_tail = this->log_remainder(z);
121 }
122 auto const log_z = this->logarithm(z);
123 // Fuse the cancellation in the real component before the product rounds.
124 // Near the real axis, x*atan(y/x) rounds to y at working precision;
125 // evaluating the ratio first can underflow and lose that contribution.
126 R const leading_real = math::fma(z.real(), log_z.real() - one, -z.imag() * log_z.imag());
127 R const leading_imag = this->near_real(z) ? z.imag() * log_z.real()
128 : math::fma(z.imag(), log_z.real() - one, z.real() * log_z.imag());
129 sum.add(complex_type(leading_real, leading_imag));
130 sum.add(-half * log_z);
131 sum.add(complex_type(half_log_two_pi_));
132 complex_type inverse = this->reciprocal(z), power = inverse;
133 complex_type const square = inverse * inverse;
134 for (std::size_t k = 0; k + 1 < coefficients_.size(); ++k)
135 {
136 sum.add(coefficients_[k] * power);
137 power *= square;
138 }
139 return this->finish(sum, math::exp(log_tail), shifts);
140 }
141
146 log_gamma_result<R> log_gamma_ratio_right(complex_type const& input, R const& shift) const
147 {
148 complex_type z = this->argument(input);
149 this->check_input_precision(precision_of(shift));
150 R h = this->number(shift);
151 R const zero = this->number(0), one = this->number(1), half = one / this->number(2);
152 if (!uni20::isfinite(h)) throw std::invalid_argument("log-Gamma ratio requires finite shift");
153 complex_type upper(z.real() + h, z.imag());
154 if (!uni20::isfinite(upper)) throw std::overflow_error("log-Gamma shifted argument exceeds scalar range");
155 if (!(upper.real() > zero)) throw std::invalid_argument("log-Gamma ratio requires Re(z+h)>0");
156 if (h == zero) return {complex_type(this->output(zero)), this->output(zero), this->output(zero), 0, 0};
157
158 // Recur from the smaller real part. The larger argument can already
159 // satisfy the asymptotic bound while addition of one rounds away there.
160 // Retaining h separately accounts for that common shift to working precision.
161 bool const reverse = h < zero;
162 if (reverse)
163 {
164 std::swap(z, upper);
165 h = -h;
166 }
167
168 accumulator sum(zero);
169 R logarithm_tail = zero;
170 std::size_t shifts = 0;
171 // Each remainder <= target/2, so their sum <= target. The factor two
172 // is handled in logs to avoid underflow changing the admission decision.
173 R const two = this->number(2), eight = this->number(8);
174 R const threshold = log_target_ - math::log(two);
175 R tail0 = this->log_remainder(z), tail1 = zero;
176 while (true)
177 {
178 // A failing first bound already requires another shift. Evaluate the
179 // second bound only when it can change that decision.
180 if (tail0 <= threshold)
181 {
182 tail1 = this->log_remainder(upper);
183 if (tail1 <= threshold) break;
184 }
185 this->check_shift(shifts, z);
186 sum.add(-this->log_increment(z, h, two, eight));
187 z += one;
188 upper = complex_type(z.real() + h, z.imag());
189 ++shifts;
190 tail0 = this->log_remainder(z);
191 }
192 auto const delta = this->log_difference(z, h);
193 bool const near_real = this->near_real(z);
194 auto add_leading = [&](complex_type const& term) { sum.add(near_real ? complex_type(term.real(), zero) : term); };
195 if (delta.small)
196 {
197 add_leading(h * this->logarithm(z));
198 add_leading((h - half) * delta.log1p);
199 add_leading(h * delta.relative_remainder);
200 logarithm_tail += math::abs(h - half) * delta.log_error + math::abs(h) * delta.relative_error;
201 }
202 else
203 {
204 add_leading(h * (this->logarithm(upper) - one));
205 add_leading((z - half) * delta.log1p);
206 }
207 if (near_real)
208 {
209 // First-order imaginary part of the paired leading term:
210 // y*[log(1+h/x) + h/(2*x*(x+h))]. The omitted relative correction
211 // is O((y/x)^2), below working roundoff in this branch. Preserve y*h/x
212 // even when either y/x or h/x cannot be represented on its own.
213 R const product = this->product_quotient(z.imag(), h, z.real());
214 R const phase = delta.small ? product * (one + delta.relative_remainder.real()) : z.imag() * delta.log1p.real();
215 sum.add(complex_type(zero, phase + (half * product) / upper.real()));
216 }
217 complex_type const inv0 = this->reciprocal(z), inv1 = this->reciprocal(upper);
218 complex_type const square0 = inv0 * inv0, square1 = inv1 * inv1;
219 complex_type const delta_inv = (-h * inv0) * inv1;
220 complex_type const delta_square = delta_inv * (inv0 + inv1);
221 complex_type power = inv0, delta_power = delta_inv;
222 for (std::size_t k = 0; k + 1 < coefficients_.size(); ++k)
223 {
224 sum.add(coefficients_[k] * delta_power);
225 delta_power = delta_power * square1 + power * delta_square;
226 power *= square0;
227 }
228 auto result = this->finish(sum, math::exp(tail0) + math::exp(tail1) + logarithm_tail, shifts);
229 if (reverse) result.value = -result.value;
230 return result;
231 }
232
233 private:
234 struct accumulator
235 {
236 complex_type sum, correction;
237 R scaled_mass, epsilon;
238 explicit accumulator(R zero)
239 : sum(zero), correction(zero), scaled_mass(zero), epsilon(numeric_limits<R>::epsilon(zero))
240 {}
241 static R error(R const& a, R const& b, R const& next)
242 {
243 return math::abs(a) >= math::abs(b) ? (a - next) + b : (b - next) + a;
244 }
245 void add(complex_type const& term)
246 {
247 complex_type const next = sum + term;
248 correction +=
249 complex_type(error(sum.real(), term.real(), next.real()), error(sum.imag(), term.imag(), next.imag()));
250 sum = next;
251 // Scale before taking a magnitude or summing. A finite complex
252 // value can have a magnitude outside R's range, yet finite error.
253 scaled_mass += math::hypot(epsilon * term.real(), epsilon * term.imag());
254 }
255 };
256
257 struct logarithm_difference
258 {
259 complex_type log1p, relative_remainder;
260 R log_error, relative_error;
261 bool small;
262 };
263
264 static Precision working_precision(Precision p)
265 {
266 if (p.is_exact()) throw std::invalid_argument("log-Gamma requires explicit finite precision");
267 require_precision<R>(p);
268 if constexpr (has_runtime_precision_v<R>)
269 {
270 if (p.bit_count() > std::numeric_limits<Precision::bit_count_type>::max() - 64)
271 throw std::overflow_error("log-Gamma guard precision exceeds supported range");
272 return Precision::bits(p.bit_count() + 64);
273 }
274 return p;
275 }
276 template <class T> R number(T const& x) const { return scalar_at_precision<R>(x, work_); }
277 template <class T> T output(T const& x) const { return scalar_at_precision<T>(x, precision_); }
278 void check_input_precision(Precision p) const
279 {
280 if (!p.is_exact() && p != precision_) throw std::invalid_argument("log-Gamma input precision mismatch");
281 }
282 complex_type argument(complex_type const& z) const
283 {
284 this->check_input_precision(precision_of(z));
285 if (!uni20::isfinite(z) || !(z.real() > 0)) throw std::invalid_argument("log-Gamma requires finite Re(z)>0");
286 return scalar_at_precision<complex_type>(z, work_);
287 }
288 // Scaling avoids overflow of abs(z), and avoids squaring tiny components.
289 R scale(complex_type const& z) const { return std::max(math::abs(z.real()), math::abs(z.imag())); }
290 bool near_real(complex_type const& z) const { return math::abs(z.imag()) <= z.real() * small_angle_; }
291 // Only the final product/quotient is rounded into the scalar exponent range.
292 R product_quotient(R const& a, R const& b, R const& c) const
293 {
294 auto const aa = math::frexp(a), bb = math::frexp(b), cc = math::frexp(c);
295 auto const exponent = static_cast<__int128_t>(aa.exponent) + bb.exponent - cc.exponent;
296 // MPFR permits exponent ranges whose intermediate sum exceeds int64.
297 // Beyond these limits the final scaling necessarily overflows/underflows.
298 auto const limited = std::clamp(exponent, static_cast<__int128_t>(std::numeric_limits<std::int64_t>::min()),
299 static_cast<__int128_t>(std::numeric_limits<std::int64_t>::max()));
300 return math::ldexp((aa.fraction * bb.fraction) / cc.fraction, static_cast<std::int64_t>(limited));
301 }
302 complex_type logarithm(complex_type const& z) const
303 {
304 R const scale = this->scale(z), re = z.real() / scale, im = z.imag() / scale;
305 return {math::log(scale) + math::log(math::hypot(re, im)), math::atan2(z.imag(), z.real())};
306 }
307 complex_type reciprocal(complex_type const& z) const
308 {
309 R const scale = this->scale(z), re = z.real() / scale, im = z.imag() / scale;
310 R const norm = re * re + im * im;
311 return {(re / norm) / scale, (-im / norm) / scale};
312 }
313 R log_remainder(complex_type const& z) const
314 {
315 R const scale = this->scale(z), re = z.real() / scale, im = z.imag() / scale;
316 R const norm = math::hypot(re, im), n = this->number(coefficients_.size());
317 R const sec2 = this->number(2) / (this->number(1) + re / norm);
318 // DLMF 5.11(ii): first omitted term times sec^(2n)(arg(z)/2).
319 return log_coefficient_ + n * math::log(sec2) -
320 (this->number(2) * n - this->number(1)) * (math::log(scale) + math::log(norm));
321 }
322 // Recurrence needs only log(1+h/z), whereas the leading Stirling term
323 // also needs log(1+h/z)/(h/z)-1 without cancellation. Avoid evaluating
324 // that separate power series at every recurrence step. Here h>0; when
325 // h/scale<=1/8 the normalized denominators are in [1, 2+1/8], so the
326 // log1p/atan2 arguments cannot overflow or hide a representable quotient.
327 complex_type log_increment(complex_type const& z, R const& h, R const& two, R const& eight) const
328 {
329 R const scale = this->scale(z);
330 if (h > scale / eight) return this->log_difference(z, h).log1p;
331 R const re = z.real() / scale, im = z.imag() / scale, delta = h / scale;
332 R const norm = re * re + im * im;
333 R const real = math::log1p(delta * (two * re + delta) / norm) / two;
334 R const imag = math::atan2(-delta * im, norm + delta * re);
335 return {real, imag};
336 }
337 logarithm_difference log_difference(complex_type const& z, R const& h) const
338 {
339 R const zero = this->number(0), one = this->number(1), scale = this->scale(z);
340 if (math::abs(h) <= scale / this->number(8))
341 {
342 // R=log(1+w)/w-1 is evaluated directly. In particular w==0
343 // from underflow must not turn z*log(1+w)-h into -h.
344 complex_type const w = complex_type(h / scale) / complex_type(z.real() / scale, z.imag() / scale);
345 complex_type power = -w;
346 accumulator remainder(zero);
347 R const target = numeric_limits<R>::epsilon(work_) / this->number(32);
348 R denominator = this->number(2);
349 for (std::size_t k = 0; k < options_.max_terms; ++k)
350 {
351 complex_type const term = power / denominator;
352 remainder.add(term);
353 power *= -w;
354 // The next term and all successors are <= |power|/(k+3)/(1-1/8).
355 if (math::abs(power) <= target)
356 {
357 complex_type const r = remainder.sum + remainder.correction;
358 R const error = (math::abs(power) / (denominator + one)) * (this->number(8) / this->number(7));
359 return {w * (one + r), r, math::abs(w) * error, error, true};
360 }
361 denominator += one;
362 }
363 throw std::runtime_error("log-Gamma ratio exhausted its logarithm term budget");
364 }
365 complex_type const upper(z.real() + h, z.imag());
366 R const upper_scale = this->scale(upper);
367 auto const lower_binary = math::frexp(scale), upper_binary = math::frexp(upper_scale);
368 R const lower_magnitude = lower_binary.fraction * math::hypot(z.real() / scale, z.imag() / scale);
369 R const upper_magnitude =
370 upper_binary.fraction * math::hypot(upper.real() / upper_scale, upper.imag() / upper_scale);
371 // Cancel the common binary scale before taking logarithms. Subtracting
372 // log magnitudes separately loses digits when both arguments are tiny.
373 R const real = math::log(upper_magnitude / lower_magnitude) +
374 this->number(upper_binary.exponent - lower_binary.exponent) * math::log(this->number(2));
375 R const imag = math::atan2(upper.imag(), upper.real()) - math::atan2(z.imag(), z.real());
376 return {complex_type(real, imag), complex_type(zero), zero, zero, false};
377 }
378 void check_shift(std::size_t shifts, complex_type const& z) const
379 {
380 if (shifts == options_.max_shifts) throw std::runtime_error("log-Gamma exhausted its recurrence budget");
381 R const next = z.real() + this->number(1);
382 if (!uni20::isfinite(next) || !(next > z.real()))
383 throw std::overflow_error("log-Gamma recurrence cannot advance at working precision");
384 }
385 log_gamma_result<R> finish(accumulator const& sum, R const& tail, std::size_t shifts) const
386 {
387 auto const value = this->output(sum.sum + sum.correction);
388 R const roundoff = this->number(64) * (numeric_limits<R>::epsilon(work_) + sum.scaled_mass);
389 R const output_epsilon = numeric_limits<R>::epsilon(precision_);
390 R const rounded_error = this->output(roundoff) + output_epsilon +
391 math::hypot(output_epsilon * value.real(), output_epsilon * value.imag());
392 if (!uni20::isfinite(value) || !uni20::isfinite(rounded_error) || !uni20::isfinite(tail))
393 throw std::overflow_error("log-Gamma value or diagnostic exceeds scalar range");
394 return {value, this->output(tail), rounded_error, coefficients_.size() - 1, shifts};
395 }
396
397 Precision precision_, work_;
398 log_gamma_options options_;
399 std::vector<R> coefficients_;
400 R log_coefficient_, log_target_, small_angle_, half_log_two_pi_;
401};
402} // namespace uni20
AsyncTask square(ReadBuffer< int > in, WriteBuffer< int > out)
Definition async_tbb_reduction_example.cpp:39
Exact arithmetic or an explicit binary working precision.
Definition core/precision.hpp:13
static constexpr Precision bits(bit_count_type count)
Select a positive binary significand size.
Definition core/precision.hpp:21
constexpr bit_count_type bit_count() const
Number of binary significand bits.
Definition core/precision.hpp:43
Owning arbitrary-size rational with exact, eager arithmetic.
Definition exact_rational.hpp:161
Immutable reusable right-half-plane log-Gamma and real-shift log-ratio context.
Definition complex_log_gamma.hpp:58
log_gamma_result< R > log_gamma_ratio_right(complex_type const &input, R const &shift) const
Evaluate log Gamma(z+h)-log Gamma(z), h real and both real parts positive.
Definition complex_log_gamma.hpp:146
log_gamma_context(log_gamma_options options={})
Construct at the fixed scalar's native precision.
Definition complex_log_gamma.hpp:63
Precision precision() const
Requested output precision.
Definition complex_log_gamma.hpp:104
complex< R > complex_type
Definition complex_log_gamma.hpp:60
log_gamma_context(Precision p, log_gamma_options options={})
Prepare coefficients at explicit finite precision p.
Definition complex_log_gamma.hpp:69
log_gamma_result< R > log_gamma_right(complex_type const &input) const
Evaluate analytic log Gamma(z) on finite Re(z)>0.
Definition complex_log_gamma.hpp:107
locale_t value
Definition data_table.cpp:49
typename detail::complex_type< Real >::type complex
Project-level complex scalar spelling.
Definition types.hpp:66
constexpr bool isfinite(I const &x) noexcept
Returns whether an integer scalar is finite.
Definition math.hpp:121
Scalar math helper utilities.
T product(std::vector< T > const &values)
Definition mpreal_exact_example.cpp:13
Definition cli.cpp:6
streaming_table table(std::string title, stream destination)
Create a schema-first streaming display table.
Definition display.cpp:800
std::optional< DenseMatrix< W > > correction(Backend &backend, LuFactorization< F > const &factor, DenseMatrix< R > const &rhs, LuRefinementPrecision< F, W, R > p, std::vector< bool > const &active, std::int64_t target_exponent, SolveInfo &info)
Definition lu_refine.hpp:93
constexpr detail::scalar_math::exp_fn exp
Natural exponential with scalar-specific precision handling.
Definition math.hpp:465
constexpr detail::scalar_math::atan2_fn atan2
Quadrant-aware inverse tangent of y/x in radians.
Definition math.hpp:505
constexpr detail::scalar_math::log1p_fn log1p
Natural logarithm of one plus the argument, retaining small increments.
Definition math.hpp:483
constexpr detail::scalar_math::ldexp_fn ldexp
Multiply by a power of two using the scalar's exponent interface.
Definition math.hpp:511
constexpr detail::scalar_math::sqrt_fn sqrt
Square root; exact inputs requiring approximation need an explicit finite precision.
Definition math.hpp:461
constexpr detail::scalar_math::abs_fn abs
Absolute value or complex magnitude, preserving the selected scalar implementation.
Definition math.hpp:459
constexpr detail::scalar_math::fma_fn fma
Fused multiplication and addition with one final rounding.
Definition math.hpp:555
constexpr detail::scalar_math::real_fn real
Read the real component by value; real and integral scalars retain their type.
Definition math.hpp:671
constexpr detail::scalar_math::frexp_fn frexp
Binary fraction and exponent as an owning result.
Definition math.hpp:535
constexpr detail::scalar_math::imag_fn imag
Read the imaginary component by value; real zero retains the input's type and precision.
Definition math.hpp:673
constexpr detail::scalar_math::hypot_fn hypot
Euclidean length with scalar-specific overflow and underflow handling.
Definition math.hpp:507
constexpr detail::scalar_math::log_fn log
Natural logarithm with scalar-specific precision handling.
Definition math.hpp:467
Definition data_table.cpp:8
Definition async/async.hpp:26
void sum(BackendSelector selector, async::Async< OutputTensor > &output, async::Async< InputTensor > const &input)
Schedule a full sum into an explicit async scalar tensor.
Definition linalg/async/reductions.hpp:309
constexpr Precision precision_of(T const &value)
Read scalar precision without changing any scalar or provider state.
Definition runtime_precision.hpp:53
mpreal norm(mpcomplex const &z)
Definition mpcomplex.hpp:494
int add(int a, int b)
Definition dummy.cpp:6
mpreal remainder(mpreal const &a, mpreal const &b)
Nearest-even-quotient remainder; exact operands retain exact rational results.
Definition mpreal_functions.hpp:534
constexpr Precision native_precision()
Concrete precision of a fixed scalar family, including complex components.
Definition runtime_precision.hpp:41
Work limits for recurrence and Stirling/logarithm series evaluation.
Definition complex_log_gamma.hpp:19
std::size_t max_shifts
Definition complex_log_gamma.hpp:20
std::size_t max_terms
Definition complex_log_gamma.hpp:21
Analytic log-Gamma value, truncation bound and heuristic roundoff indicator.
Definition complex_log_gamma.hpp:31
R roundoff_indicator
Definition complex_log_gamma.hpp:34
std::size_t terms
Definition complex_log_gamma.hpp:35
R truncation_bound
Definition complex_log_gamma.hpp:33
std::size_t shifts
Definition complex_log_gamma.hpp:36
complex< R > value
Definition complex_log_gamma.hpp:32
Project-level numeric limits customization point.
Definition numeric_limits.hpp:16