From 5fd21953664e98797b1f194cd07658a9b03ecb44 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Wed, 21 Oct 2026 17:47:37 +0000 Subject: [PATCH] Read a limit at -oo on the approach it is rewritten to, and simplify without the memo while one is read lim x->-oo ln(x) + ln(x + 1) - ln(x^2 + x) was 0, on 2.5.0 and on master. 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. The limit machinery gathers ln(a) + ln(b) into ln(a b) where a and b hold a positive sign on the approach, which it reads from the approach in scope. SolveByRewriting reads a destination of -oo as +oo with -x written for x, and then simplified the rewritten expression while the approach in scope still said -oo. Against -oo, -x and 1 - x are positive, so ln(-x) + ln(1 - x) was gathered, and it cancelled against ln(x^2 - x). The rewriting now runs with +oo in scope, as the Gruntz branch and SimplifyAndComputeLimitToInfinity already did. And a nested Simplify is no longer looked up in what an earlier one answered while an approach is being read. The memo is keyed on the tree, the level and the settings, and on an approach a simplification can gather logarithms it does not gather on its own account, so one tree has an answer per approach. Through Simplify of a limit node master happened to answer 2 ln(-1) here, because the memo carried an answer across; with the memo bypassed and the limit unfixed it came back 0. Measured on builds of v2.5.0, master 6e16aae0 and this change: the four limits in the new test that were 0 are now 2 pi i (or its square), and the six that were right are unchanged, through both Limit and Simplify. Without the SolveByRewriting change, those four fail and the rest pass. Indefinite integration never reads an approach, so the bypass does not reach it. Fixes #2022. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 21 ++++++++ .../Continuous/Limits/Transformations.cs | 24 +++++++++ .../Functions/Simplification/Simplificator.cs | 9 ++-- ...ogarithmsGatheredOnTheirOwnApproachTest.cs | 53 +++++++++++++++++++ 4 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 Sources/Tests/UnitTests/Calculus/LogarithmsGatheredOnTheirOwnApproachTest.cs 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}"); + } + } +}