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
12 changes: 12 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,18 @@ Pythagorean identity with a coefficient is now a rule of its own, found wherever
| `"2 + (-2)*cos(x)^2 + (-2)*sin(x)^2".Simplify()` | `2 + (-2) * cos(x) ^ 2 + (-2) * sin(x) ^ 2` | `0` |
| `"2 + (-2) * cos(2 * x) + (-4) * sin(x) ^ 2".Simplify()` | unchanged | `0` |

### A compiled division by zero over `double` has no value

**Silent: compiled over `double` or `float`, a division by zero and 0 to a power that is not positive are `NaN`, as in
evaluation, FastExpression and the compiler over `Complex`.** The IEEE quotient made `1/0` an infinity, and `Math.Pow`
made `0^(-1)` an infinity and `0^0` 1 ([#2286](https://github.com/asc-community/AngouriMath/issues/2286)).

| Input | Was (2.6.0) | Now |
|---|---|---|
| `"1/x".Compile<double, double>("x")(0)` | `+Infinity` | `NaN` |
| `"x^(-1)".Compile<double, double>("x")(0)` | `+Infinity` | `NaN` |
| `"x^x".Compile<double, double>("x")(0)` | `1` | `NaN` |

### 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
Expand Down
3 changes: 3 additions & 0 deletions Sources/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -968,5 +968,8 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed
[Tests/UnitTests/Common/PowerOfNaNOverDoubleTest.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/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/BranchCutCompilationTest.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
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,11 @@ public virtual Expression ConvertBinaryNode(Expression left, Expression right, E
Sumf => Operator(ExpressionType.Add, left, right),
Minusf => Operator(ExpressionType.Subtract, left, right),
Mulf => Operator(ExpressionType.Multiply, left, right),
// Over the reals by MathAllMethods.Divide, which gives a division by zero no value, as
// evaluation does. https://github.com/asc-community/AngouriMath/issues/2286
Divf when ShouldBeAtLeastDouble(left) is var newLeft && ShouldBeAtLeastDouble(right) is var newRight
&& (newLeft.Type == typeof(double) || newLeft.Type == typeof(float)) && newLeft.Type == newRight.Type
=> Expression.Call(GetDef("Divide", 2, newLeft.Type), newLeft, newRight),
Divf => Operator(ExpressionType.Divide, ShouldBeAtLeastDouble(left), ShouldBeAtLeastDouble(right)),
// ((a % b) + b) % b, and not the bare a % b: the runtime truncates, where the
// node is the floored remainder that takes the sign of the divisor, and this is
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,19 @@ public static System.Numerics.Complex Pow(System.Numerics.Complex a, System.Nume
=> AngouriMath.Core.Compilation.RealProducts.Power(a, b);
// A power of a number that does not exist, or to such a power, has no value, as in evaluation and the other
// compilers: Math.Pow takes NaN^0 and 1^NaN as 1. https://github.com/asc-community/AngouriMath/issues/2250
// Nor does 0 to a power that is not positive, which evaluation and the other compilers answer NaN: Math.Pow
// takes 0^0 as 1 and 0^(-1) as +oo. https://github.com/asc-community/AngouriMath/issues/2286
public static double Pow(double a, double b)
=> double.IsNaN(a) || double.IsNaN(b) ? double.NaN : Math.Pow(a, b);
=> double.IsNaN(a) || double.IsNaN(b) || a == 0 && b <= 0 ? double.NaN : Math.Pow(a, b);
public static float Pow(float a, float b)
=> float.IsNaN(a) || float.IsNaN(b) ? float.NaN : (float)Math.Pow(a, b);
=> float.IsNaN(a) || float.IsNaN(b) || a == 0 && b <= 0 ? float.NaN : (float)Math.Pow(a, b);

// A division by zero has no value, as in evaluation and the other compilers, where the IEEE quotient is an
// infinity. https://github.com/asc-community/AngouriMath/issues/2286
public static double Divide(double a, double b)
=> b == 0 ? double.NaN : a / b;
public static float Divide(float a, float b)
=> b == 0 ? float.NaN : a / b;
public static long Pow(long a, long b)
=> (long)Math.Pow(a, b);
public static int Pow(int a, int b)
Expand Down Expand Up @@ -425,6 +434,8 @@ public static bool IsNaN(System.Numerics.Complex a)
{ ("Min", 2, typeof(long)), Def<long>((a, b) => Min(a, b)) },
{ ("Min", 2, typeof(int)), Def<int>((a, b) => Min(a, b)) },
{ ("Min", 2, typeof(BigInteger)), Def<BigInteger>((a, b) => Min(a, b)) },
{ ("Divide", 2, typeof(double)), Def<double>((a, b) => Divide(a, b)) },
{ ("Divide", 2, typeof(float)), Def<float>((a, b) => Divide(a, b)) },
{ ("Sin", 1, typeof(System.Numerics.Complex)), Def<System.Numerics.Complex>(a => Sin(a)) },
{ ("Sin", 1, typeof(double)), Def<double>(a => Sin(a)) },
{ ("Sin", 1, typeof(float)), Def<float>(a => Sin(a)) },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,19 @@ namespace AngouriMath.Core.Compilation.IntoLinq
=> AngouriMath.Core.Compilation.RealProducts.Power(a, b);
// A power of a number that does not exist, or to such a power, has no value, as in evaluation and the other
// compilers: Math.Pow takes NaN^0 and 1^NaN as 1. https://github.com/asc-community/AngouriMath/issues/2250
// Nor does 0 to a power that is not positive, which evaluation and the other compilers answer NaN: Math.Pow
// takes 0^0 as 1 and 0^(-1) as +oo. https://github.com/asc-community/AngouriMath/issues/2286
public static double Pow(double a, double b)
=> double.IsNaN(a) || double.IsNaN(b) ? double.NaN : Math.Pow(a, b);
=> double.IsNaN(a) || double.IsNaN(b) || a == 0 && b <= 0 ? double.NaN : Math.Pow(a, b);
public static float Pow(float a, float b)
=> float.IsNaN(a) || float.IsNaN(b) ? float.NaN : (float)Math.Pow(a, b);
=> float.IsNaN(a) || float.IsNaN(b) || a == 0 && b <= 0 ? float.NaN : (float)Math.Pow(a, b);

// A division by zero has no value, as in evaluation and the other compilers, where the IEEE quotient is an
// infinity. https://github.com/asc-community/AngouriMath/issues/2286
public static double Divide(double a, double b)
=> b == 0 ? double.NaN : a / b;
public static float Divide(float a, float b)
=> b == 0 ? float.NaN : a / b;
public static long Pow(long a, long b)
=> (long)Math.Pow(a, b);
public static int Pow(int a, int b)
Expand Down Expand Up @@ -297,6 +306,9 @@ namespace AngouriMath.Core.Compilation.IntoLinq
{ ("<#= name #>", 2, typeof(<#= type #>)), Def<<#= type #>>((a, b) => <#= name #>(a, b)) },
<# } #>
<# } #>
<# foreach (var type in new[]{ "double", "float" }) { #>
{ ("Divide", 2, typeof(<#= type #>)), Def<<#= type #>>((a, b) => Divide(a, b)) },
<# } #>
<# foreach (var name in unary) { #>
<# foreach (var type in types) { #>
{ ("<#= name #>", 1, typeof(<#= type #>)), Def<<#= type #>>(a => <#= name #>(a)) },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ namespace AngouriMath.Core.Compilation.IntoLinq
/// <inheritdoc/>
public double Negate(double a) => -a;
/// <inheritdoc/>
public double Divide(double a, double b) => a / b;
public double Divide(double a, double b) => b == 0 ? double.NaN : a / b;
/// <inheritdoc/>
public double CreateOne() => 1;
/// <inheritdoc/>
Expand Down Expand Up @@ -67,7 +67,7 @@ namespace AngouriMath.Core.Compilation.IntoLinq
/// <inheritdoc/>
public float Negate(float a) => -a;
/// <inheritdoc/>
public float Divide(float a, float b) => a / b;
public float Divide(float a, float b) => b == 0 ? float.NaN : a / b;
/// <inheritdoc/>
public float CreateOne() => 1;
/// <inheritdoc/>
Expand Down
64 changes: 64 additions & 0 deletions Sources/Tests/UnitTests/Common/DivisionByZeroOverDoubleTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//
// 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;
using AngouriMath.Core.Compilation.IntoLinq;
using AngouriMath.Extensions;
using GenericTensor.Core;
using Xunit;

namespace AngouriMath.Tests.Common
{
/// <summary>
/// Compiled over <see cref="double"/>, a division by zero, and 0 to a power that is not positive, have no value, as in
/// evaluation, FastExpression and the compiler over <see cref="System.Numerics.Complex"/>: the IEEE quotient made 1/0
/// +oo, and <see cref="System.Math.Pow(double, double)"/> made 0^(-1) +oo and 0^0 1.
/// <a href="https://github.com/asc-community/AngouriMath/issues/2286">#2286</a>
/// </summary>
[Trait("Area", "Common")]
public sealed class DivisionByZeroOverDoubleTest
{
[Theory]
[InlineData("1/x", 0)]
[InlineData("-1/x", 0)]
[InlineData("1/(x - x)", 3)]
[InlineData("x / floor(x)", 0.5)]
[InlineData("x^(-1)", 0)]
[InlineData("x^(-1/2)", 0)]
[InlineData("x^x", 0)]
public void ItHasNoValue(string expression, double at)
{
var overDoubles = expression.ToEntity().Compile<double, double>("x")(at);
Assert.True(double.IsNaN(overDoubles), $"{expression} at {at}: {overDoubles} over double");
Assert.True(float.IsNaN(expression.ToEntity().Compile<float, float>("x")((float)at)), $"{expression} at {at} over float");
Assert.True(expression.ToEntity().Substitute("x", at).Evaled.IsNaN, $"{expression} at {at} evaluates to a value");
}

/// <summary>
/// A matrix argument divided by a zero scalar, entry by entry as a scalar is. A matrix written into the expression
/// is divided before it is compiled, and only an argument reaches the compiled matrix division.
/// </summary>
[Fact]
public void AMatrixOverZeroHasNoEntries()
{
var matrix = GenTensor<double, DoubleOperations>.CreateMatrix(new double[,] { { 1, 2 }, { 3, 4 } });
var divided = "A / x".ToEntity()
.Compile<GenTensor<double, DoubleOperations>, double, GenTensor<double, DoubleOperations>>("A", "x")(matrix, 0);
for (var i = 0; i < 2; i++)
for (var j = 0; j < 2; j++)
Assert.True(double.IsNaN(divided[i, j]), $"[{i}, {j}] is {divided[i, j]}");
}

[Theory]
[InlineData("1/x", 4, 0.25)]
[InlineData("x^(1/2)", 0, 0)]
[InlineData("x^2", 0, 0)]
[InlineData("0 / x", 2, 0)]
public void WhereItHasOneItIsThatValue(string expression, double at, double value)
=> Assert.Equal(value, expression.ToEntity().Compile<double, double>("x")(at));
}
}
Loading