Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ read first.
| **silent** | numeric root sets | one root per starting point | one root per root |
| **silent** | many limits | `NaN`, or unevaluated | a value |
| **silent** | a vanishing sine behind a constant factor | `NaN` | a value — but `sin(x)*ln(x)*2` loses its `0` |
| **silent** | a limit assembling `oo^0` or `1^oo` | that form's value, `1` | the real answer, or unevaluated |
| **silent** | some integrals | not antiderivatives | closed forms |
| **silent** | two integrals | answered correctly | unevaluated — a deliberate loss |
| loud | `Compile` over a missing variable | `KeyNotFoundException` | `UncompilableNodeException` |
Expand Down Expand Up @@ -801,6 +802,53 @@ already `NaN` — and no answer changed into a different answer.
[#749](https://github.com/asc-community/AngouriMath/issues/749), PR
[#759](https://github.com/asc-community/AngouriMath/pull/759).

### An indeterminate power form is no longer read off as its value

A limit was answered, where nothing else had a reading of it, by substituting the destination and
evaluating. That is right wherever the expression is continuous there, and an indeterminate form is
exactly where it is not. Almost all of them already declined, because they evaluate to `NaN` and
every caller reads `NaN` as "no limit" — `0 * oo`, `oo - oo`, `oo / oo`, `0^0`. The two that do not
are `oo^0` and `1^oo`, which this library's arithmetic answers with `1`, so a limit that assembled
either read that `1` off as its answer:

```
lim x->+oo (e^x) ^ (1/ln(x)) was 1 is +oo
lim x->+oo (x^2) ^ (1/ln(x)) was 1 is e^2
lim x->+oo (x!) ^ (1/x) was 1 is unevaluated (it is +oo)
lim x->+oo (x!) ^ (1/ln(x)) was 1 is unevaluated (it is +oo)
```

`(x^2)^(1/ln x)` is `e^(2*ln(x)/ln(x))`, that is `e^2` at every `x`, so the old answer was not merely
imprecise. `(x!)^(1/x)` grows like `x/e` by Stirling — `(100!)^(1/100)` is already `37.99`.

This does **not** change what `(+oo)^0` or `1^oo` evaluate to as expressions. Both are still `1`, the
same convention SymPy and IEEE 754's `pow` use; the change is that a *limit* no longer reads its
answer off one. `0^0` is deliberately untouched for the opposite reason: its `NaN` is how the library
says `lim x->0 x^x` does not exist, which `LimitTest.TestNoLimit` pins, and declining it would turn a
considered "does not exist" into "not settled".

**What it costs.** Three limits that were answered `1` are now unevaluated, because the substitution
that used to land on them is gone and nothing else has a reading:

```
lim x->+oo (x!) ^ (1/x^2) was 1 is unevaluated (it is 1)
lim x->0 (1/x) ^ x was 1 is unevaluated
lim x->0 (1/x) ^ (x^2) was 1 is unevaluated
```

The first was right by luck: the same substitution gave `1` for `(x!)^(1/x)`, where the answer is
`+oo`, and the two cannot be told apart at the point where the value is read off. Answering it
properly wants Stirling's expansion of `ln(x!)`, which is the second half of #754. The other two are
two-sided limits at `0` whose base has no two-sided limit at all and which are not real to the left
of `0`, so the `1` came from the complex continuation; **their one-sided readings are unchanged** —
`lim x->0+ (1/x)^x` is still `1`.

Measured over 225 generated powers at `0` and `+oo`: 2 wrong values became right ones, 3 wrong values
became unevaluated, 1 `NaN` became unevaluated, and 3 right values became unevaluated.

[#754](https://github.com/asc-community/AngouriMath/issues/754), PR
[#760](https://github.com/asc-community/AngouriMath/pull/760).

### `lim x->2 signum(x)` no longer kills the process

`Signumf` was the one node whose limit override handed back an unevaluated limit of the very
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,9 +246,11 @@ partial record Logf

partial record Powf
{
internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side) =>
ComputeLimitImpl(this, x, dist, side) is { } lim ? lim
: ComputeLimitImpl(
internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side)
{
if (ComputeLimitImpl(this, x, dist, side) is { } lim)
return lim;
var substituted =
(Base.ComputeLimitDivideEtImpera(x, dist, side), Exponent.ComputeLimitDivideEtImpera(x, dist, side))
switch {
({ IsFinite: true } lim1, { IsFinite: true } lim2) => New(lim1, lim2),
Expand All @@ -257,8 +259,21 @@ partial record Powf
({ IsFinite: true } lim1, { } exp) => New(lim1, exp),
({ } bas, { IsFinite: true } lim2) => New(bas, lim2),
_ => New(Base, Exponent)
},
x, dist, side);
};
// Putting each part's own limit in place of the part leaves a power that says
// nothing whenever the two of them meet in an indeterminate form. The sum, the
// product and the quotient are already guarded against this by IsDeterminate
// above; the power was not, so lim x->+oo (x!)^(1/x) assembled (+oo)!^0 and
// read 1 off it, where the limit is +oo. Declining hands the expression to the
// rules behind the descent, and leaves it unevaluated -- "not settled" --
// rather than wrong if none of them answers either.
// https://github.com/asc-community/AngouriMath/issues/754
if (substituted is Powf(var substitutedBase, var substitutedExponent)
&& !substituted.ContainsNode(x)
&& IsIndeterminatePowerForm(substitutedBase, substitutedExponent))
return null;
return ComputeLimitImpl(substituted, x, dist, side);
}
}

partial record Arcsinf
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@ internal static class LimitSolvers
{
MultithreadingFunctional.ExitIfCancelled();
if (limit == Real.NaN) return null;
// Reading the value at the destination is the limit only where the expression
// is continuous there, and an indeterminate form is exactly where it is not.
// Every other one of them already declines on the line above, because this
// library's arithmetic answers 0 * oo, oo - oo, oo / oo and 0^0 with NaN. The
// two power forms do not: (+oo)^0 and 1^oo evaluate to 1 -- the same
// convention SymPy and IEEE 754's pow use, and not one this touches -- so a
// limit that assembled one of them read that 1 off as its answer, and
// lim x->+oo (x!)^(1/x) came back 1 where it is +oo.
// https://github.com/asc-community/AngouriMath/issues/754
if (LimitFunctional.HoldsAnIndeterminatePowerForm(res)) return null;
if (!limit.RealPart.IsFinite)
return limit.RealPart; // TODO: sometimes we get { oo + value * i } so we assume it is just infinity
if (limit == Integer.Zero) return limit;
Expand Down
44 changes: 44 additions & 0 deletions Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,50 @@ private static Entity ApplySecondRemarkable(Entity expr, Variable x, Entity dest
private static bool IsInfiniteNode(Entity expr)
=> expr.ContainsNode("+oo") || expr.ContainsNode("-oo"); // TODO: is it correct?

/// <summary>
/// Whether a power assembled out of the limits of its base and its exponent is an
/// indeterminate <em>form</em> that this library's arithmetic nonetheless answers with
/// a value, so that reading the value off would answer the limit wrongly.
/// </summary>
/// <remarks>
/// Substituting each part's own limit into a node and reading the result off is right
/// wherever the arithmetic is continuous there, and an indeterminate form is exactly
/// where it is not. Nearly all of them are already declined without any of this,
/// because they evaluate to NaN and every caller treats NaN as "no limit":
/// <c>0 * oo</c>, <c>oo - oo</c>, <c>oo / oo</c> and -- the one that matters here --
/// <c>0^0</c>. That last is why <c>lim x-&gt;0 x^x</c> is NaN, which this library means
/// deliberately and pins in <c>LimitTest.TestNoLimit</c>, so <c>0^0</c> is **not**
/// listed here: declining it would turn a considered "does not exist" into "not
/// settled".
/// <para/>
/// The gap is the two forms whose arithmetic gives 1 rather than NaN. <c>oo^0</c> and
/// <c>1^oo</c> are each the shape of limits with different values -- <c>(1 + 1/x)^x</c>
/// is <c>e</c> and <c>1^x</c> is 1, both of them <c>1^oo</c> -- and a limit that
/// assembled one of them read that 1 off as its answer:
/// <c>lim x-&gt;+oo (x!)^(1/x)</c> came back 1 where it is +oo.
/// <para/>
/// This says nothing about what the expression <c>(+oo)^0</c> evaluates to on its own.
/// That is a question of convention, on which this library agrees with SymPy and with
/// IEEE 754's <c>pow</c> in answering 1, and it is left exactly as it was.
/// <a href="https://github.com/asc-community/AngouriMath/issues/754">#754</a>
/// </remarks>
internal static bool IsIndeterminatePowerForm(Entity @base, Entity exponent)
{
var baseValue = EvalAssumingContinuous(@base);
var exponentValue = EvalAssumingContinuous(exponent);
if (exponentValue == 0)
return IsInfiniteNode(baseValue);
return baseValue == 1 && IsInfiniteNode(exponentValue);
}

/// <summary>
/// Whether an expression that a destination has already been substituted into holds an
/// indeterminate power form anywhere in it, in which case its value is not the limit.
/// </summary>
internal static bool HoldsAnIndeterminatePowerForm(Entity expr)
=> expr.Nodes.Any(node => node is Powf(var @base, var exponent)
&& IsIndeterminatePowerForm(@base, exponent));

/// <summary>
/// Whether the exponent grows without bound, which is all the second remarkable
/// limit needs.
Expand Down
182 changes: 182 additions & 0 deletions Sources/Tests/UnitTests/Calculus/IndeterminatePowerSubstitutionTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
//
// Copyright (c) 2019-2022 Angouri.
// AngouriMath is licensed under MIT.
// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.
// Website: https://am.angouri.org.
//

using AngouriMath;
using AngouriMath.Core;
using AngouriMath.Extensions;
using Xunit;

namespace AngouriMath.Tests.Calculus
{
/// <summary>
/// Reading a limit off the value at the destination is right wherever the expression is
/// continuous there, and an indeterminate form is exactly where it is not. Nearly all of
/// them decline without any help, because they evaluate to NaN and every caller reads NaN
/// as "no limit": <c>0 * oo</c>, <c>oo - oo</c>, <c>oo / oo</c>, <c>0^0</c>.
///
/// The two that do not are <c>oo^0</c> and <c>1^oo</c>, which this library's arithmetic
/// answers with 1 -- the same convention SymPy and IEEE 754's <c>pow</c> use, and one this
/// change does not touch. So a limit that assembled either of them read that 1 off as its
/// answer, and was definite about limits it had no reading of:
///
/// lim x->+oo (x!)^(1/x) was 1, is +oo
/// lim x->+oo (x!)^(1/ln(x)) was 1, is +oo
/// lim x->+oo (e^x)^(1/ln(x)) was 1, is +oo
/// lim x->+oo (x^2)^(1/ln(x)) was 1, is e^2
///
/// https://github.com/asc-community/AngouriMath/issues/754
/// </summary>
public sealed class IndeterminatePowerSubstitutionTest
{
private static Entity LimitOf(string expression, string destination) =>
expression.ToEntity().Limit("x", destination.ToEntity()).Simplify();

/// <summary>
/// The mathematics rather than the printed form, and rather than the decimal either:
/// the machinery answers <c>((x - 5)/x)^x</c> with <c>1/e^5</c> where the expectation
/// is written <c>e^(-5)</c>, and evaluating those two reaches the same number by
/// different roundings, so they disagree in the last digit while being one value.
/// </summary>
private static void AssertLimit(string expression, string destination, string expected)
{
var difference = (LimitOf(expression, destination) - expected.ToEntity()).Simplify();
while (difference is Entity.Providedf(var inner, _)) difference = inner;
Assert.Equal(Entity.Number.Integer.Create(0), difference);
}

/// <summary>
/// An infinite limit, where the difference above says nothing: (+oo) - (+oo) is NaN.
/// </summary>
private static void AssertDiverges(string expression, string destination) =>
Assert.Equal(Entity.Number.Real.PositiveInfinity, LimitOf(expression, destination).Evaled);

/// <summary>
/// Left unsettled: the expression comes back as the limit of itself, which is the
/// library's way of saying "no rule found one". Deliberately distinct from NaN, which
/// would claim the limit does not exist -- each of these has one.
/// </summary>
private static void AssertNotSettled(string expression, string destination)
{
var limit = expression.ToEntity().Limit("x", destination.ToEntity()).Evaled;
Assert.True(limit is Entity.Limitf,
$"expected lim x->{destination} {expression} to be left unsettled, got {limit}");
}

/// <summary>
/// The reported wrong answer and its relatives. Every one of these was <c>1</c>,
/// assembled as <c>(+oo)^0</c> and read off. Three are <c>+oo</c> and one is
/// <c>e^2</c>, so the <c>1</c> was not merely imprecise.
/// </summary>
[Theory]
[InlineData("(e^x) ^ (1/ln(x))")]
[InlineData("(x^2) ^ (1/ln(x))")]
[InlineData("(x!) ^ (1/x)")]
[InlineData("(x!) ^ (1/ln(x))")]
public void APowerOverALogarithmIsNoLongerRead(string expression) =>
Assert.NotEqual(Entity.Number.Integer.Create(1), LimitOf(expression, "+oo").Evaled);

/// <summary>
/// And the two of those the machinery can now finish, which it reaches through
/// <c>SolveAsIndeterminatePower</c> once the substitution stops answering first.
/// <c>(x^2)^(1/ln x)</c> is <c>e^(2*ln(x)/ln(x))</c>, that is <c>e^2</c> at every x,
/// and <c>(e^x)^(1/ln x)</c> is <c>e^(x/ln x)</c>, which diverges.
/// </summary>
[Fact]
public void ALogarithmicExponentOverAPolynomialIsAnsweredProperly() =>
AssertLimit("(x^2) ^ (1/ln(x))", "+oo", "e^2");

[Fact]
public void ALogarithmicExponentOverAnExponentialDiverges() =>
AssertDiverges("(e^x) ^ (1/ln(x))", "+oo");

/// <summary>
/// The factorial cases from the report. A factorial's derivative wants the digamma
/// function, which this library does not have, so <c>SolveAsIndeterminatePower</c>
/// declines and nothing else has a reading either -- they are left unsettled rather
/// than answered wrongly. <c>(x!)^(1/x)</c> is <c>+oo</c>: by Stirling it grows like
/// <c>x/e</c>, and <c>(100!)^(1/100)</c> is already 37.99.
/// </summary>
[Theory]
[InlineData("(x!) ^ (1/x)")]
[InlineData("(x!) ^ (1/ln(x))")]
public void AFactorialUnderAVanishingExponentIsLeftUnsettled(string expression) =>
AssertNotSettled(expression, "+oo");

/// <summary>
/// **What this costs.** <c>(x!)^(1/x^2)</c> is 1 -- its logarithm is
/// <c>ln(x!)/x^2 ~ ln(x)/x</c>, which vanishes -- and the old substitution happened to
/// land on that 1 for the same reason it landed on 1 for <c>(x!)^(1/x)</c>, where the
/// answer is <c>+oo</c>. It was right by luck rather than by reading, and the two
/// cannot be told apart at the point where the value is read off.
/// <para/>
/// Answering it properly wants Stirling's expansion of <c>ln(x!)</c>, which is the
/// second half of #754 and is not attempted here.
/// </summary>
[Fact]
public void TheFactorialCaseThatWasAccidentallyRightIsAlsoLeftUnsettled() =>
AssertNotSettled("(x!) ^ (1/x^2)", "+oo");

/// <summary>
/// <c>0^0</c> is deliberately **not** guarded. It evaluates to NaN, and that NaN is
/// how the library says <c>lim x->0 x^x</c> does not exist -- <c>x^x</c> is not real to
/// the left of 0 -- which <c>LimitTest.TestNoLimit</c> pins. Declining it would turn a
/// considered "does not exist" into "not settled".
/// </summary>
[Fact]
public void ZeroToTheZeroKeepsItsConsideredNaN()
{
var limit = "limit(x^x, x, 0)".ToEntity();
Assert.Equal(MathS.NaN, limit.InnerSimplified);
Assert.Equal(MathS.NaN, limit.Evaled);
}

/// <summary>
/// The powers that were right before and have to stay so. The first four are
/// <c>1^oo</c>, which the second remarkable limit reads before the substitution is ever
/// reached, and the rest are <c>oo^0</c> and <c>0^0</c> answered through
/// <c>SolveAsIndeterminatePower</c>. These are the ones a guard applied too eagerly
/// would silence.
/// </summary>
[Theory]
[InlineData("(1 + 1/x) ^ x", "+oo", "e")]
[InlineData("(x - 5) ^ x / x ^ x", "+oo", "e^(-5)")]
[InlineData("(1 - 1/x^2) ^ (x^2)", "+oo", "1/e")]
[InlineData("((x+1)/(x+2)) ^ x", "+oo", "1/e")]
[InlineData("(1 + x) ^ (1/x)", "0", "e")]
[InlineData("x ^ (1/x)", "+oo", "1")]
[InlineData("(1/x) ^ (1/x)", "+oo", "1")]
[InlineData("1 ^ x", "+oo", "1")]
public void ThePowersThatWereAlreadyRightAreUndisturbed(string expression, string destination, string expected) =>
AssertLimit(expression, destination, expected);

/// <summary>
/// An exponent that diverges rather than vanishing, so no indeterminate form is
/// assembled and the guard has no business firing.
/// </summary>
[Theory]
[InlineData("(1 + 1/x) ^ (x^2)")]
[InlineData("x ^ x")]
[InlineData("(x^2) ^ x")]
public void ADivergingPowerIsUntouched(string expression) =>
AssertDiverges(expression, "+oo");

/// <summary>
/// The one-sided readings of the two-sided limits this change stops answering. Both
/// are unchanged, and they are the meaningful ones: <c>lim x->0 1/x</c> has no
/// two-sided limit at all, and <c>(1/x)^x</c> is not real to the left of 0, so the
/// <c>1</c> the two-sided limit used to give came from the complex continuation by way
/// of an assembled <c>(+oo)^0</c>.
/// </summary>
[Theory]
[InlineData("(1/x) ^ x", "1")]
[InlineData("(1/x) ^ (x^2)", "1")]
[InlineData("(1/x) ^ (1/ln(x))", "1/e")]
public void TheOneSidedReadingsAreUnchanged(string expression, string expected) =>
Assert.Equal(expected.ToEntity().Evaled,
expression.ToEntity().Limit("x", 0, ApproachFrom.Right).Simplify().Evaled);
}
}
Loading