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