diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index d7ddd2dce..67fdc5b05 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -575,6 +575,24 @@ was ([#718](https://github.com/asc-community/AngouriMath/issues/718)). | `"x * sqrt(c - a*c*x) / e^(3 * atanh(a*x))".Integrate("x")` | `integral(…)` — left unevaluated, with `e` evaluated to a hundred digits inside it | an antiderivative in `sqrt(c - a c x)`, `provided c - a * c * x >= 0` | | `"1/sqrt(x + x^(3/2))".Integrate("x")` | `integral(1 / sqrt(x + x ^ (3/2)), x)` — left unevaluated | `2 * sqrt(1 + sqrt(x)) / (1/2) + C provided x >= 0` | +### A zero imaginary part is on the real axis, whatever its sign + +**Silent, with the downcasting off.** A decimal's negative zero — `0E-100 * -0.43` is `-0E-102` +— is an artefact of the exponent arithmetic and not a limit from below, but the phase read +`-pi` off its sign the way `Math.Atan2` does, and with `DowncastingEnabled` off, where +`Complex.Create` keeps `-1.54 - 0i` a `Complex`, a power went through the polar form on the +wrong side of the negative axis: `(-1.54)^(-1/2)` was `+0.80 i` with the setting off and +`-0.80 i` with it on. The setting decided a branch, and a numerical check that turns it off +called a right antiderivative wrong. Now a number on the negative axis has argument `pi` for +either zero, a complex with an exactly zero imaginary part takes the real paths of `Pow`, and +the square root's sign is read from a strictly negative imaginary part +([#1378](https://github.com/asc-community/AngouriMath/issues/1378)). Nothing changes with +the downcasting on. + +| Input, under `MathS.Settings.DowncastingEnabled.As(false, …)` | Was (2.5.0) | Now | +|---|---|---| +| `"(2*1.44*(1/sqrt(-5.286))^2-1)^(-1/2)".ToEntity().Evaled` | `5.5500952471953097…` | `-0.804560841963042…i`, as with the downcasting on | +| `"((1/sqrt(-5.286))*(2*1.44*(1/sqrt(-5.286))^2-1)^(-1/2))^(-3)".ToEntity().Evaled` | `23.335336258472899…` | `-23.335336258472899…`, as with the downcasting on | ### `subset` is a statement between sets, sets are equal by double containment, and `powerset` is a function `A subset B` — also `A ⊆ B`, and `B superset A` / `B ⊇ A` for the same node — is the statement, diff --git a/Sources/.editorconfig b/Sources/.editorconfig index 4374c6b05..1b37f71fd 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -291,6 +291,9 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed [AngouriMath/Functions/NumberTheory/ResidueClasses.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/Core/SignedZeroBranchTest.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 + [AngouriMath/Core/Entity/Omni/Sets/SetOperators.Subset.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/Core/Entity/Continuous/Number/Operators.cs b/Sources/AngouriMath/Core/Entity/Continuous/Number/Operators.cs index 2065d5144..7373218ca 100644 --- a/Sources/AngouriMath/Core/Entity/Continuous/Number/Operators.cs +++ b/Sources/AngouriMath/Core/Entity/Continuous/Number/Operators.cs @@ -418,7 +418,9 @@ public static Complex Sqrt(Complex num) else { y = Hypot(re, im, context).Subtract(re, context).Divide(2, context).SqrtByIntegerRoot(context); - if (im.IsNegative) y = -y; + // Strictly negative: a negative zero is on the axis, where the root is + // +i times the root of the modulus, as for any negative real. + if (im.Sign < 0) y = -y; x = im.Divide(y.Multiply(2, context), context); } return Complex.Create(x, y); @@ -461,6 +463,18 @@ static Complex BinaryIntPow(Complex num, EInteger val) var squared = half * half; return divRem[1].IsZero ? squared : squared * BinaryIntPow(num, divRem[1]); } + // A complex whose imaginary part is exactly zero is the real number it is, and + // takes the real paths below: with the downcasting off, Complex.Create keeps + // `-1.54 - 0i` a Complex, and through the polar form the sign of that zero + // put the number a hair below the negative axis -- `(-1.54)^(-1/2)` came back + // `+0.80 i` there and `-0.80 i` with the downcasting on. The phase reads a + // zero as a zero now, and a real base gets the exact half-power arithmetic + // rather than a cosine of pi/2 at a hundred digits. + // https://github.com/asc-community/AngouriMath/issues/1378 + if (@base is not Real && @base.ImaginaryPart.EDecimal.IsZero) + @base = Real.Create(@base.RealPart.EDecimal); + if (power is not Real && power.ImaginaryPart.EDecimal.IsZero) + power = Real.Create(power.RealPart.EDecimal); // e to a real power is the exponential of the power: raising the constant's // hundred digits loses to the power what it multiplies their error by -- // e^700 came back to ninety-seven digits -- and the exponential is a series diff --git a/Sources/AngouriMath/Functions/InternalAMExtensions.cs b/Sources/AngouriMath/Functions/InternalAMExtensions.cs index 558c950ac..2d9892f19 100644 --- a/Sources/AngouriMath/Functions/InternalAMExtensions.cs +++ b/Sources/AngouriMath/Functions/InternalAMExtensions.cs @@ -686,6 +686,17 @@ public static EDecimal Acos(this EDecimal x, EContext context) /// Analogy of for more see this /// /// + /// + /// A zero is a zero, whatever its sign. Math.Atan2 reads a negative zero + /// as a limit from below and answers -pi on the negative axis for it, and this + /// did the same. A decimal's negative zero is not a limit: it is what + /// 0E-100 * -0.43 multiplies to, an artefact of the exponent arithmetic, and + /// reading -pi off it put -1.54 - 0i a hair below the negative axis, so + /// that (-1.54)^(-1/2) came back +0.80 i with the downcasting off and + /// -0.80 i with it on -- the setting deciding a branch. A number on the negative + /// axis has argument pi. EDecimal.Sign is 0 for either zero, and is what + /// is read here. https://github.com/asc-community/AngouriMath/issues/1378 + /// public static EDecimal Arctan2(this EDecimal y, EDecimal x, EContext context) { if (y.IsNaN() || x.IsNaN()) return EDecimal.NaN; @@ -702,22 +713,18 @@ public static EDecimal Arctan2(this EDecimal y, EDecimal x, EContext context) (_, inf) => consts.HalfPi, (_, -inf) => -consts.HalfPi, (-inf, -1) => -consts.Pi, - (-inf, 0) => y.IsNegative ? -consts.Pi : consts.Pi, + (-inf, 0) => consts.Pi, (-inf, 1) => consts.Pi, (inf, _) => EDecimal.Zero, (1, _) => Arctan(y.Divide(x, context), context), (-1, -1) => Arctan(y.Divide(x, context), context).Subtract(consts.Pi, context), - (-1, 0) => y.IsNegative ? -consts.Pi : consts.Pi, + (-1, 0) => consts.Pi, (-1, 1) => Arctan(y.Divide(x, context), context).Add(consts.Pi, context), (0, 1) => consts.HalfPi, (0, -1) => -consts.HalfPi, - (0, 0) => (x.IsNegative, y.IsNegative) switch - { - (true, true) => -consts.Pi, - (false, true) => EDecimal.NegativeZero, - (true, false) => consts.Pi, - (false, false) => EDecimal.Zero, - }, + // The origin: pi for a negative zero on the x axis, as Math.Atan2 has it, and + // zero otherwise -- the sign of y decides nothing, see above. + (0, 0) => x.IsNegative ? consts.Pi : EDecimal.Zero, _ => throw new AngouriBugException("Unexpected scenario"), }; } diff --git a/Sources/Tests/UnitTests/Core/SignedZeroBranchTest.cs b/Sources/Tests/UnitTests/Core/SignedZeroBranchTest.cs new file mode 100644 index 000000000..125c346e3 --- /dev/null +++ b/Sources/Tests/UnitTests/Core/SignedZeroBranchTest.cs @@ -0,0 +1,66 @@ +// +// 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.Extensions; +using PeterO.Numbers; +using Xunit; +using static AngouriMath.Entity.Number; + +namespace AngouriMath.Tests.Core +{ + /// + /// A decimal's negative zero is an artefact of the exponent arithmetic -- 0E-100 * -0.43 + /// is -0E-102 -- and not a limit from below, so a number on the negative axis has + /// argument pi whatever the sign of its zero imaginary part, and its powers, roots + /// and logarithm are the principal ones. With the downcasting off Complex.Create + /// keeps -1.54 - 0i a Complex, and the phase read -pi off the zero's sign: + /// (-1.54)^(-1/2) was +0.80 i there and -0.80 i with the downcasting on, + /// the setting deciding a branch, and a numerical check that turns the downcasting off called + /// a right antiderivative wrong on the strength of it. + /// + /// + [Trait("Area", "Core")] + public sealed class SignedZeroBranchTest + { + private static Complex Evaluated(string expression, bool downcasting) + => MathS.Settings.DowncastingEnabled.As(downcasting, () => (Complex)expression.ToEntity().Evaled); + + private static void AssertClose(Complex expected, Complex actual, string what) + { + var re = Math.Abs((double)(expected.RealPart - actual.RealPart)); + var im = Math.Abs((double)(expected.ImaginaryPart - actual.ImaginaryPart)); + Assert.True(re < 1e-40 && im < 1e-40, $"{what}: expected {expected}, got {actual}"); + } + + /// The same value with the downcasting on and off, and it is the principal one. + [Theory] + [InlineData("(2*1.44*(1/sqrt(-5.286))^2-1)^(-1/2)")] + [InlineData("((1/sqrt(-5.286))*(2*1.44*(1/sqrt(-5.286))^2-1)^(-1/2))^(-3)")] + [InlineData("sqrt((1/sqrt(-2))^2 * 3)")] + [InlineData("ln((1/sqrt(-2))^2)")] + public void TheSettingDoesNotDecideTheBranch(string expression) + { + var on = Evaluated(expression, downcasting: true); + var off = Evaluated(expression, downcasting: false); + AssertClose(on, off, expression); + } + + [Fact] + public void ANegativeZeroImaginaryPartIsOnTheAxis() + { + MathS.Settings.DowncastingEnabled.As(false, () => + { + var minusFour = Complex.Create(EDecimal.FromInt32(-4), EDecimal.NegativeZero); + AssertClose(Complex.Create(0, 2), Complex.Sqrt(minusFour), "sqrt(-4 - 0i)"); + AssertClose(Complex.Create(0, 2), Complex.Pow(minusFour, Rational.Create(1, 2)), "(-4 - 0i)^(1/2)"); + AssertClose(Complex.Create(EDecimal.FromInt32(4).Log(MathS.Settings.DecimalPrecisionContext), MathS.DecimalConst.pi), + Complex.Ln(minusFour), "ln(-4 - 0i)"); + }); + } + } +}