diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index dabd21a22..3ae77e481 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -287,6 +287,21 @@ for `u` made a matrix of vectors. A lambda written in place is now applied befor | `apply(lambda(u, [[0, -1], [1, 0]] * u), [p, q])` | `[[0, [-p, -q]], [[p, q], 0]]` | `[-q, p]` | | `apply(lambda(u, [1, 1]T * u), [p, q])` | `[[[p, q], [p, q]]]` | `p + q` | +### l'Hopital's rule is not applied across the jumps of a step function + +**Silent: a limit of a quotient with a `floor`, `ceil`, `round` or `sgn` that jumps in every neighbourhood of the +destination is left unevaluated, where l'Hopital's rule answered it wrongly.** The rule needs both parts differentiable +near the destination, and the derivative of `floor(x)` is `0` only between the integers, so `floor(x) / x` at `+oo` read +`0 / 1`. Where a step's argument tends to a finite value the step is constant on either side, and the rule is applied as +before ([#2281](https://github.com/asc-community/AngouriMath/issues/2281)). + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `"limit(floor(x) / x, x, +oo)".ToEntity().Simplify()` | `0` | `limit(floor(x) / x, x, +oo)`; the limit is 1 | +| `"limit(ln(floor(x)) / ln(x), x, +oo)".ToEntity().Simplify()` | `0` | unevaluated; the limit is 1 | +| `"limit(ln(x) / ln(floor(x)), x, +oo)".ToEntity().Simplify()` | `NaN` | unevaluated; the limit is 1 | +| `"limit((x - floor(x)) / x, x, +oo)".ToEntity().Simplify()` | `1` | unevaluated; the limit is 0 | + ### One and a squared tangent are found anywhere in a sum **Silent: an identity reaches its value whichever way the sum is sorted.** `1 + tan(x)^2 = sec(x)^2` and diff --git a/Sources/.editorconfig b/Sources/.editorconfig index 96e41cc4c..9078bcd7c 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -123,6 +123,9 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed [Tests/UnitTests/Calculus/LogarithmOfARationalFunctionByPartsTest.cs] file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n +[Tests/UnitTests/Calculus/LHopitalAcrossAJumpTest.cs] +file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n + [AngouriMath/Functions/Algebra/Polynomials/FractionFreeDeterminant.cs] file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Solvers.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Solvers.cs index 06a5ab1db..cc6a45554 100644 --- a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Solvers.cs +++ b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Solvers.cs @@ -259,6 +259,12 @@ private static bool IsRealValued(Entity expr, Variable x) && (lowerLogLimit.Nodes.Any(child => child == Real.PositiveInfinity || child == Real.NegativeInfinity) || lowerLogLimit == Integer.Zero)) { + // Nor through a step that jumps all the way in, for the reason l'Hopital's rule + // itself declines it: ln(floor(x)) / ln(x) answered 0. + // https://github.com/asc-community/AngouriMath/issues/2281 + if (LimitFunctional.HasAStepThatJumpsNear(upperLogArgument, x, Real.PositiveInfinity, ApproachFrom.BothSides) + || LimitFunctional.HasAStepThatJumpsNear(lowerLogArgument, x, Real.PositiveInfinity, ApproachFrom.BothSides)) + return null; var upperDerivative = upperLogArgument.Differentiate(x).InnerSimplified; var lowerDerivative = lowerLogArgument.Differentiate(x).InnerSimplified; // A derivative the library cannot compute comes back *unevaluated* rather than diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs index 46d3584a0..f6e65bf51 100644 --- a/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs +++ b/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs @@ -1370,6 +1370,34 @@ private static bool IsIndeterminateQuotient(Entity numerator, Entity denominator finally { lHopitalDepth--; } } + /// + /// Whether a step function in jumps in every neighbourhood of the + /// destination: a floor, a ceil or a round whose argument does not tend to a finite value, + /// or a sign whose argument has no limit, and so may cross zero all the way in. + /// + /// + /// l'Hopital's rule needs both parts differentiable on a punctured neighbourhood of the + /// destination. The derivative of floor(x) is 0 provided not x in ZZ, and every + /// neighbourhood of +oo holds integers, so floor(x) / x at +oo read 0 / 1 and answered 0, + /// where it is 1. Where the argument tends to a finite value the step is constant on + /// either side of the destination, and the rule holds there. + /// #2281 + /// + internal static bool HasAStepThatJumpsNear(Entity expr, Variable x, Entity dest, ApproachFrom side) + { + return expr.Nodes.Any(node => node switch + { + Floorf(var argument) => DoesNotSettle(argument), + Ceilf(var argument) => DoesNotSettle(argument), + Roundf(var argument) => DoesNotSettle(argument), + Signumf(var argument) => argument.ContainsNode(x) + && EvalAssumingContinuous(argument.Limit(x, dest, side)) is not Real { IsNaN: false }, + _ => false + }); + bool DoesNotSettle(Entity argument) => argument.ContainsNode(x) + && EvalAssumingContinuous(argument.Limit(x, dest, side)) is not Real { IsFinite: true }; + } + private static Entity? ApplylHopitalRuleImpl(Entity expr, Variable x, Entity dest, ApproachFrom side) { if (expr is not Divf && AsQuotient(expr, x, dest, side) is { } quotient) @@ -1379,7 +1407,8 @@ private static bool IsIndeterminateQuotient(Entity numerator, Entity denominator if (numLimit == 0 && denLimit == 0 || IsInfiniteNode(numLimit) && IsInfiniteNode(denLimit)) if (num is not Number && den is not Number) - if (num.ContainsNode(x) && den.ContainsNode(x)) + if (num.ContainsNode(x) && den.ContainsNode(x) + && !HasAStepThatJumpsNear(expr, x, dest, side)) { // Simplified, because the shape of the quotient is what the // machinery below matches on and differentiation does not diff --git a/Sources/Tests/UnitTests/Calculus/LHopitalAcrossAJumpTest.cs b/Sources/Tests/UnitTests/Calculus/LHopitalAcrossAJumpTest.cs new file mode 100644 index 000000000..83b8546ac --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/LHopitalAcrossAJumpTest.cs @@ -0,0 +1,49 @@ +// +// 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.Linq; +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Calculus +{ + /// + /// l'Hopital's rule is not applied across the jumps of a step function. It needs both parts differentiable on a punctured + /// neighbourhood of the destination, the derivative of floor(x) is 0 provided not x in ZZ, and every neighbourhood of +oo + /// holds integers: floor(x) / x at +oo read 0 / 1 and answered 0, where it is 1. + /// #2281 + /// + [Trait("Area", "Calculus")] + public sealed class LHopitalAcrossAJumpTest + { + [Theory] + [InlineData("limit(floor(x) / x, x, +oo)", "1")] + [InlineData("limit(ceil(x) / x, x, +oo)", "1")] + [InlineData("limit(floor(2x) / x, x, +oo)", "2")] + [InlineData("limit(floor(x)^2 / x^2, x, +oo)", "1")] + [InlineData("limit(ln(floor(x)) / ln(x), x, +oo)", "1")] + [InlineData("limit(ln(x) / ln(floor(x)), x, +oo)", "1")] + [InlineData("limit((x - floor(x)) / x, x, +oo)", "0")] + [InlineData("limitright(x * floor(1/x), x, 0)", "1")] + public void ItIsTheLimitOrNothing(string limit, string value) + { + var answer = limit.ToEntity().Simplify(); + Assert.True(answer.Nodes.Any(node => node is Entity.Limitf) || answer.Evaled == value.ToEntity().Evaled, + $"{limit} is {answer}, where it is {value}"); + } + + /// Where a step's argument settles, the step is constant on either side, and the rule holds. + [Theory] + [InlineData("limit(floor(x + 1/2) * sin(x) / x, x, 0)", "0")] + [InlineData("limit((ceil(x + 1/2) + 1) * sin(x) / x, x, 0)", "2")] + [InlineData("limit(sgn(x + 1) * (1 - cos(x)) / x^2, x, 0)", "1/2")] + [InlineData("limit(sin(x) / x, x, 0)", "1")] + [InlineData("limit(ln(x) / x, x, +oo)", "0")] + public void WhereTheStepSettlesItIsAnswered(string limit, string value) + => Assert.Equal(value.ToEntity().Evaled, limit.ToEntity().Simplify().Evaled); + } +}