Skip to content

Commit 2be40ba

Browse files
Sum the incomplete gamma function's lower series as DLMF 8.7.3 in the left half-plane (#2088)
Gamma(a) - gamma(a, z) summed gamma(a, z) as z^a e^(-z) sum_k z^k/(a)_(k + 1) (DLMF 8.7.1) everywhere. On the negative axis its terms alternate, are e^|z| in size and sum to e^(-|z|), so twice |z|/ln 10 guard digits were carried, and at that precision Gamma(a) was computed afresh: Gamma(1/3, -143) took 9.6 s, Gamma(1/2, -100 + 5i) 5.6 s, Gamma(-5/2, -60) 2.4 s. In the left half-plane it is now z^a sum_k (-z)^k/(k! (a + k)) (DLMF 8.7.3), as E_1 already was, whose terms are of one sign on that axis and which loses (|z| + Re z)/ln 10 digits: 6 ms, 9 ms and 6 ms. The large guard had been hiding two defects on the cut, where each part of the value should be accurate to its own size: - z^a was e^(a ln|z| + i a pi) with a pi rounded, so cos(pi/2) was 10^(-W) where it is 0. Times e^143, that was the real part of Gamma(1/2, -143), which is sqrt(pi). The angle is reduced exactly before pi multiplies it, and is exactly 0 at a half-whole a. - Past (P + 5) ln 10 the continued fraction, whose coefficients are real there, gives e^(i pi a) times a real function, the mean of the two sides. It falls short of the value from above by -i e^(i pi a) sin(pi a) Gamma(a). That is below the precision of the modulus, but it is the whole of the real part at a half-whole a and of the imaginary part at a = -n. On master Gamma(1/2, -300)'s real part was 3e13 times sqrt(pi), Gamma(11/2, -300)'s 1.6e25 times Gamma(11/2), and Gamma(-3, -300)'s imaginary part 9e4 times pi/6. Checked part by part against mpmath at 300-400 digits on 320 points: 16 orders, half-whole, whole, complex and odd-denominator ones among them, at 20 points of the left half-plane including both sides of the threshold. Every part agrees to 1e-97 of its own size. On master, 18 of the 80 cases past the threshold had a part wrong, by up to 1e113 times its size. The grid took 48.4 s on master and 3.4 s here. Part of #2075. Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 6821140 commit 2be40ba

2 files changed

Lines changed: 91 additions & 15 deletions

File tree

‎Sources/AngouriMath/Core/Entity/Continuous/Number/IncompleteGamma.cs‎

