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
28 changes: 28 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,34 @@ is rounded as before: `(Entity)(1m / 3)` is still `1/3` ([#1944](https://github.
| The derivative of the antiderivative of `1/sqrt(a + i*a*sinh(c + d*x))` at `a, c, d, x = 21/10, 19/10, 17/10, 227283/1250`, at 2000 digits | `2.0185464…E-68` | `2.0185464…E-68 - 2.0185464…E-68i` |
| `"1/3 + 1e-60".EvalNumerical()`, `"sin(pi)".EvalNumerical()`, `(Entity)(1.0 / 3)` | `1/3`, `0`, `1/3` | `1/3`, `0`, `1/3` |

### A computed value is zero only where its terms cancelled

**Silent: wrong answers, and values where there was NaN.** A number an evaluation produced was rounded onto zero below
half the working digits outright, `1e-50` at the default hundred. That held for any number, whatever produced it. So
`e^(-155)` was `0` at a hundred digits, the modulus of a complex number near `1e-136` was `0`, and a quotient by that
modulus was `NaN`. The derivative of a right antiderivative of `1/sqrt(a + i a sinh(c + d x))` at a large argument was
therefore `NaN` at up to two hundred digits and `1.5329…` at three hundred, where it is `2.0185…E-68 - 2.0185…E-68i`.

A number is now rounded onto zero only as a residual of the cancellation that produced it: a difference below half the
working digits of its larger term. A product, a quotient, a power, a root, an exponential or a modulus cancels nothing
and is not rounded onto zero, and a cosine's or a sine's residual is measured against the modulus it multiplies.
`sin(pi)`, `e^(i pi) + 1` and `sqrt(2) sqrt(3) - sqrt(6)` are still `0`. A quotient of decimals is rounded to the
working precision's number of decimal places, as it was, and one that has lost digits that way is divided again to the
precision's digits: a quotient of `3.5e-68` kept thirty-three digits, and one below `1e-100` was `0`. The derivative above is right from two hundred digits; its terms cancel in their sixty-eighth digit,
so at fewer than about twice that many digits the difference is below half of them and reads as a residual
([#1944](https://github.com/asc-community/AngouriMath/issues/1944)).

| Input, at the default hundred digits unless stated | Was (2.6.0) | Now |
|---|---|---|
| `"e^(-155)".EvalNumerical()` | `0` | `4.8345416380533358…E-68` |
| `"e^(-60) * e^(-60)".EvalNumerical()` | `0` | `7.6676480737219996…E-53` |
| `"1/(10^67*sqrt(2) + 10^67*sqrt(2)*i)".EvalNumerical()` | `0` | `3.5355339059327376…E-68 - 3.5355339059327376…E-68i` |
| `"sqrt(-e^(-300))".EvalNumerical()` | `0` | `7.1750959731644104…E-66i` |
| The derivative above, at 300 digits | `1.5329402…` | `2.0185464…E-68 - 2.0185464…E-68i` |
| The derivative above, at 200 digits | `NaN` | `2.0185464…E-68 - 2.0185464…E-68i` |
| The derivative above, at 100 digits | `NaN` | `2.0185464…E-68`, without its imaginary part: a residual at that precision |
| `"sin(pi)".EvalNumerical()`, `"(1 + sqrt(2) i) (1 - sqrt(2) i)".EvalNumerical()` | `0`, `3` | `0`, `3` |

---

## 2.6.0 — since 2.5.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,27 +80,36 @@ public static Complex Create(Real real, Real imaginary) =>
/// <summary>
/// Creates an instance of Complex
/// </summary>
public static Complex Create(EDecimal real, EDecimal imaginary) => Create(real, imaginary, fromMachineNumbers: false);
public static Complex Create(EDecimal real, EDecimal imaginary) => Create(real, imaginary, EDecimal.One, EDecimal.One, fromMachineNumbers: false);

/// <summary>
/// A number an arithmetic operation produced, each part zero only as a residual of the cancellation that
/// produced it, as <see cref="Real.CreateAtScale(EDecimal, EDecimal)"/> reads one.
/// https://github.com/asc-community/AngouriMath/issues/1944
/// </summary>
internal static Complex CreateAtScales(EDecimal real, EDecimal imaginary, EDecimal realScale, EDecimal imaginaryScale)
=> Create(real, imaginary, realScale, imaginaryScale, fromMachineNumbers: false);

/// <summary>
/// A number from a pair of <see cref="double"/>s, each part rounded as
/// <see cref="Real.FromMachineNumber(EDecimal)"/> rounds it.
/// </summary>
internal static Complex FromMachineNumbers(EDecimal real, EDecimal imaginary) => Create(real, imaginary, fromMachineNumbers: true);
internal static Complex FromMachineNumbers(EDecimal real, EDecimal imaginary) => Create(real, imaginary, EDecimal.One, EDecimal.One, fromMachineNumbers: true);

private static Complex Create(EDecimal real, EDecimal imaginary, bool fromMachineNumbers)
private static Complex Create(EDecimal real, EDecimal imaginary, EDecimal realScale, EDecimal imaginaryScale, bool fromMachineNumbers)
{
if (!MathS.Settings.DowncastingEnabled)
return new Complex(real, imaginary);
Real Part(EDecimal part) => fromMachineNumbers ? Real.FromMachineNumber(part) : Real.Create(part);
Real Part(EDecimal part, EDecimal scale) => fromMachineNumbers ? Real.FromMachineNumber(part) : Real.CreateAtScale(part, scale);
if (imaginary.IsZero)
return Part(real);
return Part(real, realScale);
if (real.IsNaN() || imaginary.IsNaN())
return Real.NaN;
if (imaginary.IsFinite && imaginary.Abs().LessThan(MathS.Settings.DowncastingTolerance))
return Part(real);
if (imaginary.IsFinite && !imaginaryScale.IsZero && imaginary.Abs().LessThan(
ReferenceEquals(imaginaryScale, EDecimal.One) ? MathS.Settings.DowncastingTolerance : MathS.Settings.DowncastingTolerance.Multiply(imaginaryScale.Abs())))
return Part(real, realScale);
else
return new Complex(Part(real), Part(imaginary));
return new Complex(Part(real, realScale), Part(imaginary, imaginaryScale));
}

/// <summary>
Expand All @@ -122,8 +131,10 @@ public void Deconstruct(out Real realPart, out Real imaginaryPart) =>
/// https://github.com/asc-community/AngouriMath/issues/1615
/// </remarks>
public new virtual Real Abs()
=> Real.Create((RealPart.EDecimal * RealPart.EDecimal + ImaginaryPart.EDecimal * ImaginaryPart.EDecimal)
.SqrtByIntegerRoot(MathS.Settings.DecimalPrecisionContext));
// A modulus is never a residual of cancellation: the sum of two squares cancels nothing.
// https://github.com/asc-community/AngouriMath/issues/1944
=> Real.CreateAtScale((RealPart.EDecimal * RealPart.EDecimal + ImaginaryPart.EDecimal * ImaginaryPart.EDecimal)
.SqrtByIntegerRoot(MathS.Settings.DecimalPrecisionContext), EDecimal.Zero);

/// <summary>
/// The phase of a complex number (aka angle)
Expand All @@ -136,7 +147,9 @@ public void Deconstruct(out Real realPart, out Real imaginaryPart) =>
public static Complex CreatePolar(EDecimal magnitude, EDecimal phase)
{
var context = MathS.Settings.DecimalPrecisionContext;
return Create(magnitude.Multiply(phase.Cos(context), context), magnitude.Multiply(phase.Sin(context), context));
// The residual of a cosine or a sine is the magnitude's: cos(pi/2) leaves 1e-100 of it.
var scale = magnitude.Abs();
return CreateAtScales(magnitude.Multiply(phase.Cos(context), context), magnitude.Multiply(phase.Sin(context), context), scale, scale);
}

/// <summary>-oo + -ooi</summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,19 +65,30 @@ private protected Real() : base(null, null) { }
/// such logarithms lost its imaginary part.
/// https://github.com/asc-community/AngouriMath/issues/1944
/// </remarks>
public static Real Create(EDecimal value) => Create(value, fromMachineNumber: false);
public static Real Create(EDecimal value) => Create(value, fromMachineNumber: false, EDecimal.One);

/// <summary>
/// A value an arithmetic operation produced, which is a residual of cancellation, and zero, only below
/// half the working digits of <paramref name="cancelledScale"/>, the size of the terms that cancelled;
/// zero for a value no cancellation produced -- a product, a quotient or a power of nonzero values --
/// which is never rounded onto zero. With the floor at half the working digits outright, every value
/// below 1e-50 was zero at a hundred digits, e^(-155) among them, and a modulus near 1e-136 was zero, so
/// that a quotient by it was NaN. https://github.com/asc-community/AngouriMath/issues/1944
/// </summary>
internal static Real CreateAtScale(EDecimal value, EDecimal cancelledScale)
=> Create(value, fromMachineNumber: false, cancelledScale);

/// <summary>
/// A number from a <see cref="float"/>, a <see cref="double"/> or a <see cref="decimal"/>, which carries
/// that type's digits and not the working precision's: it is the small rational it is near within
/// <see cref="MathS.Settings.PrecisionErrorZeroRange"/>, ten to the minus sixteen, so that <c>1m/3</c>
/// is <c>1/3</c>. https://github.com/asc-community/AngouriMath/issues/1944
/// </summary>
internal static Real FromMachineNumber(EDecimal value) => Create(value, fromMachineNumber: true);
internal static Real FromMachineNumber(EDecimal value) => Create(value, fromMachineNumber: true, EDecimal.One);

// The tolerance is made only past the cheap check below, which almost every value stops at: made at
// the call, it was 580 more bytes on each evaluation of a cubic at a hundred digits.
private static Real Create(EDecimal value, bool fromMachineNumber)
private static Real Create(EDecimal value, bool fromMachineNumber, EDecimal cancelledScale)
{
if (!MathS.Settings.DowncastingEnabled)
return new Real(value);
Expand All @@ -91,11 +102,14 @@ private static Real Create(EDecimal value, bool fromMachineNumber)
if (!Rational.MayBeASmallRational(value, MathS.Settings.FloatToRationalIterCount, MathS.Settings.DowncastingToleranceAsDouble))
return new Real(value);
var (intPart, intRest) = value.SplitDecimal();
// If the difference between value & round(value) is zero (see Number.IsZero), we consider value as an integer
// If the difference between value & round(value) is zero (see Number.IsZero), we consider value as an integer.
// Onto zero only as a residual of the cancellation that produced it, below the tolerance times the
// scale of what cancelled; a negative one reaches zero by rounding up.
var tolerance = MathS.Settings.DowncastingTolerance;
if (intRest.LessThan(tolerance))
bool IsAResidual() => value.IsZero || !cancelledScale.IsZero && value.Abs().LessThan(tolerance.Multiply(cancelledScale.Abs()));
if (intRest.LessThan(tolerance) && (!intPart.IsZero || IsAResidual()))
return Integer.Create(intPart);
if (intRest.GreaterThan(1 - tolerance))
if (intRest.GreaterThan(1 - tolerance) && (!intPart.Increment().IsZero || IsAResidual()))
return Integer.Create(intPart.Increment());

var attempt = Rational.FindRationalWithin(value, fromMachineNumber ? MathS.Settings.PrecisionErrorZeroRange : tolerance);
Expand Down
Loading
Loading