From 7c30af01adda4fe47504427de4fd682c045b1915 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sat, 31 Oct 2026 20:17:15 +0000 Subject: [PATCH] tanh and coth of a large number are their limits tanh and coth are spelled (e^(2x) - 1) / (e^(2x) + 1) and its reciprocal. Past the range of the decimals both sides are infinite, so tanh(5568) and coth(332640) evaluated to NaN, where they are 1; past a double's, tanh(400) compiled to NaN in all three compilers. The negative side was right only because e^(-11136) is 0. A quotient of one power plus two constants, (b^v + c1) / (b^v + c2), is the same value as 1 + (c1 - c2) / (b^v + c2) wherever it has one, and the second does not divide an overflow by an overflow. ExponentialQuotients writes it so. Evaluation asks for it where both sides of a quotient come out infinite, and the compilers' pre-pass writes every such quotient so, since a double cannot tell an overflow from an infinity. At +oo it is the limit, 1, as tanh(-oo) was already -1 and arctan(+oo) is pi/2. Respelling tanh itself as 1 - 2/(e^(2x) + 1) was measured and not taken: 21 tests change, nearly all integrals of hyperbolic functions the integrator reads in the quotient form. HyperbolicTangentPastTheRangeTest takes both sides evaluated and compiled, the infinities, and the compiled values inside the range, near 0 as well, against Math.Tanh. Seven of its cases fail on master. compilesweep's 112 findings are the same list on master and here. The suite, the trio, F#, AOT and the eight harnesses pass. Fixes #2254. Co-Authored-By: Claude Opus 5.5 (1M context) --- BREAKING-CHANGES.md | 17 +++++ Sources/.editorconfig | 6 ++ .../Functions/Compilation/IntoFE/Compiler.cs | 1 + .../Compilation/IntoLinq/IntoLinqCompiler.cs | 1 + ...aluation.Continuous.Arithmetics.Classes.cs | 5 ++ .../Functions/ExponentialQuotients.cs | 57 ++++++++++++++++ .../HyperbolicTangentPastTheRangeTest.cs | 67 +++++++++++++++++++ 7 files changed, 154 insertions(+) create mode 100644 Sources/AngouriMath/Functions/ExponentialQuotients.cs create mode 100644 Sources/Tests/UnitTests/Common/HyperbolicTangentPastTheRangeTest.cs 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)}"); + } + } +}