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}");
+ }
+ }
+}