diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 864a86844..9bab3fdbe 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -204,6 +204,29 @@ did, and `ln(x + 2) / ln(x + 3)` at 0 was `ln(2) / ln(3)`. With symbols it was n | `"limit(ln(a + 1/x) / ln(b + 1/x), x, +oo)".ToEntity().Simplify()` | `a / b` | `ln(a) / ln(b)` | | `"limit(ln(1 + x) / ln(y + x), x, 0)".ToEntity().Simplify()` | `1 / y` | `0 provided not ln(y) = 0` | +### A limit over a vanishing absolute value is not claimed not to exist + +**Silent: `1/abs(x)` at 0 is `+oo`, where it was `NaN`, the claim that the limit does not exist.** The sign a +vanishing divisor keeps was read off its derivatives, and the derivative of `abs(x)` is `sgn(x)`, 0 at 0, with +nothing after it: each side was left unevaluated, and the two-sided limit was `NaN`. An absolute value, and a real +power, product or quotient of absolute values, now vanishes from above. And two sides that agree are the two-sided +limit for an expression real on both sides, which now includes an absolute value, a sine, a cosine and an +arctangent of something real, and an arcsine and an arccosine inside (-1, 1). Where the sides differ, as for +`x / abs(x)`, the limit is still `NaN`. + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `"1/abs(x)".ToEntity().Limit("x", 0)` | `NaN` | `+oo` | +| `"1/abs(x)".ToEntity().Limit("x", 0, ApproachFrom.Right)` | `limitright(1 / abs(x), x, 0)` | `+oo` | +| `"-2/abs(x)".ToEntity().Limit("x", 0)` | `NaN` | `-oo` | +| `"1/abs(sin(x))".ToEntity().Limit("x", 0)` | `NaN` | `+oo` | +| `"(x + 1)/abs(x)".ToEntity().Limit("x", 0)` | `NaN` | `+oo` | +| `"1/abs(x - 1)".ToEntity().Limit("x", 1)` | `NaN` | `+oo` | +| `"abs(x)^(-3/2)".ToEntity().Limit("x", 0)` | `NaN` | `+oo` | +| `"abs(x)^(-3/2)".ToEntity().Limit("x", 0, ApproachFrom.Right)` | `limitright(abs(x) ^ (-3/2), x, 0)` | `+oo` | +| `"sqrt(x^2)^(-3/2)".ToEntity().Limit("x", 0)` | `NaN` | `+oo` | +| `"sin(arcsin(x)) * (tan(-1) + 2/x)".ToEntity().Limit("x", 0)` | `NaN` | `2` | + ### An equation in two different square roots is solved exactly **Silent: an equation `m_1 sqrt(A) + m_2 sqrt(B) + P = 0` in x alone, with two different square roots, is answered in diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs index e465e7f1f..50931b529 100644 --- a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs +++ b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs @@ -328,6 +328,13 @@ partial record Powf // the logarithm above declines it. https://github.com/asc-community/AngouriMath/issues/2266 if (exponentLimit?.Evaled is not Integer && ArrivesOnTheCutOffTheRealLine(Base, baseLimit, x, dist)) return null; + // A base that is never negative and vanishes, to an exponent tending to a negative + // number, tends to +oo: 0 to a negative power has no value, and abs(x)^(-3/2) at 0 + // was left unevaluated from each side for it. Where the base vanishes, it does from + // above. + if (baseLimit?.Evaled is Real { IsZero: true } && exponentLimit?.Evaled is Real { IsNegative: true, IsFinite: true } + && IsNeverNegative(Base)) + return Real.PositiveInfinity; var substituted = (baseLimit, exponentLimit) switch { diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs index c0130ad6e..33cc7480e 100644 --- a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs @@ -216,12 +216,34 @@ bool RealThere(Entity node) Mulf(var multiplier, var multiplicand) => RealThere(multiplier) && RealThere(multiplicand), Divf(var dividend, var divisor) => RealThere(dividend) && RealThere(divisor), Powf(var @base, Integer) => RealThere(@base), - Powf(var @base, var exponent) => RealThere(@base) && RealThere(exponent) && PositiveOnBothSides(@base), + Powf(var @base, var exponent) => RealThere(@base) && RealThere(exponent) + && (NotNegativeThere(@base) || PositiveOnBothSides(@base)), Logf(var @base, var antilogarithm) => RealThere(@base) && RealThere(antilogarithm) && PositiveOnBothSides(@base) && PositiveOnBothSides(antilogarithm), + // An absolute value is real whatever it is of, and a sine, a cosine and an + // arctangent of a real argument are real. An arcsine and an arccosine are real + // on [-1, 1], so near the point only where their argument is inside it there. + // 1/abs(x)^3 at 0 and sin(arcsin(x)) * (c + 2/x) were NaN for want of these, + // where each side agrees with the other. + Absf => true, + Sinf(var argument) => RealThere(argument), + Cosf(var argument) => RealThere(argument), + Arctanf(var argument) => RealThere(argument), + Arcsinf(var argument) => RealThere(argument) && StrictlyInsideTheUnitInterval(argument), + Arccosf(var argument) => RealThere(argument) && StrictlyInsideTheUnitInterval(argument), _ => false }; } + // Not negative on either side, so that a real power of it is real: what IsNeverNegative + // reads off the form, an even power of something real, and a real power that is itself + // real -- sqrt(x^2), whose base vanishes at 0 and is not positive there. + bool NotNegativeThere(Entity node) + => IsNeverNegative(node) + || node is Powf(var @base, Integer { EInteger.IsEven: true }) && RealThere(@base) + || node is Powf(_, not Integer) && RealThere(node); + bool StrictlyInsideTheUnitInterval(Entity node) + => node.Substitute(x, dest).Evaled is Real { IsFinite: true } value + && value.EDecimal.Abs().CompareTo(PeterO.Numbers.EDecimal.One) < 0; bool PositiveOnBothSides(Entity node) => !node.ContainsNode(x) ? node.Evaled is Real { IsPositive: true } @@ -485,7 +507,14 @@ internal static T WithinABudget(int limits, System.Func compute) return byRule; // Asking each side properly, which is what the two above were not asked. // https://github.com/asc-community/AngouriMath/issues/719 - if (WhereBothSidesAgree(expr, x, dest) is { } agreed) + // Only where a side came back NaN. A side the descent answered with a value is + // what asking that side outright answers too, since the one-sided path returns + // the descent's value before anything else, so two values that differ differ + // again: tan(x/2) at pi, +oo and -oo, asked both sides through the whole of the + // machinery each time, and a definite integral over [0, pi] spent its budget of + // limits on that. + if ((fromLeft.Evaled == MathS.NaN || fromRight.Evaled == MathS.NaN) + && WhereBothSidesAgree(expr, x, dest) is { } agreed) return agreed; return MathS.NaN; // A two-sided limit cannot exist if the limit from left and right don't match. } diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs index 6ca988618..5ffbe1905 100644 --- a/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs +++ b/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs @@ -1102,13 +1102,35 @@ internal static bool IsDeterminate(Entity combined, Variable x) // https://github.com/asc-community/AngouriMath/issues/2274 if (EvalAssumingContinuous(dividend.Limit(x, dest, side)) is not Real { IsZero: false, IsNaN: false } dividendLimit) return null; - if (SignFromVanishingOrder(divisor, x, dest, side) is not { } divisorSign) + if ((IsNeverNegative(divisor) ? 1 : SignFromVanishingOrder(divisor, x, dest, side)) is not { } divisorSign) return null; return dividendLimit.IsNegative == divisorSign > 0 ? Real.NegativeInfinity : Real.PositiveInfinity; } + /// + /// Whether an expression is real and not negative wherever it has a value, by its + /// form: an absolute value, a power of such a thing to a real exponent, a product or a + /// quotient of them, a positive constant. + /// + /// + /// The sign a vanishing divisor keeps is otherwise read off its derivatives, which an + /// absolute value does not have where it vanishes: the derivative of abs(x) is sgn(x), + /// 0 at 0, and the next one has no value there. So 1/abs(x) at 0 was left unevaluated + /// from each side and NaN from both, where it is +oo. Where such an expression vanishes + /// it does so from above, whatever its argument does. + /// + internal static bool IsNeverNegative(Entity expr) => expr switch + { + Absf => true, + Powf(var @base, var exponent) => IsNeverNegative(@base) && exponent.Evaled is Real { IsNaN: false }, + Mulf(var multiplier, var multiplicand) => IsNeverNegative(multiplier) && IsNeverNegative(multiplicand), + Divf(var dividend, var divisor) => IsNeverNegative(dividend) && IsNeverNegative(divisor), + _ when expr.Evaled is Real { IsNegative: false, IsNaN: false } => !expr.FreeVariables.Any(), + _ => false + }; + /// /// The sign an expression that vanishes at the destination keeps on one side of it, /// or where it cannot be read off. diff --git a/Sources/Tests/UnitTests/Calculus/AnAbsoluteValueVanishesFromAboveTest.cs b/Sources/Tests/UnitTests/Calculus/AnAbsoluteValueVanishesFromAboveTest.cs new file mode 100644 index 000000000..be61a7c0b --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/AnAbsoluteValueVanishesFromAboveTest.cs @@ -0,0 +1,51 @@ +// +// Copyright (c) 2019-2026 Angouri. +// AngouriMath is licensed under MIT. +// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md. +// Website: https://am.angouri.org. +// + +using AngouriMath.Core; +using Xunit; + +namespace AngouriMath.Tests.Calculus +{ + /// + /// 1/abs(x) at 0 was NaN -- the claim that the limit does not exist -- where it is +oo from both sides. The sign + /// a vanishing divisor keeps was read off its derivatives, and the derivative of abs(x) is sgn(x), 0 at + /// 0, with nothing after it; so each side was left unevaluated, and the two-sided limit was NaN for the descent's two + /// readings not matching. And where both sides were answered, they were promoted to the two-sided limit only for an + /// expression shown real on both sides, which knew nothing of an absolute value, a sine or an arcsine: + /// sin(arcsin(x)) * (tan(-1) + 2/x) is 2 from each side and was NaN. The same on 2.6.0. Found by limitcheck at + /// DEPTH=3, SEED=29; no issue was filed. + /// + [Trait("Area", "Calculus")] + public sealed class AnAbsoluteValueVanishesFromAboveTest + { + [Theory] + [InlineData("1/abs(x)", "0", "+oo")] + [InlineData("1/abs(x)^3", "0", "+oo")] + [InlineData("-2/abs(x)", "0", "-oo")] + [InlineData("1/abs(x - 1)", "1", "+oo")] + [InlineData("1/abs(sin(x))", "0", "+oo")] + [InlineData("(x + 1)/abs(x)", "0", "+oo")] + [InlineData("abs(x)^(-3/2)", "0", "+oo")] + [InlineData("sqrt(x^2)^(-3/2)", "0", "+oo")] + [InlineData("sin(arcsin(x)) * (tan(-1) + 2/x)", "0", "2")] + public void IsTheLimitFromEachSideAndBoth(string input, string destination, string expected) + { + foreach (var side in new[] { ApproachFrom.Left, ApproachFrom.Right, ApproachFrom.BothSides }) + Assert.Equal(MathS.FromString(expected), MathS.FromString(input).Limit("x", destination, side)); + } + + // Where the two sides differ, or one of them is not a limit of real values, there is still no two-sided limit. + [Theory] + [InlineData("x/abs(x)")] + [InlineData("sqrt(x^2)/x")] + [InlineData("abs(x)^(1/2)/x")] + [InlineData("arcsin(x + 1)/x")] + [InlineData("x^x")] + public void HasNoTwoSidedLimit(string input) + => Assert.Equal(MathS.NaN, MathS.FromString(input).Limit("x", 0)); + } +}