diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 101cf6441..02b879057 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -226,6 +226,24 @@ mentions answers it as a parameter, as a short system does, where it was `null` | `MathS.Equations("x - 1", "x^2 - 1").Solve("x", "y")` | `null` | `[[1, t_1]]` | | `MathS.Equations("a - 1", "b * 0").Solve("a", "b")` | `null` | `[[1, t_1]]` | +### A limit on a logarithm's cut is not read off the principal value + +**Silent: a limit of a logarithm, or of a power to an exponent that is not whole, whose argument arrives on the negative +real line other than along it is left unevaluated.** The principal logarithm is continuous on its cut from above only, and +its value there was given as the limit from any side: `x^x` comes to -1 from below as x comes down to -1, so +`limitright(ln(x^x), x, -1)` is `-i pi`, and it was `ln(-1)`, which is `i pi`. Which side an argument off the real line comes +from is not decided, so a limit from above, whose answer was right, is left unevaluated too. Along the real line the value +on the cut is the limit, and `limit(ln(x), x, -1)` is still `ln(-1)` +([#2266](https://github.com/asc-community/AngouriMath/issues/2266)). + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `"limitright(ln(x^x), x, -1)".ToEntity().Simplify()` | `ln(-1)`, which is `i pi` | unevaluated; the limit is `-i pi` | +| `"limit(ln(x^x), x, -1)".ToEntity().Simplify()` | `ln(-1)` | unevaluated; there is no limit | +| `"limitleft(ln(-1 + i*x), x, 0)".ToEntity().Simplify()` | `ln(-1)` | unevaluated; the limit is `-i pi` | +| `"limitright(sqrt(x^x), x, -1)".ToEntity().Simplify()` | `i` | unevaluated; the limit is `-i` | +| `"limitleft(ln(x^x), x, -1)".ToEntity().Simplify()` | `ln(-1)`, which is right | unevaluated | + ### `Simplify` does not add a condition the input does not have **Silent: an answer defined at fewer points is defined at all of the input's.** A polynomial division in a diff --git a/Sources/.editorconfig b/Sources/.editorconfig index 1051c8258..a48a93c67 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -129,6 +129,9 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed [Tests/UnitTests/Calculus/PowerSubstitutionIntegralTest.cs] file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n +[Tests/UnitTests/Calculus/LimitOnABranchCutTest.cs] +file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n + [Tests/UnitTests/Calculus/ProductOfPowersDerivativeTest.cs] file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Gruntz/Gruntz.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Gruntz/Gruntz.cs index 32ff6c504..6025d430c 100644 --- a/Sources/AngouriMath/Functions/Continuous/Limits/Gruntz/Gruntz.cs +++ b/Sources/AngouriMath/Functions/Continuous/Limits/Gruntz/Gruntz.cs @@ -78,6 +78,13 @@ private static void Trace(string what, object? value) { if (depth > 0) return null; // already inside; the caller is the entry + // The algorithm is for real functions, and its series takes the logarithm of a + // negative leading coefficient as the principal one, which is the limit from above + // the cut. ln(x^x) at -1 from the right is ln((1/x - 1)^(1/x - 1)) here, whose + // argument comes to -1 from below, and it answered i pi where the limit is -i pi. + // https://github.com/asc-community/AngouriMath/issues/2266 + if (expr.Nodes.Any(node => LimitFunctional.TakesItsValueOnACut(node, x, Real.PositiveInfinity))) + return null; expr = AsExponentials(expr, x); return LimitInf(expr, x); } diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs index d4dba0e85..9b9f9ae23 100644 --- a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs +++ b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs @@ -242,8 +242,15 @@ partial record Logf return lim; else { - var @base = Base.ComputeLimitDivideEtImpera(x, dist, side) is { IsFinite: true } lim1 && lim1 != 0 ? lim1 : Base; - var power = Antilogarithm.ComputeLimitDivideEtImpera(x, dist, side) is { IsFinite: true } lim2 && lim2 != 0 ? lim2 : Antilogarithm; + var baseLimit = Base.ComputeLimitDivideEtImpera(x, dist, side); + var antilogarithmLimit = Antilogarithm.ComputeLimitDivideEtImpera(x, dist, side); + // The logarithm of a limit on the cut is the limit only from above or along the + // real line. https://github.com/asc-community/AngouriMath/issues/2266 + if (ArrivesOnTheCutOffTheRealLine(Base, baseLimit, x, dist) + || ArrivesOnTheCutOffTheRealLine(Antilogarithm, antilogarithmLimit, x, dist)) + return null; + var @base = baseLimit is { IsFinite: true } lim1 && lim1 != 0 ? lim1 : Base; + var power = antilogarithmLimit is { IsFinite: true } lim2 && lim2 != 0 ? lim2 : Antilogarithm; MultithreadingFunctional.ExitIfCancelled(); return ComputeLimitImpl(New(@base, power), x, dist, side); } @@ -256,8 +263,14 @@ partial record Powf { if (ComputeLimitImpl(this, x, dist, side) is { } lim) return lim; + var baseLimit = Base.ComputeLimitDivideEtImpera(x, dist, side); + var exponentLimit = Exponent.ComputeLimitDivideEtImpera(x, dist, side); + // A power to an exponent that is not whole has the logarithm's cut, for the reason + // 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; var substituted = - (Base.ComputeLimitDivideEtImpera(x, dist, side), Exponent.ComputeLimitDivideEtImpera(x, dist, side)) + (baseLimit, exponentLimit) switch { ({ IsFinite: true } lim1, { IsFinite: true } lim2) => New(lim1, lim2), (_, { } l2) when !Base.ContainsNode(x) => New(Base, l2), diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Solvers.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Solvers.cs index a6f8696df..182a65ac4 100644 --- a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Solvers.cs +++ b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Solvers.cs @@ -54,6 +54,10 @@ internal static class LimitSolvers { MultithreadingFunctional.ExitIfCancelled(); if (limit == Real.NaN) return null; + // Nor where a logarithm's argument, or a power's base, arrives on the cut off the + // real line, where the value there is the limit from above and not from below. + // https://github.com/asc-community/AngouriMath/issues/2266 + if (expr.Nodes.Any(node => LimitFunctional.TakesItsValueOnACut(node, x, Infinity))) return null; // Reading the value at the destination is the limit only where the expression // is continuous there, and an indeterminate form is exactly where it is not. // Every other one of them already declines on the line above, because this @@ -160,6 +164,7 @@ static bool OnAJump(Entity argument, System.Func jump) MultithreadingFunctional.ExitIfCancelled(); var innerLimit = LimitFunctional.ComputeLimit(logArgument, x, Real.PositiveInfinity); if (innerLimit is null) return null; + if (LimitFunctional.ArrivesOnTheCutOffTheRealLine(logArgument, innerLimit, x, Real.PositiveInfinity)) return null; // do same as wolframalpha: https://www.wolframalpha.com/input/?i=ln%28-inf%29 if (innerLimit == Real.NegativeInfinity) return Real.PositiveInfinity; if (innerLimit == Real.PositiveInfinity) return Real.PositiveInfinity; @@ -327,6 +332,9 @@ private static bool LogarithmDiverges(Entity argumentLimit) else { + if (LimitFunctional.ArrivesOnTheCutOffTheRealLine(upperLogArgument, upperLogLimit, x, Real.PositiveInfinity) + || LimitFunctional.ArrivesOnTheCutOffTheRealLine(lowerLogArgument, lowerLogLimit, x, Real.PositiveInfinity)) + return null; var div = (MathS.Ln(upperLogLimit) / MathS.Ln(lowerLogLimit)); var divEvaled = div.Evaled; return divEvaled switch diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs index 46d3584a0..e4f6ed4d8 100644 --- a/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs +++ b/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs @@ -855,6 +855,76 @@ internal static bool HoldsAnIndeterminatePowerForm(Entity expr) => expr.Nodes.Any(node => node is Powf(var @base, var exponent) && IsIndeterminatePowerForm(@base, exponent)); + /// + /// Whether , a logarithm's or the base of a power, tends to + /// on the negative real line, where the logarithm has its cut, and + /// is not known to arrive there along the real line. + /// + /// + /// The principal logarithm is continuous on its cut from above only. Its value there, + /// ln(-1) = i pi, is the limit along the line and from above, and from below the limit + /// is -i pi. So a limit read off a logarithm of an argument's limit is that limit only + /// where the argument does not come from below: x^x tends to -1 from below as x comes + /// down to -1, and limitright(ln(x^x), x, -1) answered i pi. A power to an + /// exponent that is not whole is e^(exponent * ln(base)) and has the same cut. Which + /// side an argument off the real line comes from is not decided here, so a limit there is + /// declined, including one from above, whose answer was right. + /// #2266 + /// + internal static bool ArrivesOnTheCutOffTheRealLine(Entity argument, Entity? limit, Variable x, Entity dest) + => argument.ContainsNode(x) + && limit?.Evaled is Real { IsFinite: true, IsNegative: true } + && !IsRealOnTheApproach(argument, x, dest); + + /// + /// Whether a node of an expression that is about to be read at is a + /// logarithm or a power whose argument arrives on the cut off the real line, by + /// , with the argument's value at the destination + /// as its limit. A power to an exponent whose value there is whole is continuous across the + /// cut, where each of its values is reached from both sides. + /// + internal static bool TakesItsValueOnACut(Entity node, Variable x, Entity dest) + { + return node switch + { + Logf(var @base, var antilogarithm) => OnTheCut(@base) || OnTheCut(antilogarithm), + Powf(var @base, var exponent) => exponent.Substitute(x, dest).Evaled is not Integer && OnTheCut(@base), + _ => false + }; + bool OnTheCut(Entity argument) => ArrivesOnTheCutOffTheRealLine(argument, argument.Substitute(x, dest), x, dest); + } + + /// + /// Whether is real as runs along the real line to + /// , decided structurally, as is. A power's + /// base and a logarithm's argument count as positive where their values at the destination + /// are, which they then are near it. Anything else is + /// 's, and unlisted is not real, which costs + /// coverage and never correctness. + /// + private static bool IsRealOnTheApproach(Entity expr, Variable x, Entity dest) + { + if (dest.Evaled is not Real) + return false; + if (!expr.ContainsNode(x)) + return expr.Evaled is Real { IsFinite: true }; + return expr switch + { + Variable variable => variable == x, + Sumf(var augend, var addend) => Along(augend) && Along(addend), + Minusf(var minuend, var subtrahend) => Along(minuend) && Along(subtrahend), + Mulf(var multiplier, var multiplicand) => Along(multiplier) && Along(multiplicand), + Divf(var dividend, var divisor) => Along(dividend) && Along(divisor), + Powf(var @base, Integer) => Along(@base), + Powf(var @base, var exponent) => Along(@base) && Along(exponent) && PositiveThere(@base), + Logf(var @base, var antilogarithm) => Along(@base) && Along(antilogarithm) + && PositiveThere(@base) && PositiveThere(antilogarithm), + _ => TreeAnalyzer.IsRealValued(expr, x) + }; + bool Along(Entity part) => IsRealOnTheApproach(part, x, dest); + bool PositiveThere(Entity part) => part.Substitute(x, dest).Evaled is Real { IsPositive: true }; + } + /// /// Whether the exponent grows without bound, which is all the second remarkable /// limit needs. diff --git a/Sources/Tests/UnitTests/Calculus/LimitOnABranchCutTest.cs b/Sources/Tests/UnitTests/Calculus/LimitOnABranchCutTest.cs new file mode 100644 index 000000000..d9c8e61fc --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/LimitOnABranchCutTest.cs @@ -0,0 +1,68 @@ +// +// 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 System.Linq; +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Calculus +{ + /// + /// A limit of a logarithm, or of a power to an exponent that is not whole, whose argument arrives on the negative real + /// line other than along it is not read off the principal value there. The principal logarithm is continuous on its cut + /// from above only: x^x comes to -1 from below as x comes down to -1, so limitright(ln(x^x), x, -1) is -i pi, and it + /// answered ln(-1), which is i pi. Each expected value is the expression's near the destination. + /// #2266 + /// + [Trait("Area", "Calculus")] + public sealed class LimitOnABranchCutTest + { + /// From below the cut, or from both sides of it, where the principal value is not the limit. + [Theory] + [InlineData("limitright(ln(x^x), x, -1)", "-i pi")] + [InlineData("limitright(sqrt(x^x), x, -1)", "-i")] + [InlineData("limitright(log(2, x^x), x, -1)", "-i pi / ln(2)")] + [InlineData("limitright(ln(x^x) * x, x, -1)", "i pi")] + [InlineData("limitleft(ln(-1 + i x), x, 0)", "-i pi")] + [InlineData("limitleft(sqrt(-1 + i x), x, 0)", "-i")] + [InlineData("limitleft(ln(-1 - i x^2), x, 0)", "-i pi")] + [InlineData("limitright(ln(-e^(i x)), x, 0)", "-i pi")] + public void ItIsTheLimitOrNothing(string limit, string value) + { + var answer = limit.ToEntity().Simplify(); + Assert.True(IsUnevaluated(answer) || answer.Evaled == value.ToEntity().Evaled, $"{limit} is {answer}, where it is {value}"); + } + + /// Where the sides disagree there is no limit, and the principal value was given for it. + [Theory] + [InlineData("limit(ln(x^x), x, -1)")] + [InlineData("limit(ln(-1 + i x), x, 0)")] + [InlineData("limit(ln(e^(i x) - 2), x, pi)")] + public void ThereIsNone(string limit) + { + var answer = limit.ToEntity().Simplify(); + Assert.True(IsUnevaluated(answer) || answer == MathS.NaN, $"{limit} is {answer}"); + } + + /// A limit left as a limit, where ln(-1) is an answer like any number. + private static bool IsUnevaluated(Entity answer) => answer.Nodes.Any(node => node is Entity.Limitf); + + /// Along the real line the value on the cut is the limit, and is answered as before. + [Theory] + [InlineData("limit(ln(x), x, -1)", "i pi")] + [InlineData("limitright(ln(x), x, -1)", "i pi")] + [InlineData("limit(ln(x - 2), x, 0)", "ln(-2)")] + [InlineData("limit(sqrt(x - 2), x, 0)", "sqrt(-2)")] + [InlineData("limit(ln(-1 + x^2), x, 0)", "i pi")] + [InlineData("limit(x^(1/3), x, -8)", "-2")] + [InlineData("limitright(x^x, x, -1)", "-1")] + [InlineData("limitright(ln(x^x), x, -1/2)", "ln((-1/2)^(-1/2))")] + [InlineData("limit(ln(sin(x) - 2), x, 0)", "ln(-2)")] + public void AlongTheRealLineItIsTheValue(string limit, string value) + => Assert.Equal(value.ToEntity().Evaled, limit.ToEntity().Simplify().Evaled); + } +}