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
36 changes: 35 additions & 1 deletion Sources/AngouriMath/Convenience/MathS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5700,7 +5700,41 @@ internal static EDecimal DowncastingTolerance
/// </summary>
public static Setting<EContext> DecimalPrecisionContext =>
decimalPrecisionContext ??= new EContext(100, ERounding.HalfUp, -100, 1000, false);
[ThreadStatic] private static Setting<EContext>? decimalPrecisionContext;
[ThreadStatic] private static Setting<EContext>? decimalPrecisionContext;

/// <summary>
/// Whether functions are being read as real-valued or complex-valued. It is a
/// statement about the reading and not about any one expression -- an
/// <see cref="Entity"/>'s own <c>Codomain</c> says what a particular node is
/// declared over, where this says what the library should take a function to be.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="Domain.Complex"/> by default, which is what AngouriMath has always
/// done: <c>lim x-&gt;0- ln(x)</c> answers <c>-oo</c>, reading the logarithm along
/// its complex continuation, since the logarithm of a negative real is
/// <c>ln|x| + i*pi</c> and only its magnitude is being reported.
/// </para>
/// <para>
/// Under <see cref="Domain.Real"/> a limit approached through values the function
/// does not take in the reals has no value rather than the continued one. That is
/// the missing information behind a family of questions -- whether two agreeing
/// one-sided limits may be promoted to a two-sided one, and whether
/// <c>sqrt(x^2)</c> is <c>x</c> -- because each of them is answerable one way over
/// the reals and another over the complex plane, and the library had no way to be
/// told which was meant.
/// <a href="https://github.com/asc-community/AngouriMath/issues/719">#719</a>
/// </para>
/// <example>
/// <code>
/// using var _ = MathS.Settings.Codomain.Set(Domain.Real);
/// Console.WriteLine("ln(x)".Limit("x", 0, ApproachFrom.Left));
/// </code>
/// prints the unevaluated limit, where the default prints <c>-oo</c>.
/// </example>
/// </remarks>
public static Setting<Domain> Codomain => codomain ??= Domain.Complex;
[ThreadStatic] private static Setting<Domain>? codomain;
}

/// <summary>Returns an <see cref="Entity"/> in polynomial order if possible</summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,20 @@ private static Entity ExpandLogarithm(Entity expr)
// differentiate it.
if (expr is not ContinuousNode and not Variable and not Piecewise)
return null;

// Under a real codomain a limit reached only through values the function does not
// take in the reals is not a limit of it.
//
// Before the rewrites below and not after them, which is load-bearing rather than
// tidy: each of those asks for limits of its own, and under this reading those
// sub-limits come back unevaluated -- whereupon evaluating the unevaluated limit
// asks for it again, through the same rewrite, without end. Answering here costs
// nothing and asks nothing.
// https://github.com/asc-community/AngouriMath/issues/719
if (RealCodomainWithdraws(expr, x, dest,
side is ApproachFrom.Left ? ApproachFrom.Left : ApproachFrom.Right))
return null;

expr = expr.Replace(ExpandLogarithm);

// In front of both paths, not only the two-sided one. Each of these is guarded on
Expand Down
88 changes: 88 additions & 0 deletions Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,94 @@ private static Entity EvalAssumingContinuous(Entity expr) =>
Providedf(var inner, _) => inner,
var x => x
};
/// <summary>
/// How close in the approach is sampled when asking whether a function stays real on
/// the way to its destination. Powers of ten rather than a fixed step, because what
/// matters is near and not evenly spaced: <c>sqrt(x - 1)</c> is real just to the left
/// of 2 and not just to the left of 1, and only the nearer samples tell those apart.
/// </summary>
/// <remarks>
/// It stops at a thousandth, and the reason is cost rather than principle. The sample
/// point goes into the expression, and an expression may put it in an exponent:
/// <c>(1 + x)^(1/x)</c> at 1e-9 is <c>1.000000001</c> raised to a billion, evaluated
/// at a hundred digits, and asking that six times on every limit the machinery takes
/// turned a 20 ms limit into a timeout.
/// <para/>
/// Sampling less far in only ever *misses* a function that leaves the reals nearer the
/// destination than this reaches, and a miss leaves the limit answered exactly as it
/// was before. The guard withdraws on evidence and never on the absence of it, so the
/// cheap end of that trade is the safe one.
/// </remarks>
[ConstantField] private static readonly int[] ApproachScales = { 1, 2, 3 };

