From f1f8bfee7217e607ef5f298df99093f1447b6ee2 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Mon, 5 Oct 2026 01:35:03 +0000 Subject: [PATCH] A power of a constant below 1e-50 is no longer simplified to zero InnerSimplified takes a node's value where that value is an exact number, and evaluation rounds a value within 1e-50 of an integer onto it, so 1/pi^136, about 2.4e-68, became 0, and Simplify, which raises constants to such powers on its way, returned 0 for expressions that are not zero. A product, a quotient or a power of numbers none of which is zero, or a sum of such numbers of one sign, is kept as written now; evaluation still rounds. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 16 +++++ .../Evaluation/Evaluation.Classes.cs | 58 +++++++++++++++- .../PowerOfAConstantBelowTheToleranceTest.cs | 68 +++++++++++++++++++ 3 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 Sources/Tests/UnitTests/Core/PowerOfAConstantBelowTheToleranceTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 6faf6c53c..071c74aa0 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -161,6 +161,22 @@ the division's; with it on, the rounding is the setting's own and stays | `"(-60.5)!"` at 30 digits, downcasting off | `5.86118478907722232671451280188E-81` | `2.93059239453861116335725639905E-81` | | `"abs(75 + 316.22776601683796i)"`, downcasting off | `325` | `325.000000000000026076735749…` | +### A power of a constant below `1e-50` is no longer simplified to zero + +**Answers that were wrong.** Evaluation rounds a value within `1e-50` of an integer onto it, and +inner simplification takes a node's value where that value is an exact number, so `1/pi^136`, +about `2.4e-68`, became `0`, and so did whatever held it. A product, a quotient or a power of +numbers that are not zero, or a sum of such numbers of one sign, is kept as written now. Its +evaluation still rounds. `Simplify` raises constants to such powers on the way, and returned `0` +for expressions that are not zero; the integrator, simplifying under a substitution, answered such +integrals with `0` ([#1769](https://github.com/asc-community/AngouriMath/issues/1769)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"1/pi^136".ToEntity().InnerSimplified` | `0` | `1 / pi ^ 136` | +| `"e^(-160)".ToEntity().InnerSimplified` | `0` | `e ^ (-160)` | +| `"x^n*((1 - d^2)/x - x)^3*(1 + x^2 - d*x)/(x - d)/pi^2".ToEntity().Simplify()` | `0 provided not x - d = 0 and ...` | `x ^ n * ((1 - d ^ 2) / x - x) ^ 3 * (1 - d * x + x ^ 2) / (pi ^ 2 * (x - d))` | + ### A rational function with symbols in it beside a root of a linear is split into partial fractions first **Answers where there were none.** `1/(x (1 + x^2) sqrt(a + b x))` was declined, while diff --git a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Classes.cs b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Classes.cs index 857fc3595..1f86edead 100644 --- a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Classes.cs +++ b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Classes.cs @@ -33,6 +33,60 @@ public partial record Constant protected override Entity InnerSimplify(bool isExact) => isExact ? this : Value; } + /// + /// Whether , the exact number evaluates + /// to, is a zero that rounding alone made: is a product, a quotient + /// or a power of numbers none of which is zero, or a sum of numbers of one sign, and is + /// not zero itself. + /// + /// + /// Evaluation rounds a value within of an + /// integer onto it, ten to the minus fifty at the default hundred digits, which is what + /// makes the residual of a cancellation the zero it is. A product cancels nothing, and was + /// rounded the same way: 1/pi^136, about 2.4e-68, evaluated to 0, and + /// that zero, being an exact number, was taken as its value. The common denominator + /// raises constants to such powers, and Simplify returned 0 for + /// x^n ((1 - d^2)/x - x)^3 (1 + x^2 - d x)/(x - d)/pi^2. A sum whose terms have + /// one sign cancels nothing either; any other sum's zero may be a cancellation's, and is + /// taken as it was. + /// https://github.com/asc-community/AngouriMath/issues/1769 + /// + private static bool IsAZeroOnlyRoundingMade(Entity node, Entity value) + => value is Integer { IsZero: true } + && (node is Mulf or Divf or Powf && CannotBeZero(node) || node is Sumf or Minusf && SignOfANonzero(node) is not null); + + /// + /// Whether is a number or a constant that is not zero, or a product, + /// a quotient or a power of those with a finite divisor or exponent. + /// + private static bool CannotBeZero(Entity expr) => expr switch + { + Complex number => !number.IsZero && number.IsFinite, + Variable { IsConstant: true } constant => constant.Evaled is Complex { IsZero: false, IsFinite: true }, + Mulf(var left, var right) => CannotBeZero(left) && CannotBeZero(right), + Divf(var dividend, var divisor) => CannotBeZero(dividend) && divisor.Evaled is Complex { IsFinite: true }, + Powf(var @base, var exponent) => CannotBeZero(@base) && exponent.Evaled is Complex { IsFinite: true }, + _ => SignOfANonzero(expr) is not null + }; + + /// + /// The sign of where it is a real number or a constant that is not + /// zero, or a product, a quotient, a power or a sum of those whose sign the parts decide; + /// where they do not. + /// + private static int? SignOfANonzero(Entity expr) => expr switch + { + Real number => !number.IsZero && number.IsFinite ? (number.IsPositive ? 1 : -1) : null, + Variable { IsConstant: true } constant => constant.Evaled is Real { IsZero: false } value ? (value.IsPositive ? 1 : -1) : null, + Mulf(var left, var right) => SignOfANonzero(left) * SignOfANonzero(right), + Divf(var dividend, var divisor) => SignOfANonzero(dividend) * SignOfANonzero(divisor), + Powf(var @base, Integer power) => SignOfANonzero(@base) is { } sign ? (power.EInteger.IsEven ? 1 : sign) : null, + Powf(var @base, var exponent) => SignOfANonzero(@base) == 1 && exponent.Evaled is Real { IsFinite: true } ? 1 : null, + Sumf(var left, var right) => SignOfANonzero(left) is { } sign && SignOfANonzero(right) == sign ? sign : null, + Minusf(var left, var right) => SignOfANonzero(left) is { } sign && SignOfANonzero(right) == -sign ? sign : null, + _ => null + }; + /// /// For two-argument nodes /// Used in InnerSimplify and InnerEval @@ -74,7 +128,7 @@ private Entity ExpandOnTwoArguments( bool propagateSet = true, bool settlesNaN = false) { - if (isExact && this.Evaled is (Number { IsExact: true } or Boolean) and var n) + if (isExact && this.Evaled is (Number { IsExact: true } or Boolean) and var n && !IsAZeroOnlyRoundingMade(this, n)) return n; left = left.InnerSimplified(isExact); right = right.InnerSimplified(isExact); @@ -97,7 +151,7 @@ Entity ops(Entity a, Entity b) { if (operation(a, b) is { } res) return res; - if (isExact && defaultCtor(this, a, b).Evaled is Number { IsExact: true } n) + if (isExact && defaultCtor(this, a, b) is var built && built.Evaled is Number { IsExact: true } n && !IsAZeroOnlyRoundingMade(built, n)) return n; return defaultCtor(this, a, b); } diff --git a/Sources/Tests/UnitTests/Core/PowerOfAConstantBelowTheToleranceTest.cs b/Sources/Tests/UnitTests/Core/PowerOfAConstantBelowTheToleranceTest.cs new file mode 100644 index 000000000..0e7953984 --- /dev/null +++ b/Sources/Tests/UnitTests/Core/PowerOfAConstantBelowTheToleranceTest.cs @@ -0,0 +1,68 @@ +// +// 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 AngouriMath; +using AngouriMath.Extensions; +using Xunit; +using static AngouriMath.Entity; + +namespace AngouriMath.Tests.Core +{ + /// + /// A product, a quotient or a power of nonzero numbers is not zero however small it is. + /// Evaluation rounds a value within 1e-50 of an integer onto it, and the exact zero + /// that made of 1/pi^136 was taken as its value. + /// https://github.com/asc-community/AngouriMath/issues/1769 + /// + [Trait("Area", "Core")] + public sealed class PowerOfAConstantBelowTheToleranceTest + { + private static double Value(Entity expr) => (double)((Number.Complex)expr.EvalNumerical()).RealPart; + + // With a rational for each constant, both sides are exact numbers. + private static Entity Exactly(Entity expr) => expr.Substitute(MathS.pi, 3).Substitute(MathS.e, 2).Evaled; + + [Theory] + [InlineData("1 / pi ^ 136")] + [InlineData("(pi ^ 136) ^ (-1)")] + [InlineData("pi ^ (-136) * pi ^ (-136)")] + [InlineData("e ^ (-160)")] + [InlineData("pi ^ (-136) + pi ^ (-137)")] + [InlineData("-pi ^ (-136) - e ^ (-160)")] + public void ItIsNotRoundedToZero(string written) + { + var expr = written.ToEntity(); + var simplified = expr.InnerSimplified; + Assert.NotEqual(Number.Integer.Zero, simplified); + Assert.Equal(Exactly(expr), Exactly(simplified)); + } + + [Fact] + public void NorIsAPowerOfARoot() + => Assert.NotEqual(Number.Integer.Zero, "sqrt(2) ^ (-400)".ToEntity().InnerSimplified); + + // What the rounding is for, the residual of a cancellation, is still zero. + [Theory] + [InlineData("sqrt(2) ^ 2 - 2")] + [InlineData("pi ^ (-136) - pi ^ (-136)")] + public void ACancellationIsStillZero(string written) + => Assert.Equal(Number.Integer.Zero, written.ToEntity().InnerSimplified); + + // The common denominator raises pi to the 136th power on the way, and the expression + // simplified to zero. + [Fact] + public void SimplifyKeepsTheValueOfAQuotientByAPowerOfPi() + { + var expr = "x ^ n * ((1 - d ^ 2) / x - x) ^ 3 * (1 + x ^ 2 - d * x) / (x - d) / pi ^ 2".ToEntity(); + static Entity At(Entity e) => e.Substitute("x", 1.3).Substitute("d", 0.7).Substitute("n", 0.37); + var expected = Value(At(expr)); + Assert.True(Math.Abs(expected + 0.24771026954308137) < 1e-12, $"the expression is {expected} there"); + Assert.True(Math.Abs(Value(At(expr.Simplify())) - expected) < 1e-12); + } + } +}