Skip to content

Commit bac5181

Browse files
committed
Improved the accuracy of Sine and Cosine functions.
1 parent fa3b87c commit bac5181

File tree

2 files changed

+24
-23
lines changed

2 files changed

+24
-23
lines changed

src/Fix64.cs

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -429,13 +429,10 @@ public static Fix64 Sqrt(Fix64 x) {
429429

430430
/// <summary>
431431
/// Returns the Sine of x.
432-
/// This function has about 9 decimals of accuracy for small values of x.
433-
/// It may lose accuracy as the value of x grows.
434-
/// Performance: about 25% slower than Math.Sin() in x64, and 200% slower in x86.
432+
/// The relative error is less than 1E-10 for x in [-2PI, 2PI], and less than 1E-7 in the worst case.
435433
/// </summary>
436434
public static Fix64 Sin(Fix64 x) {
437-
bool flipHorizontal, flipVertical;
438-
var clampedL = ClampSinValue(x.m_rawValue, out flipHorizontal, out flipVertical);
435+
var clampedL = ClampSinValue(x.m_rawValue, out var flipHorizontal, out var flipVertical);
439436
var clamped = new Fix64(clampedL);
440437

441438
// Find the two closest values in the LUT and perform linear interpolation
@@ -478,12 +475,21 @@ public static Fix64 FastSin(Fix64 x) {
478475
return new Fix64(flipVertical ? -nearestValue : nearestValue);
479476
}
480477

481-
482-
483-
[MethodImplAttribute(MethodImplOptions.AggressiveInlining)]
478+
484479
static long ClampSinValue(long angle, out bool flipHorizontal, out bool flipVertical) {
485-
// Clamp value to 0 - 2*PI using modulo; this is very slow but there's no better way AFAIK
486-
var clamped2Pi = angle % PI_TIMES_2;
480+
var largePI = 7244019458077122842;
481+
// Obtained from ((Fix64)1686629713.065252369824872831112M).m_rawValue
482+
// This is (2^29)*PI, where 29 is the largest N such that (2^N)*PI < MaxValue.
483+
// The idea is that this number contains way more precision than PI_TIMES_2,
484+
// and (((x % (2^29*PI)) % (2^28*PI)) % ... (2^1*PI) = x % (2 * PI)
485+
// In practice this gives us an error of about 1,25e-9 in the worst case scenario (Sin(MaxValue))
486+
// Whereas simply doing x % PI_TIMES_2 is the 2e-3 range.
487+
488+
var clamped2Pi = angle;
489+
for (int i = 0; i < 29; ++i)
490+
{
491+
clamped2Pi %= (largePI >> i);
492+
}
487493
if (angle < 0) {
488494
clamped2Pi += PI_TIMES_2;
489495
}
@@ -507,7 +513,7 @@ static long ClampSinValue(long angle, out bool flipHorizontal, out bool flipVert
507513

508514
/// <summary>
509515
/// Returns the cosine of x.
510-
/// See Sin() for more details.
516+
/// The relative error is less than 1E-10 for x in [-2PI, 2PI], and less than 1E-7 in the worst case.
511517
/// </summary>
512518
public static Fix64 Cos(Fix64 x) {
513519
var xl = x.m_rawValue;
@@ -641,7 +647,8 @@ public int CompareTo(Fix64 other) {
641647
}
642648

643649
public override string ToString() {
644-
return ((decimal)this).ToString();
650+
// Up to 10 decimal places
651+
return ((decimal)this).ToString("0.##########");
645652
}
646653

647654
public static Fix64 FromRaw(long rawValue) {

tests/Fix64Tests.cs

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -446,21 +446,15 @@ public void Sin()
446446
Assert.True(delta <= 3 * Fix64.Precision, string.Format("Sin({0}): expected {1} but got {2}", angle, expected, actualF));
447447
}
448448

449+
var deltas = new List<decimal>();
449450
foreach (var val in m_testCases)
450451
{
451452
var f = Fix64.FromRaw(val);
452453
var actualF = Fix64.Sin(f);
453454
var expected = (decimal)Math.Sin((double)f);
454455
var delta = Math.Abs(expected - (decimal)actualF);
455-
Assert.True(delta <= 0.003M, string.Format("Sin({0}): expected {1} but got {2}", f, expected, actualF));
456+
Assert.True(delta <= 0.0000001M, string.Format("Sin({0}): expected {1} but got {2}", f, expected, actualF));
456457
}
457-
458-
Console.WriteLine("Max delta = {0}", m_testCases.Max(val => {
459-
var f = Fix64.FromRaw(val);
460-
var actualF = Fix64.Sin(f);
461-
var expected = (decimal)Math.Sin((double)f);
462-
return Math.Abs(expected - (decimal)actualF);
463-
}));
464458
}
465459

466460
[Fact]
@@ -516,7 +510,7 @@ public void Cos()
516510
var actualF = Fix64.Cos(f);
517511
var expected = (decimal)Math.Cos((double)f);
518512
var delta = Math.Abs(expected - (decimal)actualF);
519-
Assert.True(delta <= 0.004M, string.Format("Cos({0}): expected {1} but got {2}", f, expected, actualF));
513+
Assert.True(delta <= 0.0000001M, string.Format("Cos({0}): expected {1} but got {2}", f, expected, actualF));
520514
}
521515
}
522516

@@ -590,9 +584,9 @@ public void Atan2()
590584
{
591585
var yf = (Fix64)y;
592586
var xf = (Fix64)x;
593-
var actual = (decimal)Fix64.Atan2(yf, xf);
587+
var actual = Fix64.Atan2(yf, xf);
594588
var expected = (decimal)Math.Atan2((double)yf, (double)xf);
595-
var delta = Math.Abs(actual - expected);
589+
var delta = Math.Abs((decimal)actual - expected);
596590
deltas.Add(delta);
597591
Assert.True(delta <= 0.005M, string.Format("Precision: Atan2({0}, {1}): expected {2} but got {3}", yf, xf, expected, actual));
598592
}

0 commit comments

Comments
 (0)