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
22 changes: 22 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,28 @@ then had no truth value for. A product or a quotient of two real numbers is now
| `"-x".ToEntity().Compile("x").Call(double.PositiveInfinity)` | `(-Infinity, NaN)` | `(-Infinity, 0)` |
| `"x / 2".ToEntity().Compile<Complex, Complex>("x")(double.PositiveInfinity)` | `(Infinity, NaN)` | `(Infinity, 0)` |

### Compiled arithmetic of a complex number is evaluation's at a whole power and at an infinity

**Silent: a compiled whole power of a number that is not real, and a compiled product, quotient or signum at an
infinity, are the values evaluation gives.** In FastExpression and in the LINQ compiler over `Complex`:
- A whole power of a number that is not real is a product of its factors, as evaluation takes it. `Complex.Pow` takes
`e^(n ln z)`, which left a rounded part: `i^2` was `(-1, 1.2e-16)`, which is not real.
- A part that is 0 times a part that is infinite is 0, and a number that is 0 times an infinite one has no value, as
in evaluation. `Complex` took `0 · ∞` as `NaN` in every part, so `i * x` at `+oo` was `(NaN, ∞)`.
- A quotient by a real number other than 0 is taken part by part.
- `sgn` of a real infinity is its sign, where `z / |z|` was `∞ / ∞`.