/// <summary>
/// Whether the expression takes values outside the reals on the way in. Under a real
/// codomain such a limit has no value, rather than the value its complex continuation
/// approaches.
/// </summary>
/// <remarks>
/// <c>lim x-&gt;0- ln(x)</c> is the plain case: the logarithm of a negative real is
/// <c>ln|x| + i*pi</c>, and answering <c>-oo</c> reports the magnitude of something
/// that is not a real number at any point of the approach.
/// <para/>
/// Decided by sampling rather than symbolically, and deliberately: what is being asked
/// is whether the function *takes* non-real values near a point, which no property of
/// the tree answers. The direction of error is chosen too. A sample that cannot be
/// evaluated, or that comes back non-finite, is passed over rather than counted, and an
/// expression carrying a second variable is not judged at all -- so the answer is only
/// ever "yes, demonstrably" or "not shown", and a limit is withdrawn on evidence.
/// <a href="https://github.com/asc-community/AngouriMath/issues/719">#719</a>
/// </remarks>
private static bool LeavesTheRealsOnTheApproach(Entity expr, Variable x, Entity dest, ApproachFrom side)
{
if (expr.Vars.Count() != 1)
return false;
var towardsNegative = side is ApproachFrom.Left;
if (!dest.IsFinite)
towardsNegative = dest.Evaled is Real { IsNegative: true };
foreach (var scale in ApproachScales)
{
var ten = EInteger.FromInt32(10).Pow(scale);
Entity point;
if (dest.IsFinite)
{
var step = (Entity)Rational.Create(EInteger.One, ten);
point = towardsNegative ? dest - step : dest + step;
}
else
{
var far = (Entity)Integer.Create(ten);
point = towardsNegative ? -far : far;
}
Complex value;
try
{
if (expr.Substitute(x, point).EvalNumerical() is not Complex evaluated)
continue;
value = evaluated;
}
catch (Core.Exceptions.AngouriBugException) { throw; }
catch (System.Exception) { continue; }
if (!value.IsFinite)
continue;
var imaginary = value.ImaginaryPart.EDecimal.Abs();
var real = value.RealPart.EDecimal.Abs();
// Relative, since the point may be evaluated at a scale where an absolute
// threshold means nothing, and a rounding artefact must not withdraw a limit.
if (imaginary.GreaterThan(EDecimal.Create(1, -6).Multiply(EDecimal.Max(real, EDecimal.One, null))))
return true;
}
return false;
}

/// <summary>
/// Whether the reading in force is of real-valued functions, in which case a limit
/// reached only through the complex plane is not one.
/// </summary>
internal static bool RealCodomainWithdraws(Entity expr, Variable x, Entity dest, ApproachFrom side)
=> MathS.Settings.Codomain.Value is AngouriMath.Core.Domain.Real
&& LeavesTheRealsOnTheApproach(expr, x, dest, side);

