From a283e744b6932a082490223e7207b50b85eaaba7 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Fri, 30 Oct 2026 14:20:25 +0000 Subject: [PATCH] Compiled arithmetic of a complex number is evaluation's at a whole power and at an infinity A compiled whole power of a number that is not real went through Complex.Pow, which takes e^(n ln z) and leaves a rounded part: i^2 was (-1, 1.2e-16), which is not real, so iverson((x i)^2 < 0) at 3 was NaN where it is 1. RealProducts.Power now takes such a power as a product of its factors, by squaring, as evaluation's BinaryIntPow does. Past 2^62 it is left to Complex.Pow. System.Numerics.Complex's product takes 0 * oo as NaN in every part, so i * x at +oo was (NaN, oo) and (x + i) * 2 at -oo was (-oo, NaN). Number.OpMul takes a zero part times an infinite part as 0, and a whole zero times an infinity as NaN (#1186), and RealProducts.Product now does the same. A quotient by a real number other than 0 is taken part by part, where Complex multiplied each part by the divisor's imaginary 0: ln(x) / 2 at -oo was (NaN, NaN), and ln(ln(x)) at 0 was (oo, NaN), through ComplexBranches.Log's quotient by ln(e). The compiled sgn was z / |z|, which is oo / oo at an infinity. A real number's is now its sign, as evaluation's is. ComplexArithmeticCompilationTest compares 22 values under both compilers with evaluation, and 19 fail on master. The other three pin what has not changed: x * (2 + 3i) at -oo, and a zero times an infinity, which has no value. CompilationIntoLinqTest.TestUpcastDowncast5 compared (4 + 3i)^2 with Complex.Pow's (7.000000000000001, 24). It is 16 - 9 + 24i exactly, as evaluation has it, and the compiled power is that now. Found by work/compilesweep. Fixes #2247. Co-Authored-By: Claude Opus 5.5 (1M context) --- BREAKING-CHANGES.md | 22 +++++ Sources/.editorconfig | 3 + .../Functions/Compilation/RealProducts.cs | 59 +++++++++++-- .../Functions/InternalAMExtensions.cs | 8 +- .../Common/CompilationIntoLinqTest.cs | 4 +- .../ComplexArithmeticCompilationTest.cs | 82 +++++++++++++++++++ 6 files changed, 171 insertions(+), 7 deletions(-) create mode 100644 Sources/Tests/UnitTests/Common/ComplexArithmeticCompilationTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index fef54c9ea..669f545b4 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -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("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("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 diff --git a/Sources/.editorconfig b/Sources/.editorconfig index 006adb650..a7fe50676 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -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 diff --git a/Sources/AngouriMath/Functions/Compilation/RealProducts.cs b/Sources/AngouriMath/Functions/Compilation/RealProducts.cs index 245d210fc..e7ae386e1 100644 --- a/Sources/AngouriMath/Functions/Compilation/RealProducts.cs +++ b/Sources/AngouriMath/Functions/Compilation/RealProducts.cs @@ -19,15 +19,37 @@ namespace AngouriMath.Core.Compilation /// internal static class RealProducts { + /// + /// The product as evaluation takes it: a part that is 0 times a part that is infinite is 0, so i times + /// +oo is +oo i, and a number that is 0 times an infinite one is NaN, as 0 * oo is. + /// took 0 · ∞ as NaN in every part, so i * x at +oo + /// was (NaN, ∞). https://github.com/asc-community/AngouriMath/issues/2247 + /// 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); + } /// - /// The quotient, real where both are real and the divisor is not 0. A division by 0 is left to - /// , whose NaN is evaluation's: 1/0 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: multiplied each part by + /// the divisor's imaginary 0, so ln(-oo), +oo + pi i, over 1 was (∞, NaN). A division by 0 + /// is left to , whose NaN is evaluation's: 1/0 has no value. + /// https://github.com/asc-community/AngouriMath/issues/2247 /// 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; /// /// The power, real where both are real and it is: a positive base, or a negative one to a whole power. @@ -38,6 +60,8 @@ public static NumericsComplex Quotient(NumericsComplex a, NumericsComplex b) /// 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); @@ -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; + + /// + /// A whole power of a number that is not real, as a product of its factors, which is how evaluation takes it. + /// takes e^(n ln z), + /// whose rounded angle leaves a part that should be 0 at about 1e-16: i^2 was (-1, 1.2e-16), which is + /// not real, so (i pi)^2 > 0 had no truth value where it is false. Past 2^62 the exponent is + /// left to it, where a product would take more than 124 multiplications. + /// https://github.com/asc-community/AngouriMath/issues/2247 + /// + 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; + } } } diff --git a/Sources/AngouriMath/Functions/InternalAMExtensions.cs b/Sources/AngouriMath/Functions/InternalAMExtensions.cs index b3ef7078e..848eb7704 100644 --- a/Sources/AngouriMath/Functions/InternalAMExtensions.cs +++ b/Sources/AngouriMath/Functions/InternalAMExtensions.cs @@ -180,8 +180,14 @@ public static IEnumerable IntersectSequences(this (IEnumerable, IEnumer yield return s; } + /// + /// z / |z|, and the sign of a real number, so that it is 1 at +oo and -1 at -oo, as + /// evaluation has it, where z / |z| was ∞ / ∞, NaN. https://github.com/asc-community/AngouriMath/issues/2247 + /// 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); diff --git a/Sources/Tests/UnitTests/Common/CompilationIntoLinqTest.cs b/Sources/Tests/UnitTests/Common/CompilationIntoLinqTest.cs index a7d17da85..2c0357d9b 100644 --- a/Sources/Tests/UnitTests/Common/CompilationIntoLinqTest.cs +++ b/Sources/Tests/UnitTests/Common/CompilationIntoLinqTest.cs @@ -389,8 +389,10 @@ public void TestUpcastDowncast4() => Assert.Equal((int)Math.Sin(4), "sin(a)".Compile("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("a")(new(4, 3))); + => Assert.Equal(new Complex(7, 24), "a ^ 2".Compile("a")(new(4, 3))); [Fact] public void TestUpcastDowncast6() // | | | | | diff --git a/Sources/Tests/UnitTests/Common/ComplexArithmeticCompilationTest.cs b/Sources/Tests/UnitTests/Common/ComplexArithmeticCompilationTest.cs new file mode 100644 index 000000000..d83d125aa --- /dev/null +++ b/Sources/Tests/UnitTests/Common/ComplexArithmeticCompilationTest.cs @@ -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 +{ + /// + /// 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 i^2 is -1 where made it + /// (-1, 1.2e-16); a part that is 0 times an infinite part is 0, so i * x at +oo is +oo i and + /// not (NaN, ∞); a quotient by a real number is taken part by part; and sgn(x) at an infinity is its sign. + /// #2247 + /// + [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 agree) + { + var expected = Evaluated(expression, at); + var fast = expression.ToEntity().Compile("x").Call(Numeric(at)); + var linq = expression.ToEntity().Compile("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"); + } + + /// The parts evaluation has, to the last place, and a 0 part exactly 0. + [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); + + /// (pi i)^2 is -pi^2, a real number, so a comparison has a truth value. + [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); + + /// A number that is 0 times an infinite one has no value, as 0 * oo has none. + [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)); + } +}