([#2247](https://github.com/asc-community/AngouriMath/issues/2247))

| Input | Was (2.6.0) | Now |
|---|---|---|
| `"x^2".ToEntity().Compile("x").Call(Complex.ImaginaryOne)` | `(-1, 1.2246467991473532E-16)` | `(-1, 0)` |
| `"x^3".ToEntity().Compile("x").Call(new Complex(1, 1))` | `(-2.0000000000000004, 2.0000000000000004)` | `(-2, 2)` |
| `"i * x".ToEntity().Compile("x").Call(double.PositiveInfinity)` | `(NaN, Infinity)` | `(0, Infinity)` |
| `"(x + i) * 2".ToEntity().Compile<Complex, Complex>("x")(double.NegativeInfinity)` | `(-Infinity, NaN)` | `(-Infinity, 2)` |
| `"ln(x) / 2".ToEntity().Compile("x").Call(double.NegativeInfinity)` | `(NaN, NaN)` | `(Infinity, 1.5707963267948966)` |
| `"sgn(x)".ToEntity().Compile("x").Call(double.PositiveInfinity)` | `(NaN, NaN)` | `(1, 0)` |

### A compiled function takes the side of its branch cut evaluation takes

**Silent: a compiled logarithm, root or inverse function of a number on its cut is evaluation's value, where it was
Expand Down
3 changes: 3 additions & 0 deletions Sources/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,9 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed
[Tests/UnitTests/Common/CongruenceCompilationTest.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/ComplexArithmeticCompilationTest.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/LinqCompilationValueTest.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

Expand Down
59 changes: 54 additions & 5 deletions Sources/AngouriMath/Functions/Compilation/RealProducts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,37 @@ namespace AngouriMath.Core.Compilation
/// </summary>
internal static class RealProducts
{
/// <summary>
/// The product as evaluation takes it: a part that is 0 times a part that is infinite is 0, so <c>i</c> times
/// <c>+oo</c> is <c>+oo i</c>, and a number that is 0 times an infinite one is <c>NaN</c>, as <c>0 * oo</c> is.
/// <see cref="System.Numerics.Complex"/> took <c>0 · ∞</c> as <c>NaN</c> in every part, so <c>i * x</c> at <c>+oo</c>
/// was <c>(NaN, ∞)</c>. https://github.com/asc-community/AngouriMath/issues/2247
/// </summary>
public static NumericsComplex Product(NumericsComplex a, NumericsComplex b)
=> a.Imaginary == 0 && b.Imaginary == 0 ? new NumericsComplex(a.Real * b.Real, 0) : a * b;
{
if (a.Imaginary == 0 && b.Imaginary == 0)
return new NumericsComplex(a.Real * b.Real, 0);
if (a == NumericsComplex.Zero && IsInfinite(b) || b == NumericsComplex.Zero && IsInfinite(a))
return new NumericsComplex(double.NaN, 0);
var (ac, bd) = (PartProduct(a.Real, b.Real), PartProduct(a.Imaginary, b.Imaginary));
var (ad, bc) = (PartProduct(a.Real, b.Imaginary), PartProduct(a.Imaginary, b.Real));
return new NumericsComplex(ac - bd, ad + bc);

static double PartProduct(double x, double y) => x == 0 && double.IsInfinity(y) || y == 0 && double.IsInfinity(x) ? 0 : x * y;
static bool IsInfinite(NumericsComplex z) => double.IsInfinity(z.Real) || double.IsInfinity(z.Imaginary);
}

/// <summary>
/// The quotient, real where both are real and the divisor is not 0. A division by 0 is left to
/// <see cref="System.Numerics.Complex"/>, whose <c>NaN</c> is evaluation's: <c>1/0</c> has no value.
/// The quotient, real where both are real and the divisor is not 0, and taken part by part where only the
/// divisor is real and not 0, as evaluation takes it: <see cref="System.Numerics.Complex"/> multiplied each part by
/// the divisor's imaginary 0, so <c>ln(-oo)</c>, <c>+oo + pi i</c>, over <c>1</c> was <c>(∞, NaN)</c>. A division by 0
/// is left to <see cref="System.Numerics.Complex"/>, whose <c>NaN</c> is evaluation's: <c>1/0</c> has no value.
/// https://github.com/asc-community/AngouriMath/issues/2247
/// </summary>
public static NumericsComplex Quotient(NumericsComplex a, NumericsComplex b)
=> a.Imaginary == 0 && b.Imaginary == 0 && b.Real != 0 ? new NumericsComplex(a.Real / b.Real, 0) : a / b;
=> b.Imaginary == 0 && b.Real != 0
? new NumericsComplex(a.Real / b.Real, a.Imaginary == 0 ? 0 : a.Imaginary / b.Real)
: a / b;

/// <summary>
/// The power, real where both are real and it is: a positive base, or a negative one to a whole power.
Expand All @@ -38,6 +60,8 @@ public static NumericsComplex Quotient(NumericsComplex a, NumericsComplex b)
/// </summary>
public static NumericsComplex Power(NumericsComplex a, NumericsComplex b)
{
if (a.Imaginary != 0 && b.Imaginary == 0 && IsWhole(b.Real))
return WholePower(a, b.Real);
if (a.Imaginary != 0 || b.Imaginary != 0)
return NumericsComplex.Pow(ComplexBranches.Unsigned(a), ComplexBranches.Unsigned(b));
var (x, y) = (a.Real, b.Real);
Expand All @@ -48,9 +72,34 @@ public static NumericsComplex Power(NumericsComplex a, NumericsComplex b)
return y > 0 ? NumericsComplex.Zero : new NumericsComplex(double.NaN, 0);
if (x > 0)
return new NumericsComplex(y == 0.5 ? Math.Sqrt(x) : Math.Pow(x, y), 0);
if (!double.IsInfinity(y) && Math.Floor(y) == y)
if (IsWhole(y))
return new NumericsComplex(Math.Pow(x, y), 0);
return NumericsComplex.Pow(ComplexBranches.Unsigned(a), ComplexBranches.Unsigned(b));
}

private static bool IsWhole(double y) => !double.IsInfinity(y) && Math.Floor(y) == y;

/// <summary>
/// A whole power of a number that is not real, as a product of its factors, which is how evaluation takes it.
/// <see cref="System.Numerics.Complex.Pow(System.Numerics.Complex, System.Numerics.Complex)"/> takes <c>e^(n ln z)</c>,
/// whose rounded angle leaves a part that should be 0 at about <c>1e-16</c>: <c>i^2</c> was <c>(-1, 1.2e-16)</c>, which is
/// not real, so <c>(i pi)^2 &gt; 0</c> had no truth value where it is false. Past <c>2^62</c> the exponent is
/// left to it, where a product would take more than 124 multiplications.
/// https://github.com/asc-community/AngouriMath/issues/2247
/// </summary>
private static NumericsComplex WholePower(NumericsComplex z, double n)
{
if (Math.Abs(n) > 4.611686018427388E+18)
return NumericsComplex.Pow(ComplexBranches.Unsigned(z), n);
var power = NumericsComplex.One;
for (var (factor, remaining) = (z, (long)Math.Abs(n)); remaining > 0; remaining >>= 1)
{
if ((remaining & 1) == 1)
power = Product(power, factor);
if (remaining > 1)
factor = Product(factor, factor);
}
return n < 0 ? Quotient(NumericsComplex.One, power) : power;
}
}
}
8 changes: 7 additions & 1 deletion Sources/AngouriMath/Functions/InternalAMExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,14 @@ public static IEnumerable<T> IntersectSequences<T>(this (IEnumerable<T>, IEnumer
yield return s;
}

/// <summary>
/// <c>z / |z|</c>, and the sign of a real number, so that it is <c>1</c> at <c>+oo</c> and <c>-1</c> at <c>-oo</c>, as
/// evaluation has it, where <c>z / |z|</c> was <c>∞ / ∞</c>, <c>NaN</c>. https://github.com/asc-community/AngouriMath/issues/2247
/// </summary>
public static System.Numerics.Complex Signum(this System.Numerics.Complex z)
=> z == 0 ? 0 : z / z.Magnitude;
=> z == 0 ? 0
: z.Imaginary == 0 ? z.Real switch { > 0 => 1, < 0 => -1, _ => double.NaN }
: z / z.Magnitude;

public static System.Numerics.Complex Abs(this System.Numerics.Complex z)
=> System.Numerics.Complex.Abs(z);
Expand Down
4 changes: 3 additions & 1 deletion Sources/Tests/UnitTests/Common/CompilationIntoLinqTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -389,8 +389,10 @@ public void TestUpcastDowncast4()
=> Assert.Equal((int)Math.Sin(4), "sin(a)".Compile<int, int>("a")(4));

[Fact]
// (4 + 3i)^2 is 16 - 9 + 24i exactly, as evaluation has it. Complex.Pow, which this compared with, rounds its
// e^(2 ln z) to (7.000000000000001, 24). https://github.com/asc-community/AngouriMath/issues/2247
public void TestUpcastDowncast5()
=> Assert.Equal(Complex.Pow(new(4, 3), 2), "a ^ 2".Compile<Complex, Complex>("a")(new(4, 3)));
=> Assert.Equal(new Complex(7, 24), "a ^ 2".Compile<Complex, Complex>("a")(new(4, 3)));

[Fact]
public void TestUpcastDowncast6() // | | | | |
Expand Down
82 changes: 82 additions & 0 deletions Sources/Tests/UnitTests/Common/ComplexArithmeticCompilationTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
//
// 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.Numerics;
using AngouriMath;
using AngouriMath.Extensions;
using Xunit;

namespace AngouriMath.Tests.Common
{
/// <summary>
/// Compiled arithmetic of numbers that are not real is evaluation's at a whole power and at an infinity. A whole power
/// is a product of its factors, so <c>i^2</c> is <c>-1</c> where <see cref="Complex.Pow(Complex, Complex)"/> made it
/// <c>(-1, 1.2e-16)</c>; a part that is 0 times an infinite part is 0, so <c>i * x</c> at <c>+oo</c> is <c>+oo i</c> and
/// not <c>(NaN, ∞)</c>; a quotient by a real number is taken part by part; and <c>sgn(x)</c> at an infinity is its sign.
/// <a href="https://github.com/asc-community/AngouriMath/issues/2247">#2247</a>
/// </summary>
[Trait("Area", "Common")]
public sealed class ComplexArithmeticCompilationTest
{
private static Complex Numeric(string value) => ((Entity.Number.Complex)value.ToEntity().Evaled).ToNumerics();

private static Complex Evaluated(string expression, string at)
=> ((Entity.Number.Complex)expression.ToEntity().Substitute("x", at.ToEntity()).Evaled).ToNumerics();

private static bool HasNaN(Complex z) => double.IsNaN(z.Real) || double.IsNaN(z.Imaginary);

private static void BothCompilersGive(string expression, string at, System.Func<Complex, Complex, bool> agree)
{
var expected = Evaluated(expression, at);
var fast = expression.ToEntity().Compile("x").Call(Numeric(at));
var linq = expression.ToEntity().Compile<Complex, Complex>("x")(Numeric(at));
Assert.True(agree(expected, fast), $"{expression} at {at}: {expected} evaluated, {fast} by FastExpression");
Assert.True(agree(expected, linq), $"{expression} at {at}: {expected} evaluated, {linq} by LINQ");
}

/// <summary>The parts evaluation has, to the last place, and a 0 part exactly 0.</summary>
[Theory]
[InlineData("x^2", "i")]
[InlineData("x^(-2)", "i")]
[InlineData("x^3", "1 + i")]
[InlineData("x^4", "1 - i")]
[InlineData("x^7", "2i")]
[InlineData("x^(-3)", "2i")]
public void AWholePowerIsAProduct(string expression, string at)
=> BothCompilersGive(expression, at, (expected, actual) => expected == actual);

/// <summary><c>(pi i)^2</c> is <c>-pi^2</c>, a real number, so a comparison has a truth value.</summary>
[Theory]
[InlineData("iverson((x * i)^2 > 0)", "3", 0)]
[InlineData("iverson((x * i)^2 < 0)", "3", 1)]
[InlineData("iverson((x * i)^2 < 0)", "pi", 1)]
[InlineData("iverson((x + x * i)^4 < 0)", "1/2", 1)]
public void ASquareOfAnImaginaryNumberIsReal(string expression, string at, double truth)
=> BothCompilersGive(expression, at, (expected, actual) => expected == truth && actual == truth);

[Theory]
[InlineData("i * x", "+oo")]
[InlineData("i * x", "-oo")]
[InlineData("x * (2 + 3i)", "-oo")]
[InlineData("(x + i) * 2", "-oo")]
[InlineData("(x - 3i) * (-1/2)", "+oo")]
[InlineData("ln(x) / 2", "-oo")]
[InlineData("(x + i) / 4", "+oo")]
[InlineData("ln(ln(x))", "0")]
[InlineData("sgn(x)", "+oo")]
[InlineData("sgn(x)", "-oo")]
public void AtAnInfinityTheValueIsEvaluations(string expression, string at)
=> BothCompilersGive(expression, at, (expected, actual) => !HasNaN(expected) && expected == actual);

/// <summary>A number that is 0 times an infinite one has no value, as <c>0 * oo</c> has none.</summary>
[Theory]
[InlineData("x * 0", "+oo + i")]
[InlineData("x * 0", "i * (+oo)")]
public void ZeroTimesAnInfinityHasNoValue(string expression, string at)
=> BothCompilersGive(expression, at, (expected, actual) => HasNaN(expected) && HasNaN(actual));
}
}
Loading