private static Entity ApplyFirstRemarkable(Entity expr, Variable x, Entity dest)
=> expr switch
{
Expand Down
151 changes: 151 additions & 0 deletions Sources/Tests/UnitTests/Calculus/RealCodomainLimitTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
//
// Copyright (c) 2019-2022 Angouri.
// AngouriMath is licensed under MIT.
// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.
// Website: https://am.angouri.org.
//

using System.Linq;
using AngouriMath;
using AngouriMath.Core;
using AngouriMath.Extensions;
using Xunit;

namespace AngouriMath.Tests.Calculus
{
/// <summary>
/// AngouriMath's one-sided limits did not stay inside the reals: <c>lim x-&gt;0- ln(x)</c>
/// answered <c>-oo</c>, which is the magnitude of <c>ln|x| + i*pi</c> and not the limit of
/// anything real-valued. There was no way to say which reading was meant, and that missing
/// information is what stops agreeing one-sided limits from being promoted to a two-sided
/// one -- promoting them without it would also give <c>lim x-&gt;0 x^x</c> the value 1,
/// which the suite pins as non-existent because <c>x^x</c> is not real for negative x.
/// <see cref="MathS.Settings.Codomain"/> is that information.
/// https://github.com/asc-community/AngouriMath/issues/719
/// </summary>
public sealed class RealCodomainLimitTest
{
private static Entity Limit(string expr, string destination, ApproachFrom side) =>
expr.ToEntity().Limit("x", destination.ToEntity(), side).Simplify();

private static Entity LimitOverTheReals(string expr, string destination, ApproachFrom side)
{
using var _ = MathS.Settings.Codomain.Set(Domain.Real);
return Limit(expr, destination, side);
}

private static bool IsUnevaluated(Entity limit) => limit.Nodes.Any(node => node is Entity.Limitf);

/// <summary>
/// The default is what AngouriMath has always done, so that nothing changes for anyone
/// who does not ask. This is the whole of the compatibility claim and is checked
/// against the values themselves rather than against the setting's value.
/// </summary>
[Theory]
[InlineData("ln(x)", "0", "-oo")]
[InlineData("x * ln(x)", "0", "0")]
[InlineData("x ^ x", "0", "1")]
[InlineData("sqrt(x)", "0", "0")]
public void TheDefaultReadingIsUnchanged(string expr, string destination, string expected)
{
Assert.Equal(Domain.Complex, MathS.Settings.Codomain.Value);
Assert.Equal(expected.ToEntity().Evaled,
Limit(expr, destination, ApproachFrom.Left).Evaled);
}

/// <summary>
/// Over the reals each of these is approached through values the function does not
/// take, so it has no limit rather than the one its continuation approaches.
/// </summary>
[Theory]
[InlineData("ln(x)", "0")]
[InlineData("x * ln(x)", "0")]
[InlineData("x ^ x", "0")]
[InlineData("sqrt(x)", "0")]
[InlineData("ln(x) / x", "0")]
[InlineData("sqrt(x) + 1", "0")]
public void ALimitReachedOnlyThroughTheComplexPlaneIsWithdrawn(string expr, string destination) =>
Assert.True(IsUnevaluated(LimitOverTheReals(expr, destination, ApproachFrom.Left)),
$"{expr} at {destination}- still answered over the reals");

/// <summary>
/// The other side of the same point, where the function *is* real, is untouched. That
/// is the whole distinction: it is the approach that is judged, not the expression.
/// </summary>
[Theory]
[InlineData("ln(x)", "0", "-oo")]
[InlineData("x * ln(x)", "0", "0")]
[InlineData("x ^ x", "0", "1")]
[InlineData("sqrt(x)", "0", "0")]
public void TheSideOnWhichItIsRealStillAnswers(string expr, string destination, string expected) =>
Assert.Equal(expected.ToEntity().Evaled,
LimitOverTheReals(expr, destination, ApproachFrom.Right).Evaled);

/// <summary>
/// A function real on both sides is unaffected in either direction, which is most of
/// them -- the setting has to be nearly invisible or it is not usable.
/// </summary>
[Theory]
[InlineData("sin(x) / x", "0", "1")]
[InlineData("(1 - cos(x)) / x ^ 2", "0", "1/2")]
[InlineData("x ^ 2", "0", "0")]
[InlineData("1 / x ^ 2", "0", "+oo")]
[InlineData("e ^ x", "0", "1")]
[InlineData("(1 + x) ^ (1/x)", "0", "e")]
public void AFunctionRealOnBothSidesIsUnaffected(string expr, string destination, string expected)
{
foreach (var side in new[] { ApproachFrom.Left, ApproachFrom.Right })
Assert.Equal(expected.ToEntity().Evaled,
LimitOverTheReals(expr, destination, side).Evaled);
}

// At infinity there is one direction of approach and it is the destination's own sign.
[Theory]
[InlineData("ln(x)", "+oo", "+oo")]
[InlineData("x ^ 2", "+oo", "+oo")]
[InlineData("1 / x", "+oo", "0")]
[InlineData("x ^ 2", "-oo", "+oo")]
public void ALimitAtInfinityThatStaysRealIsUnaffected(string expr, string destination, string expected) =>
Assert.Equal(expected.ToEntity().Evaled,
LimitOverTheReals(expr, destination, ApproachFrom.Left).Evaled);

[Theory]
[InlineData("ln(x)", "-oo")]
[InlineData("sqrt(x)", "-oo")]
public void ALimitAtInfinityThatLeavesTheRealsIsWithdrawn(string expr, string destination) =>
Assert.True(IsUnevaluated(LimitOverTheReals(expr, destination, ApproachFrom.Left)),
$"{expr} at {destination} still answered over the reals");

/// <summary>
/// The setting is restored on the way out, and it is thread-static like every other
/// one -- worth pinning, since a codomain that leaked would change answers far from
/// where it was set.
/// </summary>
[Fact]
public void TheSettingIsRestoredWhenItGoesOutOfScope()
{
Assert.Equal(Domain.Complex, MathS.Settings.Codomain.Value);
using (var _ = MathS.Settings.Codomain.Set(Domain.Real))
Assert.Equal(Domain.Real, MathS.Settings.Codomain.Value);
Assert.Equal(Domain.Complex, MathS.Settings.Codomain.Value);
}

/// <summary>
/// What this does not yet do. Promoting two agreeing one-sided limits to a two-sided
/// one is the point of the setting and is deliberately not part of it: step 1 changes
/// what every one-sided limit answers under a real codomain, and that wants measuring
/// on its own before anything is built on top. Pinned so the next step has somewhere
/// to land and so the current state is a decision rather than an oversight.
/// </summary>
[Fact]
public void AgreeingOneSidedLimitsAreNotYetPromoted()
{
using var _ = MathS.Settings.Codomain.Set(Domain.Real);
// Both sides are 0 and the two-sided answer is still NaN.
Assert.Equal(Entity.Number.Integer.Create(0),
Limit("x * ln(x)", "0", ApproachFrom.Right).Evaled);
Assert.Equal(MathS.NaN.Evaled,
"x * ln(x)".ToEntity().Limit("x", 0).Simplify().Evaled);
}
}
}
Loading