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
18 changes: 18 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions Sources/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
16 changes: 15 additions & 1 deletion Sources/AngouriMath/Core/Entity/Continuous/Number/Operators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down
25 changes: 16 additions & 9 deletions Sources/AngouriMath/Functions/InternalAMExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,17 @@ public static EDecimal Acos(this EDecimal x, EContext context)
/// Analogy of <see cref="Math.Atan2(double, double)"/> for more see this
/// <img src="http://i.imgur.com/TRLjs8R.png"/>
/// </summary>
/// <remarks>
/// <b>A zero is a zero, whatever its sign.</b> <c>Math.Atan2</c> reads a negative zero
/// as a limit from below and answers <c>-pi</c> on the negative axis for it, and this
/// did the same. A decimal's negative zero is not a limit: it is what
/// <c>0E-100 * -0.43</c> multiplies to, an artefact of the exponent arithmetic, and
/// reading <c>-pi</c> off it put <c>-1.54 - 0i</c> a hair below the negative axis, so
/// that <c>(-1.54)^(-1/2)</c> came back <c>+0.80 i</c> with the downcasting off and
/// <c>-0.80 i</c> with it on -- the setting deciding a branch. A number on the negative
/// axis has argument <c>pi</c>. <c>EDecimal.Sign</c> is 0 for either zero, and is what
/// is read here. https://github.com/asc-community/AngouriMath/issues/1378
/// </remarks>
public static EDecimal Arctan2(this EDecimal y, EDecimal x, EContext context)
{
if (y.IsNaN() || x.IsNaN()) return EDecimal.NaN;
Expand All @@ -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"),
};
}
Expand Down
66 changes: 66 additions & 0 deletions Sources/Tests/UnitTests/Core/SignedZeroBranchTest.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// A decimal's negative zero is an artefact of the exponent arithmetic -- <c>0E-100 * -0.43</c>
/// is <c>-0E-102</c> -- and not a limit from below, so a number on the negative axis has
/// argument <c>pi</c> whatever the sign of its zero imaginary part, and its powers, roots
/// and logarithm are the principal ones. With the downcasting off <c>Complex.Create</c>
/// keeps <c>-1.54 - 0i</c> a Complex, and the phase read <c>-pi</c> off the zero's sign:
/// <c>(-1.54)^(-1/2)</c> was <c>+0.80 i</c> there and <c>-0.80 i</c> 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.
/// <see href="https://github.com/asc-community/AngouriMath/issues/1378"/>
/// </summary>
[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}");
}

/// <summary>The same value with the downcasting on and off, and it is the principal one.</summary>
[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)");
});
}
}
}
Loading