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
17 changes: 17 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,23 @@ so at fewer than about twice that many digits the difference is below half of th
| 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` |

### A function of a small argument is its value, not zero

**Silent: wrong answers.** A function's value was rounded onto zero below half the working digits outright, for the
residual a function leaves at its own zeros, such as `sin(pi)`'s. That residual is relative to the argument, and a
function of an exact small argument cancels nothing, so `sin(10^(-60))` was `0` at the default hundred digits. The
sine, cosine, tangent, logarithm, arcsine, arccosine and arctangent of a real argument, and the error functions and
`Si` and `Shi`, now measure their residual against their argument. `sin(pi)`, `cos(pi/2)` and `tan(pi)` are still `0`
([#2074](https://github.com/asc-community/AngouriMath/issues/2074)).

| Input | Was (2.6.0) | Now |
|---|---|---|
| `"sin(10^(-60))".EvalNumerical()` | `0` | `1E-60`, to the hundredth digit |
| `"arctan(10^(-60))".EvalNumerical()` | `0` | `1E-60`, to the hundredth digit |
| `"erf(10^(-60))".EvalNumerical()` | `0` | `1.1283791670955125…E-60` |
| `"Si(10^(-60))".EvalNumerical()` | `0` | `1E-60`, to the hundredth digit |
| `"sin(pi)".EvalNumerical()` | `0` | `0` |

---

## 2.6.0 — since 2.5.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ public static Complex Erf(Complex z)
if (re.IsZero && im.IsZero)
return Integer.Zero;
var (erfRe, erfIm) = Erf(re, im, context, extraDigits: 0);
return Complex.Create(erfRe.RoundToPrecision(context), erfIm.RoundToPrecision(context));
// Its residual is its argument's: erf(1e-60) is 1.13e-60 and no residual of anything.
// https://github.com/asc-community/AngouriMath/issues/2074
var scale = im.IsZero ? re : Magnitude((re, im));
return Complex.CreateAtScales(erfRe.RoundToPrecision(context), erfIm.RoundToPrecision(context), scale, scale);
}

/// <summary>
Expand Down Expand Up @@ -115,7 +118,7 @@ public static Complex Erfi(Complex z)
if (x.IsInfinity())
return x.IsNegative ? Real.NegativeInfinity : Real.PositiveInfinity;
var context = MathS.Settings.DecimalPrecisionContext.Value;
return ErfiOfAReal(x, context).RoundToPrecision(context);
return Real.CreateAtScale(ErfiOfAReal(x, context).RoundToPrecision(context), x);
}
var erf = Erf(Complex.Create(z.ImaginaryPart.EDecimal.Negate(), z.RealPart.EDecimal));
// -i (a + i b) = b - i a
Expand Down
16 changes: 9 additions & 7 deletions Sources/AngouriMath/Core/Entity/Continuous/Number/Operators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -782,8 +782,10 @@ public static Complex Ln(Complex x)
var context = MathS.Settings.DecimalPrecisionContext;
if (LostToExponentRange(x) && x is Rational { ERational: { Sign: > 0 } ratio })
return LnOfRatio(ratio, context);
// A function's residual is its argument's: sin(pi) leaves about 1e-100 pi, where sin(1e-60)
// cancels nothing and is 1e-60. https://github.com/asc-community/AngouriMath/issues/2074
if (x is Real { EDecimal: { IsNegative: false } real })
return real.NaturalLogarithm(context);
return Real.CreateAtScale(real.NaturalLogarithm(context), real);
// From https://source.dot.net/#System.Runtime.Numerics/System/Numerics/Complex.cs,cf15f2e5cc49cef1
return Complex.Create(x.Abs().EDecimal.NaturalLogarithm(context), x.ImaginaryPart.EDecimal.Arctan2(x.RealPart.EDecimal, context));
}
Expand All @@ -793,7 +795,7 @@ public static Complex Sin(Complex num)
{
var context = MathS.Settings.DecimalPrecisionContext;
if (num is Real { EDecimal: var real })
return real.Sin(context);
return Real.CreateAtScale(real.Sin(context), real);
// From https://source.dot.net/#System.Runtime.Numerics/System/Numerics/Complex.cs,cf15f2e5cc49cef1
var (re, im) = (num.RealPart.EDecimal, num.ImaginaryPart.EDecimal);
// We need both sinh and cosh of imaginary part.
Expand Down Expand Up @@ -857,7 +859,7 @@ public static Complex Cos(Complex num)
{
var context = MathS.Settings.DecimalPrecisionContext;
if (num is Real { EDecimal: var real })
return real.Cos(context);
return Real.CreateAtScale(real.Cos(context), real);
// From https://source.dot.net/#System.Runtime.Numerics/System/Numerics/Complex.cs,cf15f2e5cc49cef1
var (re, im) = (num.RealPart.EDecimal, num.ImaginaryPart.EDecimal);
var p = im.Exponential(context);
Expand All @@ -874,7 +876,7 @@ public static Complex Tan(Complex num)
{
var context = MathS.Settings.DecimalPrecisionContext;
if (num is Real { EDecimal: var real })
return real.Tan(context);
return Real.CreateAtScale(real.Tan(context), real);
// From https://source.dot.net/#System.Runtime.Numerics/System/Numerics/Complex.cs,cf15f2e5cc49cef1

// tan z = sin z / cos z, but to avoid unnecessary repeated trig computations, use
Expand Down Expand Up @@ -954,7 +956,7 @@ public static Complex Arcsin(Complex num)
{
var context = MathS.Settings.DecimalPrecisionContext;
if (num is Real { EDecimal: var real } && !(real.GreaterThan(EDecimal.One) || real.LessThan(-EDecimal.One)))
return real.Arcsin(context);
return Real.CreateAtScale(real.Arcsin(context), real);
var (beta, v) = ArcSinCosInner(num, context);
return Complex.Create(beta.Arcsin(context), v);
}
Expand All @@ -964,7 +966,7 @@ public static Complex Arccos(Complex num)
{
var context = MathS.Settings.DecimalPrecisionContext;
if (num is Real { EDecimal: var real } && !(real.GreaterThan(EDecimal.One) || real.LessThan(-EDecimal.One)))
return real.Acos(context);
return Real.CreateAtScale(real.Acos(context), real);
var (beta, v) = ArcSinCosInner(num, context);
return Complex.Create(beta.Acos(context), -v);
}
Expand All @@ -974,7 +976,7 @@ public static Complex Arctan(Complex num)
{
var context = MathS.Settings.DecimalPrecisionContext;
if (num is Real { EDecimal: var real })
return real.Arctan(context);
return Real.CreateAtScale(real.Arctan(context), real);
// From https://source.dot.net/#System.Runtime.Numerics/System/Numerics/Complex.cs,cf15f2e5cc49cef1
var one = Integer.One;
var two = Integer.Create(2);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,13 @@ private static Complex TrigonometricIntegral(Complex z, bool hyperbolic, bool od
else
valueIm = im.IsNegative ? pi.Divide(-2, context) : pi.Divide(2, context);
}
// Si and Shi vanish at 0 like their argument, and their residual is its: Si(1e-60) is 1e-60.
// https://github.com/asc-community/AngouriMath/issues/2074
if (odd)
{
var scale = im.IsZero ? re : re.IsZero ? im : Magnitude((re, im));
return Complex.CreateAtScales(valueRe.RoundToPrecision(context), valueIm.RoundToPrecision(context), scale, scale);
}
return Complex.Create(valueRe.RoundToPrecision(context), valueIm.RoundToPrecision(context));
}

Expand Down
51 changes: 51 additions & 0 deletions Sources/Tests/UnitTests/Core/AFunctionOfASmallArgumentTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//
// 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 AngouriMath.Extensions;
using PeterO.Numbers;
using Xunit;
using static AngouriMath.Entity.Number;

namespace AngouriMath.Tests.Core
{
/// <summary>
/// A function's value is rounded onto zero only as the residual it leaves at its own zeros, which is relative
/// to its argument: <c>sin(pi)</c> leaves about <c>1e-100 pi</c>, where <c>sin(1e-60)</c> cancels nothing.
/// Every one of these was 0 at the default hundred digits. References from mpmath at 80 digits.
/// <a href="https://github.com/asc-community/AngouriMath/issues/2074">#2074</a>
/// </summary>
[Trait("Area", "Core")]
public sealed class AFunctionOfASmallArgumentTest
{
[Theory]
[InlineData("sin(10^(-60))", "1E-60")]
[InlineData("sin(-10^(-60))", "-1E-60")]
[InlineData("tan(10^(-60))", "1E-60")]
[InlineData("arcsin(10^(-60))", "1E-60")]
[InlineData("arctan(10^(-60))", "1E-60")]
[InlineData("erf(10^(-60))", "1.128379167095512573896158903121545171688101258658E-60")]
[InlineData("erfi(10^(-60))", "1.128379167095512573896158903121545171688101258658E-60")]
[InlineData("Si(10^(-60))", "1E-60")]
[InlineData("Shi(10^(-60))", "1E-60")]
public void IsItsValue(string expression, string reference)
{
var expected = EDecimal.FromString(reference);
var actual = Assert.IsType<Real>(expression.EvalNumerical()).EDecimal;
var error = actual.Subtract(expected, EContext.ForPrecision(200)).Abs().Divide(expected.Abs(), EContext.ForPrecision(10));
Assert.True(error.LessThan(EDecimal.FromString("1E-40")), $"{expression} is {actual}, where it is {expected}");
}

[Theory]
[InlineData("sin(pi)")]
[InlineData("sin(100*pi)")]
[InlineData("cos(pi/2)")]
[InlineData("tan(pi)")]
[InlineData("arccos(1)")]
public void TheResidualAtAZeroIsStillZero(string expression)
=> Assert.Equal(Integer.Zero, expression.EvalNumerical());
}
}
Loading