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
16 changes: 16 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
58 changes: 56 additions & 2 deletions Sources/AngouriMath/Functions/Evaluation/Evaluation.Classes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,60 @@ public partial record Constant
protected override Entity InnerSimplify(bool isExact) => isExact ? this : Value;
}

/// <summary>
/// Whether <paramref name="value"/>, the exact number <paramref name="node"/> evaluates
/// to, is a zero that rounding alone made: <paramref name="node"/> 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.
/// </summary>
/// <remarks>
/// Evaluation rounds a value within <see cref="MathS.Settings.DowncastingTolerance"/> 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: <c>1/pi^136</c>, about <c>2.4e-68</c>, evaluated to <c>0</c>, and
/// that zero, being an exact number, was taken as its value. The common denominator
/// raises constants to such powers, and <c>Simplify</c> returned <c>0</c> for
/// <c>x^n ((1 - d^2)/x - x)^3 (1 + x^2 - d x)/(x - d)/pi^2</c>. 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
/// </remarks>
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);

/// <summary>
/// Whether <paramref name="expr"/> 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.
/// </summary>
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
};

/// <summary>
/// The sign of <paramref name="expr"/> 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;
/// <see langword="null"/> where they do not.
/// </summary>
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
};

/// <summary>
/// For two-argument nodes
/// Used in InnerSimplify and InnerEval
Expand Down Expand Up @@ -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);
Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// A product, a quotient or a power of nonzero numbers is not zero however small it is.
/// Evaluation rounds a value within <c>1e-50</c> of an integer onto it, and the exact zero
/// that made of <c>1/pi^136</c> was taken as its value.
/// https://github.com/asc-community/AngouriMath/issues/1769
/// </summary>
[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);
}
}
}
Loading