From 74db309d4976348fd84588f4a023435e513838aa Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Fri, 30 Oct 2026 21:17:54 +0000 Subject: [PATCH] A limit on a logarithm's cut is not read off the principal value limitright(ln(x^x), x, -1) was ln(-1), which is i pi, where it is -i pi: x^x comes to -1 from below as x comes down to -1, and the principal logarithm is continuous on its cut from above only. The value on the cut is the limit along the real line and from above, so it was given as the limit from either side. ArrivesOnTheCutOffTheRealLine asks whether a logarithm's argument, or the base of a power to an exponent that is not whole, tends to a negative real and is not known to be real on the approach; a power's base and a logarithm's argument count as positive where their values at the destination are. Which side such an argument comes from is not decided, so a limit there is declined: by substitution, by the logarithm's and the power's descent, by SolveAsLogarithm, by the quotient of logarithms, and by Gruntz, whose series took the principal logarithm of a negative coefficient. A limit from above, whose answer was right, is declined too. LimitOnABranchCutTest takes eight limits from below the cut, three two-sided ones that do not exist, and nine along the real line, which are answered as before; eleven fail on master. The suite, the trio, F#, AOT and the eight harnesses pass. Composed with #2268, #2275 and #2276, work/limitcheck finds none of the 56 limits it found answered with another value on master, of 600 expressions. Part of #2266. Co-Authored-By: Claude Opus 5.5 (1M context) --- BREAKING-CHANGES.md | 18 +++++ Sources/.editorconfig | 3 + .../Continuous/Limits/Gruntz/Gruntz.cs | 7 ++ .../Limits/Solvers/Limit.Classes.cs | 19 ++++- .../Limits/Solvers/Limit.Solvers.cs | 8 +++ .../Continuous/Limits/Transformations.cs | 70 +++++++++++++++++++ .../Calculus/LimitOnABranchCutTest.cs | 68 ++++++++++++++++++ 7 files changed, 190 insertions(+), 3 deletions(-) create mode 100644 Sources/Tests/UnitTests/Calculus/LimitOnABranchCutTest.cs 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); + } +}