From e3cc3721c958c2d0a5803d9809b5c4a2bd333006 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Fri, 30 Oct 2026 21:35:10 +0000 Subject: [PATCH] l'Hopital's rule is not applied across the jumps of a step function limit(floor(x) / x, x, +oo) was 0, where it is 1, and so was limit(ln(floor(x)) / ln(x), x, +oo). 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, but the rule stripped the condition and read 0 / 1. SolveAsLogarithmDivision's own use of the rule did the same. HasAStepThatJumpsNear asks whether a floor, ceil or round has an argument that does not tend to a finite value, or a sign one that has no limit, and both uses of the rule decline there. Where the argument settles, the step is constant on either side of the destination and the rule is applied as before. LHopitalAcrossAJumpTest takes eight limits the rule answered wrongly, which must now be right or unevaluated, and five where a step settles, which are answered as before; the eight fail on master. The suite, the trio, F#, AOT and the eight harnesses pass. limit(round(x) / x, x, +oo) still throws, because round(+oo) does (#2282). Fixes #2281. Co-Authored-By: Claude Opus 5.5 (1M context) --- BREAKING-CHANGES.md | 15 ++++++ Sources/.editorconfig | 3 ++ .../Limits/Solvers/Limit.Solvers.cs | 6 +++ .../Continuous/Limits/Transformations.cs | 31 +++++++++++- .../Calculus/LHopitalAcrossAJumpTest.cs | 49 +++++++++++++++++++ 5 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 Sources/Tests/UnitTests/Calculus/LHopitalAcrossAJumpTest.cs 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); + } +}