diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 7a602ae02..7faa2efb9 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -442,6 +442,23 @@ made `0^(-1)` an infinity and `0^0` 1 ([#2286](https://github.com/asc-community/ | `"x^(-1)".Compile("x")(0)` | `+Infinity` | `NaN` | | `"x^x".Compile("x")(0)` | `1` | `NaN` | +### `tanh` and `coth` of a large number are their limits + +**Silent: `tanh` and `coth` of a large positive number evaluate and compile to `1`, where they were `NaN`, and at +`+oo` they are `1`, as at `-oo` they were already `-1`.** Both are spelled `(e^(2x) - 1) / (e^(2x) + 1)` and its +reciprocal, and past the range of the decimals, or of a double, both sides were infinite. A quotient of one power plus +two constants, `(b^v + c1) / (b^v + c2)`, is now evaluated as `1 + (c1 - c2) / (b^v + c2)` where both sides are +infinite, and always compiled so, since a double cannot tell an overflow from an infinity +([#2254](https://github.com/asc-community/AngouriMath/issues/2254)). + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `"tanh(5568)".ToEntity().Evaled` | `NaN` | `1` | +| `"coth(332640)".ToEntity().EvalNumerical()` | `NaN` | `1` | +| `"tanh(+oo)".ToEntity().Evaled` | `NaN` | `1` | +| `"tanh(x)".Compile("x")(400)` | `NaN` | `1` | +| `"coth(x)".ToEntity().Compile("x").Call(400)` | `(NaN, NaN)` | `(1, 0)` | + ### A lambda applied to a vector multiplies by it as a vector **Silent: a matrix of vectors becomes the product.** Applying a lambda simplified its body first, and a simplified diff --git a/Sources/.editorconfig b/Sources/.editorconfig index d3925edc9..903df26cb 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -432,6 +432,9 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed [AngouriMath/Numerics/NumberStatements.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/ExponentialQuotients.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/Compilation/CompiledMembership.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 @@ -1013,6 +1016,9 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed [Tests/UnitTests/Common/DivisionByZeroOverDoubleTest.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/Common/HyperbolicTangentPastTheRangeTest.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/Common/RefuseNonRealCompilationTest.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/Compilation/IntoFE/Compiler.cs b/Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs index ba78f1d01..c33fe5e8e 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs @@ -1016,6 +1016,7 @@ internal static FastExpression Compile(Entity func, IEnumerable variab foreach (var constant in Variable.NamedConstants.Values) func = func.Substitute(constant, constant.Value); func = Compilation.CompiledMembership.Rewritten(func); + func = Functions.ExponentialQuotients.Rewritten(func); Compilation.StatementPositions.Check(func); var visited = new HashSet(); var cache = new Dictionary(); diff --git a/Sources/AngouriMath/Functions/Compilation/IntoLinq/IntoLinqCompiler.cs b/Sources/AngouriMath/Functions/Compilation/IntoLinq/IntoLinqCompiler.cs index e6a981f99..8f270f48d 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoLinq/IntoLinqCompiler.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoLinq/IntoLinqCompiler.cs @@ -31,6 +31,7 @@ internal static TDelegate Compile( if (expr.Nodes.Any(node => node is Entity.Matrix)) expr = expr.InnerSimplified; expr = CompiledMembership.Rewritten(expr); + expr = Functions.ExponentialQuotients.Rewritten(expr); StatementPositions.Check(expr); if (MathS.Settings.RefuseNonRealCompilation && returnType is { } real && IsRealType(real) && typesAndNames.All(c => IsRealType(c.type)) && !IsRealWhereverDefined(expr, typesAndNames)) diff --git a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs index 6f05a663e..ddc4704eb 100644 --- a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs +++ b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs @@ -239,6 +239,11 @@ n0.Evaled is Complex c ? c.IsZero ? MathS.NaN : 0 : new Providedf(0, new Notf(new Equalsf(n0, 0))), (_, Integer(0)) => Real.NaN, + // tanh's spelling past the decimals' range, both sides infinite: the same + // quotient without them. https://github.com/asc-community/AngouriMath/issues/2254 + (Complex { IsFinite: false }, Complex { IsFinite: false }) when !isExact + && Functions.ExponentialQuotients.Rescaled(Dividend, Divisor) is { } rescaled + => rescaled.InnerSimplified(isExact), (Complex n1, Complex n2) when !isExact => n1 / n2, (var n1, Integer(1)) => n1, // Sizes are not divided: aleph(0) / 2 has no value. diff --git a/Sources/AngouriMath/Functions/ExponentialQuotients.cs b/Sources/AngouriMath/Functions/ExponentialQuotients.cs new file mode 100644 index 000000000..7538c9e31 --- /dev/null +++ b/Sources/AngouriMath/Functions/ExponentialQuotients.cs @@ -0,0 +1,57 @@ +// +// 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 static AngouriMath.Entity; + +namespace AngouriMath.Functions +{ + /// + /// A quotient of one power plus two constants, (b^v + c1) / (b^v + c2), as + /// 1 + (c1 - c2) / (b^v + c2). The two are the same value wherever the quotient has one, + /// and the second does not divide an overflow by an overflow. + /// + /// + /// It is how tanh and coth are spelled: is + /// (e^(2x) - 1) / (e^(2x) + 1). Past the range of the decimals, or of a double, both sides + /// of it are infinite, so tanh(5568) evaluated to NaN and tanh(400) compiled to NaN, + /// where both are 1. At a real infinity the second form is the limit, as tanh(-oo) already + /// was -1 and arctan(+oo) is pi / 2: tanh(+oo) is 1. + /// https://github.com/asc-community/AngouriMath/issues/2254 + /// + internal static class ExponentialQuotients + { + /// + /// 1 + (c1 - c2) / divisor where and + /// are one power plus a constant each, and + /// otherwise. + /// + internal static Entity? Rescaled(Entity dividend, Entity divisor) + => Split(dividend) is var (power, c1) && Split(divisor) is var (same, c2) && power == same + ? 1 + (c1 - c2) / divisor + : null; + + /// + /// Every such quotient in rewritten, for the compilers: a double + /// cannot tell an overflow from an infinity, so the rewrite is made wherever the shape is. + /// + internal static Entity Rewritten(Entity expression) + => expression.Nodes.Any(node => node is Divf(Sumf or Minusf, Sumf or Minusf)) + ? expression.Replace(node => node is Divf(var dividend, var divisor) && Rescaled(dividend, divisor) is { } rescaled ? rescaled : node) + : expression; + + /// The power and the constant of b^v + c, c + b^v or b^v - c, with b and c constants. + private static (Powf Power, Entity Constant)? Split(Entity sum) => sum switch + { + Sumf(Powf power, var constant) when IsConstant(power.Base) && IsConstant(constant) => (power, constant), + Sumf(var constant, Powf power) when IsConstant(power.Base) && IsConstant(constant) => (power, constant), + Minusf(Powf power, var constant) when IsConstant(power.Base) && IsConstant(constant) => (power, -constant), + _ => null, + }; + + private static bool IsConstant(Entity expression) => !expression.Vars.Any(); + } +} diff --git a/Sources/Tests/UnitTests/Common/HyperbolicTangentPastTheRangeTest.cs b/Sources/Tests/UnitTests/Common/HyperbolicTangentPastTheRangeTest.cs new file mode 100644 index 000000000..0463dfe6a --- /dev/null +++ b/Sources/Tests/UnitTests/Common/HyperbolicTangentPastTheRangeTest.cs @@ -0,0 +1,67 @@ +// +// 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; +using System.Numerics; +using AngouriMath; +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Common +{ + /// + /// tanh and coth are spelled (e^(2x) - 1) / (e^(2x) + 1) and its reciprocal, and past the range + /// of the decimals, or of a double, both sides are infinite: tanh(5568) evaluated to NaN and tanh(400) + /// compiled to NaN, where both are 1. The negative side was right, since e^(-11136) is 0. + /// #2254 + /// + [Trait("Area", "Common")] + public sealed class HyperbolicTangentPastTheRangeTest + { + [Theory] + [InlineData("tanh(5568)", 1)] + [InlineData("tanh(-5568)", -1)] + [InlineData("coth(332640)", 1)] + [InlineData("coth(-332640)", -1)] + [InlineData("tanh(+oo)", 1)] + [InlineData("tanh(-oo)", -1)] + [InlineData("coth(+oo)", 1)] + public void EvaluatedPastTheRangeItIsOne(string expression, int expected) + { + Assert.Equal(Entity.Number.Integer.Create(expected), expression.ToEntity().Evaled); + Assert.Equal(Entity.Number.Integer.Create(expected), expression.ToEntity().EvalNumerical()); + } + + [Theory] + [InlineData("tanh(x)", 400, 1)] + [InlineData("tanh(x)", -400, -1)] + [InlineData("coth(x)", 400, 1)] + [InlineData("coth(x)", -400, -1)] + [InlineData("tanh(x)", double.PositiveInfinity, 1)] + [InlineData("tanh(x)", double.NegativeInfinity, -1)] + public void CompiledPastTheRangeItIsOne(string expression, double at, double expected) + { + Assert.Equal(expected, expression.Compile("x")(at)); + Assert.Equal(new Complex(expected, 0), expression.Compile("x")(at)); + Assert.Equal(new Complex(expected, 0), expression.Compile("x").Call(at)); + } + + /// Inside the range the compiled values are still the hyperbolic tangent's, near 0 as well. + [Theory] + [InlineData(1e-6)] + [InlineData(-0.3)] + [InlineData(2)] + [InlineData(19)] + public void CompiledInsideTheRangeItIsTheHyperbolicTangent(double at) + { + var compiled = "tanh(x)".Compile("x")(at); + Assert.True(Math.Abs(compiled - Math.Tanh(at)) <= 1e-9 * Math.Abs(Math.Tanh(at)), $"{compiled} against {Math.Tanh(at)}"); + var coth = "coth(x)".Compile("x")(at); + Assert.True(Math.Abs(coth - 1 / Math.Tanh(at)) <= 1e-9 * Math.Abs(1 / Math.Tanh(at)), $"{coth} against {1 / Math.Tanh(at)}"); + } + } +}