diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index ff7259513..77fa74a33 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -574,6 +574,31 @@ filed; a check of limits against values near the destination found it. The same spelled `(-ln(2)/ln(x))^(exp(x) - 1)` was `1` in 2.6.0 and did not return after it; it is `1` again. +### The error functions and the exponential and trigonometric integrals have limits + +**`erf(x)` at +oo is `1`, where it was left unevaluated.** None of erf, erfc, erfi, Si, Ci, Shi, Chi, Ei or li had a +limit at all, not even at a point where it is continuous: `erf(x)` at 1 was unevaluated too. Their limits at the +infinities are now their values there: +- erf: 1 and -1 +- erfc: 0 and 2 +- erfi and Shi: +oo and -oo +- Si: pi/2 and -pi/2 +- Ci: 0 +- Chi and li: +oo +- Ei: +oo and 0 + +A finite limit of the argument is put in where the function is continuous. For the entire ones that is anywhere. +For Ci, Chi, Ei and li it is only a positive real point, since they are singular at 0 and cut below it, and li is +singular at 1 too. + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `"erf(x)".ToEntity().Limit("x", "+oo")` | `limit(erf(x), x, +oo)` | `1` | +| `"erf(x)".ToEntity().Limit("x", 1)` | `limit(erf(x), x, 1)` | `erf(1)` | +| `"Si(x)".ToEntity().Limit("x", "+oo")` | `limit(Si(x), x, +oo)` | `pi / 2` | +| `"Ei(x)".ToEntity().Limit("x", "-oo")` | `limit(Ei(x), x, -oo)` | `0` | +| `"erf(x)/x + 1".ToEntity().Limit("x", 0)` | `limit(erf(x) / x + 1, x, 0)` | `2 / sqrt(pi) + 1` | + ### A limit of a logarithm that is not decided is left unevaluated **Not silent: what overflowed the stack now returns.** A limit of a logarithm whose argument has no limit, as diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs index d375d50ee..89551f459 100644 --- a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs +++ b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs @@ -402,6 +402,115 @@ partial record Arccosf } } + /// + /// The limit of an error function or an exponential or trigonometric integral, given its + /// argument's limit: the value it tends to at an infinity, and the function of a finite limit + /// where it is continuous there. + /// + /// + /// None of these had a limit at all, so erf(x) at +oo, which is 1, was left unevaluated, and + /// so was erf(x) at 1. A logarithm of something holding one came to a stop that took more than + /// forty seconds: log(x + pi + erf(x), x^2 + tanh(1)) at +oo, which is 2. At an infinity: + /// erf to 1 and -1, erfc to 0 and 2, erfi, Shi to +oo and -oo, Si to pi/2 and -pi/2, Ci to 0, + /// Chi and li to +oo, and Ei to +oo and 0. Erf, erfc, erfi, Si and Shi are entire, so a finite + /// limit is put in as it is. Ci, Chi, Ei and li have a logarithm's singularity at 0 and its cut + /// below it, and li another at 1, so only a positive real limit other than li's 1 is put in; + /// elsewhere nothing is claimed. Each value at an infinity was checked against mpmath at 10^6 and + /// -10^6, and at 50 and -50 for erfi, Shi and Chi. + /// + private static Entity? OfAnErrorOrIntegralFunction(Entity node, Entity argument, Variable x, Entity dist, ApproachFrom side) + { + if (argument.ComputeLimitDivideEtImpera(x, dist, side) is not { } limit) + return null; + if (limit.Evaled is Real { IsFinite: false, IsNaN: false } infinity) + { + var upwards = !infinity.IsNegative; + return node switch + { + Erff => upwards ? Integer.One : Integer.MinusOne, + Erfcf => upwards ? Integer.Zero : Integer.Create(2), + Erfif or Shif => upwards ? Real.PositiveInfinity : Real.NegativeInfinity, + Sif => upwards ? MathS.pi / 2 : -MathS.pi / 2, + Cif => upwards ? Integer.Zero : null, + Chif or Lif => upwards ? Real.PositiveInfinity : null, + Eif => upwards ? Real.PositiveInfinity : Integer.Zero, + _ => null + }; + } + if (limit.Evaled is not Number { IsFinite: true, IsNaN: false } value) + return null; + var entire = node is Erff or Erfcf or Erfif or Sif or Shif; + if (!entire && !(value is Real { IsPositive: true } && !(node is Lif && value == Integer.One))) + return null; + return node switch + { + Erff => new Erff(limit), + Erfcf => new Erfcf(limit), + Erfif => new Erfif(limit), + Sif => new Sif(limit), + Cif => new Cif(limit), + Shif => new Shif(limit), + Chif => new Chif(limit), + Eif => new Eif(limit), + Lif => new Lif(limit), + _ => null + }; + } + + partial record Erff + { + internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side) + => OfAnErrorOrIntegralFunction(this, Argument, x, dist, side); + } + + partial record Erfcf + { + internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side) + => OfAnErrorOrIntegralFunction(this, Argument, x, dist, side); + } + + partial record Erfif + { + internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side) + => OfAnErrorOrIntegralFunction(this, Argument, x, dist, side); + } + + partial record Sif + { + internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side) + => OfAnErrorOrIntegralFunction(this, Argument, x, dist, side); + } + + partial record Cif + { + internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side) + => OfAnErrorOrIntegralFunction(this, Argument, x, dist, side); + } + + partial record Shif + { + internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side) + => OfAnErrorOrIntegralFunction(this, Argument, x, dist, side); + } + + partial record Chif + { + internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side) + => OfAnErrorOrIntegralFunction(this, Argument, x, dist, side); + } + + partial record Eif + { + internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side) + => OfAnErrorOrIntegralFunction(this, Argument, x, dist, side); + } + + partial record Lif + { + internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side) + => OfAnErrorOrIntegralFunction(this, Argument, x, dist, side); + } + partial record Arctanf { internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side) diff --git a/Sources/Tests/UnitTests/Calculus/ErrorAndIntegralFunctionLimitsTest.cs b/Sources/Tests/UnitTests/Calculus/ErrorAndIntegralFunctionLimitsTest.cs new file mode 100644 index 000000000..125d76b0e --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/ErrorAndIntegralFunctionLimitsTest.cs @@ -0,0 +1,59 @@ +// +// 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 +{ + /// + /// The error functions and the exponential and trigonometric integrals had no limit at all: erf(x) at +oo, + /// which is 1, was left unevaluated, and so was erf(x) at 1. Their limits at the infinities are now their + /// values there, and a finite limit of the argument is put in where the function is continuous: anywhere for the + /// entire ones, only at a positive real point for Ci, Chi, Ei and li, which are singular at 0 and cut below it, and + /// li singular at 1 too. The same on 2.6.0. Found by limitcheck at DEPTH=3, SEED=53; no issue was + /// filed. + /// + [Trait("Area", "Calculus")] + public sealed class ErrorAndIntegralFunctionLimitsTest + { + [Theory] + [InlineData("erf(x)", "+oo", "1")] + [InlineData("erf(x)", "-oo", "-1")] + [InlineData("erfc(x)", "+oo", "0")] + [InlineData("erfc(x)", "-oo", "2")] + [InlineData("erfi(x)", "+oo", "+oo")] + [InlineData("Si(x)", "+oo", "pi/2")] + [InlineData("Si(x)", "-oo", "-pi/2")] + [InlineData("Ci(x)", "+oo", "0")] + [InlineData("Shi(x)", "-oo", "-oo")] + [InlineData("Chi(x)", "+oo", "+oo")] + [InlineData("Ei(x)", "+oo", "+oo")] + [InlineData("Ei(x)", "-oo", "0")] + [InlineData("li(x)", "+oo", "+oo")] + [InlineData("erf(x)", "1", "erf(1)")] + [InlineData("Ci(x)", "2", "Ci(2)")] + [InlineData("erf(x)/x + 1", "0", "2/sqrt(pi) + 1")] + public void HasItsLimit(string input, string destination, string expected) + => Assert.Equal(MathS.FromString(expected).Simplify(), MathS.FromString(input).Limit("x", destination).Simplify()); + + [Fact] + public void IsOneSidedWhereItsArgumentDiverges() + { + Assert.Equal(MathS.FromString("1"), MathS.FromString("erf(1/x)").Limit("x", 0, ApproachFrom.Right)); + Assert.Equal(MathS.FromString("-pi/2").Simplify(), MathS.FromString("Si(1/x)").Limit("x", 0, ApproachFrom.Left).Simplify()); + Assert.Equal(MathS.NaN, MathS.FromString("erf(1/x)").Limit("x", 0)); + } + + // Nothing is claimed on Ci's cut or at li's pole. + [Theory] + [InlineData("Ci(x)", "-2")] + [InlineData("Ci(x)", "-oo")] + public void IsNotAnsweredOnACut(string input, string destination) + => Assert.IsType(MathS.FromString(input).Limit("x", destination)); + } +}