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)); + } +}