diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 897bd22df..a99813675 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -7914,6 +7914,27 @@ arctangent, arccotangent or arccosecant whose argument diverges, which is now an | `integral(1/(2 + cos(x)), x, 0, pi)` | `integral(1 / (2 + cos(x)), x, 0, pi)` | `sqrt(3) * pi / 3` | | `limitleft(arctan(tan(x / 2)), x, pi)` | `limitleft(arctan(tan(x / 2)), x, pi)` | `pi / 2` | +### A limit at -oo keeps the `i pi` of each logarithm of a negative operand + +**Wrong answers fixed.** `lim ln(x) + ln(x + 1) - ln(x^2 + x)` as `x → -∞` was `0`. Both `x` and +`x + 1` are negative there, so each of the first two logarithms carries `i pi` and the limit is +`2 pi i`. A limit at `-oo` is read at `+oo` with `-x` written for `x`, and the logarithms of +operands holding one sign on the approach are gathered into one; the rewritten expression was +simplified while the approach still said `-oo`, where `-x` and `1 - x` are positive, and its two +logarithms were gathered. It is now read on the approach it is on. A nested simplification is also +no longer answered from what an earlier one returned while a limit is being read, since it can +gather logarithms there that it does not gather on its own account +([#2022](https://github.com/asc-community/AngouriMath/issues/2022)). Both columns measured on a +build, `v2.5.0` against this change. + +| `"….".ToEntity().Limit("x", "-oo")` of | Was (2.5.0) | Is | +|---|---|---| +| `ln(x) + ln(x + 1) - ln(x ^ 2 + x)` | `0` | `ln(-1) + ln(-1)` | +| `ln(x) + ln(x - 1) - ln(x ^ 2 - x)` | `0` | `ln(-1) + ln(-1)` | +| `(ln(x) + ln(x + 1) - ln(x ^ 2 + x)) * x / (x + 1)` | `0` | `ln(-1) + ln(-1)` | +| `(ln(x) + ln(x + 1) - ln(x ^ 2 + x)) ^ 2` | `0` | `(ln(-1) + ln(-1)) ^ 2` | +| `ln(-x) + ln(-x - 1) - ln(x ^ 2 + x)` | `0` | the same | + ### A limit at infinity whose terms cancel around a slowly varying factor is answered `lim e^x (exp(1/x + e^(-x)) - exp(1/x))` as `x → ∞` is the example Gonnet and Gruntz introduce diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs index b83ca033e..46d3584a0 100644 --- a/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs +++ b/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs @@ -311,6 +311,13 @@ private static int MemoisedSign(Entity expr, Approach approach) [System.ThreadStatic] private static int approachDepth; + /// + /// Whether an approach is being read, and so whether a simplification can answer + /// otherwise than it would on its own account: and + /// fire only then. + /// + internal static bool IsReadingAnApproach => currentApproach is not null; + /// /// States that is being read on its way to , /// and returns the previous approach for to put back. @@ -1435,6 +1442,23 @@ private static bool IsIndeterminateQuotient(Entity numerator, Entity denominator // direction only. if (dest.Evaled is Real { IsNegative: true }) expr = expr.Substitute(x, -x); + // Everything below reads the expression on its way to +oo, which after the + // substitution is where it is going -- while the approach in scope is still the one + // the limit was asked for. A sign read against -oo of an expression written in -x is + // the opposite sign: the simplifier took -x and 1 - x for positive, gathered + // ln(-x) + ln(1 - x) into one logarithm, and lim x->-oo ln(x) + ln(x + 1) - ln(x^2 + x) + // came back 0 where it is 2 pi i. + // https://github.com/asc-community/AngouriMath/issues/2022 + var outerApproach = EnterApproach(x, Real.PositiveInfinity); + try { return SolveByRewritingTowardsInfinity(expr, x); } + finally { LeaveApproach(outerApproach); } + } + + /// + /// once the destination is +oo. + /// + private static Entity? SolveByRewritingTowardsInfinity(Entity expr, Variable x) + { var toInfinity = Real.PositiveInfinity; var simplified = expr.Simplify(); if (simplified is Providedf(var body, _)) diff --git a/Sources/AngouriMath/Functions/Simplification/Simplificator.cs b/Sources/AngouriMath/Functions/Simplification/Simplificator.cs index 00809b742..757254203 100644 --- a/Sources/AngouriMath/Functions/Simplification/Simplificator.cs +++ b/Sources/AngouriMath/Functions/Simplification/Simplificator.cs @@ -46,8 +46,11 @@ internal static Entity Simplify(Entity expr, int level) try { // A nested run is looked up in what the runs before it answered; the outermost - // one has no runs before it. See simplifications. - if (simplifying == 1 || RewriteRecording.Current is not null || !expr.Nodes.All(IsElementary)) + // one has no runs before it. See simplifications. Not while a limit reads an + // approach, on which logarithms gather that do not gather on their own account: + // the same tree simplified on two approaches, or on one and on none, is two answers. + if (simplifying == 1 || RewriteRecording.Current is not null || !expr.Nodes.All(IsElementary) + || Algebra.LimitFunctional.IsReadingAnApproach) return SimplifyAtDepth(expr, level); var kept = simplifications ??= new(); if (kept.TryGetValue((expr, level), out var known) && Convenience.SettingsState.StillHolds(known.Under)) @@ -148,7 +151,7 @@ internal static Entity Expanded(Entity expr) return expanded; } - /// A node whose simplification reads nothing but its children and the settings. + /// A node whose simplification reads nothing but its children and the settings, outside a limit's approach. private static bool IsElementary(Entity node) => node is Number or Variable or Sumf or Minusf or Mulf or Divf or Powf or Logf or TrigonometricFunction or Absf or Signumf; diff --git a/Sources/Tests/UnitTests/Calculus/LogarithmsGatheredOnTheirOwnApproachTest.cs b/Sources/Tests/UnitTests/Calculus/LogarithmsGatheredOnTheirOwnApproachTest.cs new file mode 100644 index 000000000..8baa85062 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/LogarithmsGatheredOnTheirOwnApproachTest.cs @@ -0,0 +1,53 @@ +// +// 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; +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Calculus +{ + /// + /// A limit at -oo is read at +oo with -x written for x, and the logarithms a limit gathers are + /// gathered on the signs their operands hold on the approach. Those signs have to be read on + /// the approach the rewritten expression is on: read against -oo, -x and 1 - x are positive, + /// ln(-x) + ln(1 - x) was gathered into one logarithm, and + /// lim x->-oo ln(x) + ln(x + 1) - ln(x^2 + x) came back 0. Both operands are negative + /// there, so each logarithm carries i pi and the limit is 2 pi i. + /// #2022 + /// + [Trait("Area", "Calculus")] + public sealed class LogarithmsGatheredOnTheirOwnApproachTest + { + [Theory] + [InlineData("ln(x) + ln(x + 1) - ln(x^2 + x)", "-oo", "2 * i * pi")] + [InlineData("ln(x) + ln(x - 1) - ln(x^2 - x)", "-oo", "2 * i * pi")] + [InlineData("(ln(x) + ln(x + 1) - ln(x^2 + x)) * x / (x + 1)", "-oo", "2 * i * pi")] + [InlineData("(ln(x) + ln(x + 1) - ln(x^2 + x))^2", "-oo", "-4 * pi^2")] + [InlineData("ln(-x) + ln(1 - x) - ln(x^2 - x)", "+oo", "2 * i * pi")] + // Where the operands are positive the logarithms do gather, on either side. + [InlineData("ln(-x) + ln(-x - 1) - ln(x^2 + x)", "-oo", "0")] + [InlineData("ln(-x) + ln(1 - x) - ln(x^2 - x)", "-oo", "0")] + [InlineData("ln(x) + ln(x + 1) - ln(x^2 + x)", "+oo", "0")] + [InlineData("ln(x^2) - 2 * ln(x)", "-oo", "-2 * i * pi")] + [InlineData("ln(x - 1) + ln(x - 2) - ln(x^2 - 3 * x + 2)", "0", "2 * i * pi")] + public void IsTheLimitOfTheLogarithmsAsTheyAre(string expression, string dest, string limit) + { + var want = limit.ToEntity().EvalNumerical(); + AssertIs(want, expression.ToEntity().Limit("x", dest), expression, dest, "Limit"); + AssertIs(want, $"limit({expression}, x, {dest})".ToEntity().Simplify(), expression, dest, "Simplify"); + } + + private static void AssertIs(Entity.Number.Complex want, Entity limit, string expression, string dest, string how) + { + Assert.DoesNotContain("limit(", limit.Stringize()); + var got = limit.EvalNumerical(); + var difference = Math.Abs((double)(got - want).RealPart) + Math.Abs((double)(got - want).ImaginaryPart); + Assert.True(difference < 1e-9, $"{how}: lim x->{dest} {expression} is {limit} = {got}, where it is {want}"); + } + } +}