diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md
index 5f1c2ff46..7cf8b322b 100644
--- a/BREAKING-CHANGES.md
+++ b/BREAKING-CHANGES.md
@@ -512,6 +512,21 @@ positive on both sides. `x^x` is still `NaN` at 0.
| `"(x^2)^x".ToEntity().Limit("x", 0)` | `NaN` | `1` |
| `"(x*x)^(x - 1)".ToEntity().Limit("x", 0)` | `NaN` | `+oo` |
+### A factor continuous at the point is taken out before l'Hopital's rule
+
+**`sin(x)^tanh(x)` at 0 from the right is `1`, where it was left unevaluated.** Read through its exponent, the power is
+a quotient l'Hopital's rule takes. The rule differentiated every factor of both parts, including the ones that tend to
+a number other than 0, such as the `1 - tanh(x)^2` the derivative of tanh brings. With tanh written through
+exponentials, each such factor doubled the derivative, and the result outgrew the rule's bound. A factor that is
+continuous at a finite point and has a non-zero real value there is now taken out as that value, kept exact:
+lim c(x) f(x) / g(x) is c(a) lim f(x) / g(x).
+
+| Input | Was (2.6.0) | Now |
+|---|---|---|
+| `"sin(x)^tanh(x)".ToEntity().Limit("x", 0, ApproachFrom.Right)` | `limitright(sin(x) ^ ((e ^ (2 * x) - 1) / (e ^ (2 * x) + 1)), x, 0)` | `1` |
+| `"tan(x)^sin(tanh(x))".ToEntity().Limit("x", 0, ApproachFrom.Right)` | `limitright(tan(x) ^ sin((e ^ (2 * x) - 1) / (e ^ (2 * x) + 1)), x, 0)` | `1` |
+| `"tan(2*x)^sin(tanh(x))".ToEntity().Limit("x", 0, ApproachFrom.Right)` | did not finish in 2 minutes; unevaluated after 13 s on the previous master | `1` |
+
### A 0^0 limit whose l'Hopital chain doubles its powers has its value
**Silent: a different answer.** l'Hopital's rule was turned away where a
diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs
index 2db75a3b9..69faf2f5a 100644
--- a/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs
+++ b/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs
@@ -1573,6 +1573,42 @@ bool DoesNotSettle(Entity argument) => argument.ContainsNode(x)
&& EvalAssumingContinuous(argument.Limit(x, dest, side)) is not Real { IsFinite: true };
}
+ ///
+ /// The factors of that are continuous at a finite destination with a
+ /// finite non-zero value there, as that value, and the product of the rest.
+ ///
+ ///
+ /// l'Hopital's rule differentiates both parts of the quotient, and a factor that tends to a
+ /// number other than 0 only makes the derivative larger: lim c(x) f(x) / g(x) is c(a) lim
+ /// f(x) / g(x). The product rule doubles it each time round, and with tanh written through
+ /// exponentials (1 - tanh(x)^2) * x * (1 + x^2) / tanh(x) at 0 from the right did not finish
+ /// in a minute, where x * (1 + x^2) / tanh(x) takes a tenth of a second. Only a factor real
+ /// for real x and with no step in it, so that its value at the point is its limit from
+ /// either side.
+ ///
+ private static (Entity Value, Entity Others) TakeOutContinuousFactors(Entity expr, Variable x, Entity dest)
+ {
+ if (expr is not Mulf || !dest.IsFinite)
+ return (Integer.One, expr);
+ Entity value = Integer.One, rest = Integer.One;
+ var any = false;
+ foreach (var factor in Mulf.LinearChildren(expr))
+ {
+ if (factor.ContainsNode(x)
+ && !factor.Nodes.Any(static node => node is Floorf or Ceilf or Roundf or Signumf or Piecewise)
+ && TreeAnalyzer.IsRealValued(factor, x)
+ && factor.Substitute(x, dest) is var there
+ && there.Evaled is Real { IsFinite: true, IsNaN: false, IsZero: false })
+ {
+ value *= there;
+ any = true;
+ }
+ else
+ rest *= factor;
+ }
+ return any && rest.ContainsNode(x) ? (value.InnerSimplified, rest.InnerSimplified) : (Integer.One, expr);
+ }
+
private static Entity? ApplylHopitalRuleImpl(Entity expr, Variable x, Entity dest, ApproachFrom side)
{
if (expr is not Divf && AsQuotient(expr, x, dest, side) is { } quotient)
@@ -1585,12 +1621,15 @@ bool DoesNotSettle(Entity argument) => argument.ContainsNode(x)
if (num.ContainsNode(x) && den.ContainsNode(x)
&& !HasAStepThatJumpsNear(expr, x, dest, side))
{
+ var (numValue, numRest) = TakeOutContinuousFactors(num, x, dest);
+ var (denValue, denRest) = TakeOutContinuousFactors(den, x, dest);
+ var taken = (numValue / denValue).InnerSimplified;
// Simplified, because the shape of the quotient is what the
// machinery below matches on and differentiation does not
// produce it: d/dx ln(x)^2 is 2 * ln(x) * (1 / x), a product
// with a quotient inside rather than the quotient
// 2 * ln(x) / x, and only the second has a limit here.
- var applied = (num.Differentiate(x) / den.Differentiate(x)).Simplify();
+ var applied = (numRest.Differentiate(x) / denRest.Differentiate(x)).Simplify();
// The domain condition simplification leaves behind is what the
// derivative of a logarithm or a root carries, and a Providedf is
// not a continuous node, so the quotient would be turned away
@@ -1614,18 +1653,25 @@ bool DoesNotSettle(Entity argument) => argument.ContainsNode(x)
// was turned away, so cos(x) * sinh(x) / sin(x) from the right at 0 was
// left unevaluated, and ln(sin(x)) / ln(sinh(x)) with it.
if (GrewTooMuch(expr, applied))
- return ItsValueAt(applied, x, dest);
+ return ItsValueAt(applied, x, dest) is { } value ? Times(taken, value) : null;
lHopitalApplications++;
MultithreadingFunctional.ExitIfCancelled();
lHopitalChain!.Add(applied);
try
{
if (ComputeLimit(applied, x, dest, side) is { } resLim)
- return resLim;
+ return Times(taken, resLim);
}
finally { lHopitalChain.RemoveAt(lHopitalChain.Count - 1); }
}
return null;
+ // A limit times the value taken out: unchanged where nothing was, exact where the limit is
+ // finite, and the infinity itself where it is one, since a finite non-zero real times +oo
+ // is +oo or -oo and a caller compares against that.
+ static Entity Times(Entity taken, Entity limit)
+ => taken == Integer.One ? limit
+ : limit.Evaled is Real { IsFinite: false, IsNaN: false } && (taken * limit).Evaled is Real { IsNaN: false } infinity ? infinity
+ : (taken * limit).InnerSimplified;
}
///
diff --git a/Sources/Tests/UnitTests/Calculus/AContinuousFactorIsNotDifferentiatedTest.cs b/Sources/Tests/UnitTests/Calculus/AContinuousFactorIsNotDifferentiatedTest.cs
new file mode 100644
index 000000000..4f9506131
--- /dev/null
+++ b/Sources/Tests/UnitTests/Calculus/AContinuousFactorIsNotDifferentiatedTest.cs
@@ -0,0 +1,47 @@
+//
+// 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 AngouriMath.Core;
+using Xunit;
+
+namespace AngouriMath.Tests.Calculus
+{
+ ///
+ /// sin(x)^tanh(x) at 0 from the right was left unevaluated, where it is 1, and tan(2x)^sin(tanh(x))
+ /// declined after thirteen seconds. Read through its exponent, the power is a quotient l'Hopital's rule takes, and the
+ /// rule differentiated every factor of both parts, including those that tend to a number other than 0, such as the
+ /// 1 - tanh(x)^2 the derivative of tanh brings. With tanh written through exponentials, each such factor
+ /// doubled the derivative, which outgrew the rule's bound. A factor that is continuous at a finite point with a non-zero
+ /// value there is now taken out as that value: lim c(x) f(x) / g(x) is c(a) lim f(x) / g(x). Found by limitcheck at
+ /// DEPTH=3, SEED=42; no issue was filed.
+ ///
+ [Trait("Area", "Calculus")]
+ public sealed class AContinuousFactorIsNotDifferentiatedTest
+ {
+ [Theory]
+ [InlineData("sin(x)^tanh(x)")]
+ [InlineData("tan(x)^sin(tanh(x))")]
+ [InlineData("tan(2*x)^sin(tanh(x))")]
+ public void IsOneFromTheRight(string input)
+ => Assert.Equal(MathS.FromString("1"), MathS.FromString(input).Limit("x", 0, ApproachFrom.Right));
+
+ // The value taken out is kept exact, and an infinity it multiplies is still that infinity.
+ [Theory]
+ [InlineData("tanh(1 + x) * (x - 1) / (x^2 - 1)", "1", "tanh(2) / 2")]
+ [InlineData("(2 + cos(x)) * x / sin(x)", "0", "3")]
+ [InlineData("(2 + cos(x)) / sin(x)^2", "0", "+oo")]
+ public void KeepsTheValueTakenOut(string input, string destination, string expected)
+ {
+ var limit = MathS.FromString(input).Limit("x", destination);
+ var wanted = MathS.FromString(expected);
+ if (wanted.Evaled is Entity.Number { IsFinite: false })
+ Assert.Equal(wanted, limit);
+ else
+ Assert.Equal(Entity.Number.Integer.Zero, (limit - wanted).Simplify());
+ }
+ }
+}