Lines changed: 79 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,11 @@ partial record Number
2828
/// algorithm. Near the negative real axis at a moderate <c>|z|</c> it stops short of the value.
2929
/// </para>
3030
/// <para>
31-
/// Short of that it is <c>Gamma(a) - gamma(a, z)</c>, the lower one summed as
32-
/// <c>z^a e^(-z) sum_k z^k / (a (a + 1) ... (a + k))</c> (DLMF 8.7.1). The terms are about <c>e^|z|</c> and the
33-
/// value can be as small as <c>e^(-|z|)</c>, so twice <c>|z|/ln 10</c> digits are carried as guard digits,
34-
/// and the sum is taken again with more where the difference shows it cancelled more.
31+
/// Short of that it is <c>Gamma(a) - gamma(a, z)</c>, the lower one summed in the right half-plane as
32+
/// <c>z^a e^(-z) sum_k z^k / (a (a + 1) ... (a + k))</c> (DLMF 8.7.1), whose terms there are about <c>e^|z|</c>
33+
/// and of one sign on the positive axis, with twice <c>|z|/ln 10</c> guard digits; and in the left half-plane as
34+
/// <c>z^a sum_k (-z)^k / (k! (a + k))</c> (DLMF 8.7.3), whose terms are of one sign on the negative axis, with
35+
/// <c>(|z| + Re z)/ln 10</c>. Each is taken again with more where the difference shows it cancelled more.
3536
/// </para>
3637
/// <para>
3738
/// At a whole <c>a = -n &lt;= 0</c>, where <c>Gamma(a)</c> has a pole, it is
@@ -76,6 +77,23 @@ public static Complex IncompleteGamma(Complex a, Complex z)
7677
var (re, im) = useTheFraction
7778
? ByContinuedFraction((ar, ai), (zr, zi), Working(context, 10))
7879
: BySeries((ar, ai), (zr, zi), modulus, context, turn);
80+
// On the cut, for a real a, the fraction's coefficients are real and it converges to e^(i pi a) times a real
81+
// function, the mean of the value from above and its conjugate from below, short of the value from above by
82+
// -i e^(i pi a) sin(pi a) Gamma(a). That is below the precision of a value e^|z| in size, but not of a part it
83+
// is the whole of: Gamma(a) at a half-whole a, the real part of Gamma(1/2, -300), which is sqrt(pi), and
84+
// -i (-1)^n pi/n! at a = -n, the imaginary part of Gamma(-3, -300), pi/6. Without it, both are 0.
85+
if (useTheFraction && turn is null && ai.IsZero && zi.IsZero && zr.Sign < 0)
86+
{
87+
var work = Working(context, 10);
88+
if (!ar.IsInteger() && ar.Multiply(EDecimal.FromInt32(2)).IsInteger())
89+
re = re.Add(GammaOf((ar, ai), work).Re, work);
90+
else if (ar.IsInteger() && ar.Sign <= 0)
91+
{
92+
var n = ar.ToEInteger().Negate();
93+
var piOverFactorial = InternalAMExtensions.ConstantCache.Lookup(work).Pi.Divide(EDecimal.FromEInteger(Factorial(n.ToInt32Checked())), work);
94+
im = n.IsEven ? im.Subtract(piOverFactorial, work) : im.Add(piOverFactorial, work);
95+
}
96+
}
7997
// The continued fraction is used on the negative axis only past (P + 5) ln 10, where the value is e^|z| in
8098
// size and (1 - rho) Gamma(a) + rho times the principal value cancels nothing.
8199
if (turn is { } fractionsTurn && useTheFraction)
@@ -149,13 +167,21 @@ private static (EDecimal Re, EDecimal Im) Turned((EInteger Turns, EInteger Q) tu
149167
private static (EDecimal Re, EDecimal Im) BySeries((EDecimal Re, EDecimal Im) a, (EDecimal Re, EDecimal Im) z, double modulus, EContext context,
150168
(EInteger Turns, EInteger Q)? turn)
151169
{
152-
var guard = (int)Math.Ceiling(2 * modulus / Math.Log(10)) + 10;
170+
// In the left half-plane the lower function is summed as z^a sum_k (-z)^k/(k! (a + k)), as E_1 is, whose terms
171+
// of size e^|z| sum to e^(-Re z): it loses (|z| + Re z)/ln 10 digits, none on the negative axis, where z^a
172+
// e^(-z) sum_k z^k/(a)_(k + 1) has terms of alternating sign that sum to e^(-|z|) and lost twice |z|/ln 10.
173+
// Carrying those digits asked Gamma(a) at a precision of its own, where it is not remembered: Gamma(1/3, -143)
174+
// took 9.6 seconds, and Gamma(-5/2, -60) 2.4.
175+
var leftHalf = z.Re.Sign < 0;
176+
var guard = leftHalf
177+
? (int)Math.Ceiling(Math.Max(0, modulus + z.Re.ToDouble()) / Math.Log(10)) + 12
178+
: (int)Math.Ceiling(2 * modulus / Math.Log(10)) + 10;
153179
for (var attempt = 0; ; attempt++)
154180
{
155181
var work = Working(context, guard);
156182
var (value, scale) = a.Im.IsZero && a.Re.IsInteger() && a.Re.Sign <= 0
157183
? AtANonPositiveWholeOrder(a.Re.ToEInteger().Negate().ToInt32Checked(), z, work)
158-
: AsGammaLessTheLower(a, z, work, turn);
184+
: AsGammaLessTheLower(a, z, leftHalf, work, turn);
159185
// How many digits the value lost to the cancellation, against the largest thing that cancelled.
160186
var size = Magnitude(value);
161187
var lost = size.IsZero ? guard : DecimalOrder(scale) - DecimalOrder(size) + 1;
@@ -166,13 +192,16 @@ private static (EDecimal Re, EDecimal Im) BySeries((EDecimal Re, EDecimal Im) a,
166192
}
167193

168194
/// <summary>
169-
/// <c>Gamma(a) - z^a e^(-z) sum_k z^k / (a)_(k + 1)</c>, and the size of the larger of the two terms.
195+
/// <c>Gamma(a) - z^a e^(-z) sum_k z^k / (a)_(k + 1)</c>, or with <paramref name="leftHalf"/>
196+
/// <c>Gamma(a) - z^a sum_k (-z)^k / (k! (a + k))</c>, and the size of the larger of the two terms.
170197
/// </summary>
171-
private static ((EDecimal Re, EDecimal Im) Value, EDecimal Scale) AsGammaLessTheLower((EDecimal Re, EDecimal Im) a, (EDecimal Re, EDecimal Im) z, EContext work,
172-
(EInteger Turns, EInteger Q)? turn)
198+
private static ((EDecimal Re, EDecimal Im) Value, EDecimal Scale) AsGammaLessTheLower((EDecimal Re, EDecimal Im) a, (EDecimal Re, EDecimal Im) z, bool leftHalf,
199+
EContext work, (EInteger Turns, EInteger Q)? turn)
173200
{
174201
var epsilon = EDecimal.Create(EInteger.One, EInteger.FromInt32(-work.Precision.ToInt32Checked()));
175-
// term_k = z^k / (a (a + 1) ... (a + k)), starting at 1/a
202+
// term_k = z^k / (a (a + 1) ... (a + k)), or (-z)^k / (k! (a + k)), each starting at 1/a, and each term the
203+
// last times z/(a + k), or times -z (a + k - 1)/(k (a + k))
204+
var minusZ = (Re: z.Re.Negate(), Im: z.Im.Negate());
176205
(EDecimal Re, EDecimal Im) sum;
177206
EDecimal peak;
178207
if (a.Im.IsZero && z.Im.IsZero)
@@ -185,8 +214,11 @@ private static ((EDecimal Re, EDecimal Im) Value, EDecimal Scale) AsGammaLessThe
185214
var aPlusK = a.Re;
186215
for (var k = 1; ; k++)
187216
{
217+
var previous = aPlusK;
188218
aPlusK = aPlusK.Add(EDecimal.One, work);
189-
term = term.Multiply(z.Re, work).Divide(aPlusK, work);
219+
term = leftHalf
220+
? term.Multiply(minusZ.Re, work).Multiply(previous, work).Divide(aPlusK.Multiply(k, work), work)
221+
: term.Multiply(z.Re, work).Divide(aPlusK, work);
190222
real = real.Add(term, work);
191223
var size = term.Abs();
192224
if (size.CompareTo(peak) > 0)
@@ -204,8 +236,11 @@ private static ((EDecimal Re, EDecimal Im) Value, EDecimal Scale) AsGammaLessThe
204236
var aPlusK = a;
205237
for (var k = 1; ; k++)
206238
{
239+
var previous = aPlusK;
207240
aPlusK = (aPlusK.Re.Add(EDecimal.One, work), aPlusK.Im);
208-
term = Multiply(Multiply(term, z, work), Reciprocal(aPlusK, work), work);
241+
term = leftHalf
242+
? Multiply(Multiply(term, minusZ, work), Multiply(previous, Reciprocal((aPlusK.Re.Multiply(k, work), aPlusK.Im.Multiply(k, work)), work), work), work)
243+
: Multiply(Multiply(term, z, work), Reciprocal(aPlusK, work), work);
209244
sum = (sum.Re.Add(term.Re, work), sum.Im.Add(term.Im, work));
210245
var size = Magnitude(term);
211246
if (size.CompareTo(peak) > 0)
@@ -217,7 +252,7 @@ private static ((EDecimal Re, EDecimal Im) Value, EDecimal Scale) AsGammaLessThe
217252
var power = ComplexPower(z, a, work);
218253
if (turn is { } realRoot)
219254
power = Multiply(Turned(realRoot, work), power, work);
220-
var prefactor = Multiply(power, ComplexExp((z.Re.Negate(), z.Im.Negate()), work), work);
255+
var prefactor = leftHalf ? power : Multiply(power, ComplexExp(minusZ, work), work);
221256
var lower = Multiply(prefactor, sum, work);
222257
var gamma = GammaOf(a, work);
223258
var scale = EDecimal.Max(Magnitude(gamma), Magnitude(prefactor).Multiply(peak, work));
@@ -328,7 +363,37 @@ private static (EDecimal Re, EDecimal Im) ComplexLn((EDecimal Re, EDecimal Im) z
328363

329364
/// <summary><c>z^a = e^(a ln z)</c>, principal.</summary>
330365
private static (EDecimal Re, EDecimal Im) ComplexPower((EDecimal Re, EDecimal Im) z, (EDecimal Re, EDecimal Im) a, EContext work)
331-
=> ComplexExp(Multiply(a, ComplexLn(z, work), work), work);
366+
{
367+
// On the cut, for a real a, |z|^a (cos pi a + i sin pi a) with each part accurate to its own size. From pi/2
368+
// rounded to W digits, cos(pi/2) is 10^(-W) where it is 0, and times the lower function's e^143 that was the
369+
// real part of Gamma(1/2, -143), which is sqrt(pi).
370+
if (z.Im.IsZero && z.Re.Sign < 0 && a.Im.IsZero)
371+
{
372+
var modulus = z.Re.Negate().Log(work).Multiply(a.Re, work).Exponential(work);
373+
var (cos, sin) = CosAndSinOfPiTimes(a.Re, work);
374+
return (modulus.Multiply(cos, work), modulus.Multiply(sin, work));
375+
}
376+
return ComplexExp(Multiply(a, ComplexLn(z, work), work), work);
377+
}
378+
379+
/// <summary>
380+
/// <c>(cos pi t, sin pi t)</c>, each accurate to its own size: with <c>n</c> the whole number nearest <c>t</c> and
381+
/// <c>s = 1/2 - |t - n|</c>, both exact, they are <c>(-1)^n sin pi s</c> and <c>(-1)^n sgn(t - n) cos pi s</c>, and
382+
/// <c>cos pi t</c> is exactly 0 at a half-whole <c>t</c>.
383+
/// </summary>
384+
private static (EDecimal Cos, EDecimal Sin) CosAndSinOfPiTimes(EDecimal t, EContext work)
385+
{
386+
var n = t.RoundToExponent(EInteger.Zero, ERounding.HalfEven);
387+
var offset = t.Subtract(n);
388+
var s = EDecimal.Create(5, -1).Subtract(offset.Abs());
389+
var sign = n.ToEInteger().IsEven ? EDecimal.One : EDecimal.One.Negate();
390+
if (s.IsZero)
391+
return (EDecimal.Zero, offset.Sign > 0 ? sign : sign.Negate());
392+
var angle = InternalAMExtensions.ConstantCache.Lookup(work).Pi.Multiply(s, work);
393+
var cos = angle.Sin(work).Multiply(sign, work);
394+
var sin = offset.IsZero ? EDecimal.Zero : angle.Cos(work).Multiply(offset.Sign > 0 ? sign : sign.Negate(), work);
395+
return (cos, sin);
396+
}
332397
}
333398
}
334399
}

‎Sources/Tests/UnitTests/Convenience/IncompleteGammaTest.cs‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//
1+
//
22
// Copyright (c) 2019-2026 Angouri.
33
// AngouriMath is licensed under MIT.
44
// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.
@@ -45,6 +45,16 @@ private static void AssertClose(string reference, EDecimal actual)
4545
// On the cut, from above.
4646
[InlineData("gamma(1/2, -3)", "1.77245385090551602729816748334114518279754945612238712821381", "-14.6261713840190916850829290647365438829669286131782239586917")]
4747
[InlineData("gamma(-1, -20)", "1.35739289356707492202577927702104389515074891049797988869358E+6", "3.14159265358979323846264338327950288419716939937510582097494")]
48+
// Far into the left half-plane, by the lower function's series of one sign there: each part to its own size,
49+
// the real part of the first being Gamma(1/2).
50+
[InlineData("gamma(1/2, -143)", "1.772453850905516027298167483341145182797549456122387128213808", "-1.066534514082282609180752885388079901236847457667946132456796E+61")]
51+
[InlineData("gamma(-5/2, -60)", "-0.9453087204829418812256893244486107641586930432652731350473642", "7.256677574613207107369164321252421746598199910922310065217093E+19")]
52+
[InlineData("gamma(1/2, -100 + 5i)", "2.607638471329857822055492550981639715386578693808752929552738E+42", "-7.00326380520744724063748887540346529570189148404061756494028E+41")]
53+
// Past the continued fraction's threshold on the cut, with the part that Gamma(a) is the whole of.
54+
[InlineData("gamma(1/2, -300)", "1.772453850905516027298167483341145182797549456122387128213808", "-1.123338927148883734198084176745306173925451665800872666587958E+129")]
55+
[InlineData("gamma(3/2, -250)", "0.8862269254527580136490837416705725913987747280611935641069039", "5.911793698136623273673640330606776777031254613823953153197099E+109")]
56+
[InlineData("gamma(-3, -300)", "2.430575190291251015731676197614951916724433231125758635038235E+120", "0.5235987755982988730771072305465838140328615665625176368291574")]
57+
[InlineData("gamma(0, -300)", "-6.496482508088665789025691894934247269238089700566375975103098E+127", "-3.141592653589793238462643383279502884197169399375105820974945")]
4858
[InlineData("gamma(100, 10)", "9.3326215443944152681699238856266700490715968264381621468593E+155", "0")]
4959
[InlineData("gamma(1/2 + i, 2 + 3i)", "-0.0106186868505695828645674618919283599789635032416382778377351", "-0.026574511726313175577690285230774989836262216835255934709977")]
5060
public void IsItsValue(string expression, string real, string imaginary)
@@ -63,6 +73,7 @@ public void IsItsValue(string expression, string real, string imaginary)
6373
[InlineData("gamma(1/3, -2)", "10.212552064243320672871765046103178972863292927441879189946367")]
6474
[InlineData("gamma(2/3, -4)", "-36.897696051078160810409733567406007863692044903007209488356707")]
6575
[InlineData("gamma(2/5, -17/15)", "-1.6376459092577785167485577680520445343763746015778210809826328")]
76+
[InlineData("gamma(1/3, -143)", "4.669432864644246857974117704263949442598436311750677119332775E+60")]
6677
[InlineData("gamma(-1/3, -300)", "9.71532351293767903599760645811774844613945799010165863950178E+126")]
6778
public void OnTheCutItTakesThePowersRealRoot(string expression, string real)
6879
{

0 commit comments

Comments
 (0)