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