Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,19 @@ not seen, and a symmetric divergent one comes to its principal value
| `"ln(x)".ToEntity().DefiniteIntegral("x", 0, 1)` | `NaN` | `-1.00000` |
| `"1/x".ToEntity().DefiniteIntegral("x", 0, 1)` | `NaN` | `NaN` |

### A definite integral whose bound has no decided limit is left as written

**Not silent: what was `NaN` is the integral as written.** Where the antiderivative is undefined at a bound, its limit
from inside the range is taken, and where that limit could not be decided the integral was `NaN`, which says it does not
exist. It is left as written now. The limit at a bound is also asked within a budget of the limits it takes along the way,
so that a simplification asking it for each spelling of the integrand ends.
([#2081](https://github.com/asc-community/AngouriMath/issues/2081))

| Input | Was (2.6.0) | Now |
|---|---|---|
| `"integral(ln(x) * erf(x), x, 0, 1)".ToEntity().Simplify()` | `NaN` | `integral(erf(x) * ln(x), x, 0, 1)`, whose value is `-0.2613…` |
| `"integral(ln(x) * Si(x), x, 0, 1)".ToEntity().Simplify()` | `NaN` | `integral(Si(x) * ln(x), x, 0, 1)`, whose value is `-0.2466…` |

### A root of a small or a large number is searched for at the size of one

**Silent: wrong answers.** The search for an exact root of a number rounded each candidate to six digits, absolutely.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,10 @@ private static bool HoldsAnIncompleteGammaFunction(Entity expr)
/// at 0 is <c>0 * -oo</c>, so the integral of <c>ln(x)</c> from 0 to 1 was <c>NaN</c> where it
/// is -1. The limit is taken only where the value is undefined, so a bound the substitution
/// answered is answered as before. Its side is known at an infinity, and at a finite bound
/// from the other bound where both have numeric values. Elsewhere, and where the limit is not
/// decided, the value stays undefined, as it is for <c>sin(x)</c> from 0 to <c>+oo</c>,
/// whose antiderivative has no limit there.
/// from the other bound where both have numeric values. Elsewhere, and where the limit is
/// undefined, the value stays undefined, as it is for <c>sin(x)</c> from 0 to <c>+oo</c>,
/// whose antiderivative has no limit there. Where the limit is not decided there is no value,
/// <see langword="null"/>, and the integral is left as written.
/// </remarks>
private static Entity? AtTheBound(Entity antiderivative, Variable x, Entity bound, Entity otherBound)
{
Expand All @@ -147,9 +148,51 @@ private static bool HoldsAnIncompleteGammaFunction(Entity expr)
// and evaluating Gamma(3/4) at every step, for over a minute. https://github.com/asc-community/AngouriMath/issues/2075
if (HoldsAnIncompleteGammaFunction(antiderivative))
return null;
var limit = antiderivative.Limit(x, bound, inside);
return limit.IsNaN || limit.Nodes.Any(node => node is Limitf) ? value : limit;
var limit = LimitAtTheBound(antiderivative, x, bound, inside);
// A limit that is not decided is no value: the integral is left as written. Undefined would say it does not
// exist, and the integral of ln(x) erf(x) from 0 to 1 was NaN where it is -0.2613, for want of erf's limit at
// 0. https://github.com/asc-community/AngouriMath/issues/2081
if (limit.Nodes.Any(node => node is Limitf))
return null;
return limit.IsNaN ? value : limit;
}

/// <summary>
/// The antiderivative's limit at a bound, remembered under the settings it was taken in, as
/// the cache of answers remembers an antiderivative. A definite integral left as written is evaluated
/// again by every candidate <c>Simplify</c> tries, and the limit that left it so is the slow part: at 0,
/// the antiderivative of <c>ln(x) erf(x)</c> is an infinity less an infinity that l'Hopital's rule does
/// not settle, two seconds of search each time, and the simplification ran past two minutes.
/// https://github.com/asc-community/AngouriMath/issues/2081
/// </summary>
private static Entity LimitAtTheBound(Entity antiderivative, Variable x, Entity bound, ApproachFrom side)
{
if (limitsAtBounds is null || limitsAtBoundsUnder is null || !SettingsState.StillHolds(limitsAtBoundsUnder))
{
limitsAtBounds = new();
limitsAtBoundsUnder = SettingsState.Snapshot();
}
var into = limitsAtBounds;
var stamp = limitsAtBoundsUnder;
var key = (antiderivative, x, bound, side);
if (into.TryGetValue(key, out var known))
return known;
// Within a budget of the limits it asks along the way. Past it the limit is undecided and the integral left
// as written, which is no wrong answer. The most a decided limit at a bound took in the tests is 957; one
// spelling of the antiderivative of ln(x) Si(x) took over 38,000 in forty seconds without deciding.
var limit = Functions.Algebra.LimitFunctional.WithinABudget(LimitsAtABound, () => antiderivative.Limit(x, bound, side));
if (SettingsState.StillHolds(stamp))
{
if (into.Count >= 256)
into.Clear();
into[key] = limit;
}
return limit;
}
[System.ThreadStatic] private static Dictionary<(Entity, Variable, Entity, ApproachFrom), Entity>? limitsAtBounds;
/// <summary>How many limits the limit at one bound may ask in all.</summary>
private const int LimitsAtABound = 2000;
[System.ThreadStatic] private static object?[]? limitsAtBoundsUnder;

/// <summary>
/// Integrates numerically over <paramref name="x"/> between two bounds, without
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,38 @@ private static Entity ExpandLogarithm(Entity expr)
_ => expr
};

/// <summary>
/// How many limits a budgeted computation may still take, every one it asks along the way counted, or
/// <see langword="null"/> where there is no budget. Past it each is undecided. Set by <see cref="WithinABudget"/>.
/// </summary>
[System.ThreadStatic] private static int? remainingLimits;

/// <summary>
/// <paramref name="compute"/> with at most <paramref name="limits"/> limits taken in all, for a caller that has a
/// use only for a limit it can have cheaply, and leaves the question open otherwise.
/// </summary>
internal static T WithinABudget<T>(int limits, System.Func<T> compute)
{
var enclosing = remainingLimits;
var start = enclosing is { } outer ? System.Math.Min(outer, limits) : limits;
remainingLimits = start;
try { return compute(); }
finally
{
// What was spent here is spent from the enclosing budget too.
var used = start - (remainingLimits ?? 0);
remainingLimits = enclosing is { } stillOuter ? stillOuter - used : null;
}
}

public static Entity? ComputeLimit(Entity expr, Variable x, Entity dest, ApproachFrom side = ApproachFrom.BothSides, bool acceptNaN = false)
{
if (remainingLimits is { } remaining)
{
if (remaining <= 0)
return null;
remainingLimits = remaining - 1;
}
// The approach is stated for the whole computation, not only where the destination
// has been normalised to +oo. Everything below simplifies as it goes, and a rule that
// may only fire while the approach is in scope is otherwise declining through most of
Expand Down
22 changes: 22 additions & 0 deletions Sources/Tests/UnitTests/Calculus/ImproperIntegralTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,28 @@ public void ABoundWhereTheAntiderivativeIsUndefinedIsALimit(string integrand, st
[Fact]
public void AnAntiderivativeWithoutALimitLeavesNaN() => Assert.Equal(MathS.NaN, Definite("sin(x)", "0", "+oo"));

/// <summary>
/// And as written where the limit is not decided: undefined would say the integral does not exist, and
/// <c>ln(x) erf(x)</c> over <c>[0, 1]</c>, whose antiderivative is an infinity less an infinity at 0 that no rule
/// settles, was <c>NaN</c> where it is -0.2613. The limit there is asked within a budget, and the simplification,
/// which asks it again for each spelling of the integrand it tries, ran past two minutes without one.
/// https://github.com/asc-community/AngouriMath/issues/2081
/// </summary>
[Theory]
[InlineData("ln(x) * erf(x)")]
[InlineData("ln(x) * Si(x)")]
public void ABoundWhoseLimitIsNotDecidedLeavesTheIntegralAsWritten(string integrand)
{
Entity? result = null;
var task = System.Threading.Tasks.Task.Run(() => result = Definite(integrand, "0", "1"));
// The budget bounds the work, in limits; this wall is a backstop against a hang that the budget no longer
// bounds, not a measure of time. The erf integral takes about nine seconds alone, and a wall of sixty
// fired in the full suite on a loaded machine.
Assert.True(task.Wait(System.TimeSpan.FromMinutes(5)), $"the integral of {integrand} from 0 to 1 was still being simplified");
Assert.IsType<Entity.Integralf>(result);
Assert.DoesNotContain(result!.Nodes, node => node == MathS.NaN);
}

/// <summary>A bound the substitution answers is answered as it was.</summary>
[Theory]
[InlineData("e^(-x)", "0", "+oo", "1")]
Expand Down
Loading