Skip to content

Commit 07898f8

Browse files
committed
refactor: sin logic (#33)
* feat: add method to extract up direction from 4x4 matrix - cleanup unneccessary extensions * feat: add method to clamp Vector3d by magnitude * task: cleanup unit tests * fix: sin accuracy - switched to using Chebyshev-polynomial-based approximation over Taylor-series expansion * refactor: rename critical static fixed values * fix: static Fixed64 const values
1 parent bee93b9 commit 07898f8

File tree

7 files changed

+76
-58
lines changed

7 files changed

+76
-58
lines changed

src/FixedMathSharp/Bounds/BoundingBox.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@ public static BoundingBox Union(BoundingBox a, BoundingBox b)
350350
public static Vector3d FindClosestPointsBetweenBoxes(BoundingBox a, BoundingBox b)
351351
{
352352
Vector3d closestPoint = Vector3d.Zero;
353-
Fixed64 minDistance = Fixed64.MaxValue;
353+
Fixed64 minDistance = Fixed64.MAX_VALUE;
354354
for (int i = 0; i < b.Vertices.Length; i++)
355355
{
356356
Vector3d point = a.ClosestPointOnSurface(b.Vertices[i]);

src/FixedMathSharp/Core/FixedMath.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public static Fixed64 Clamp01(Fixed64 value)
7878
[MethodImpl(MethodImplOptions.AggressiveInlining)]
7979
public static Fixed64 Clamp(Fixed64 f1, Fixed64 min, Fixed64? max = null)
8080
{
81-
Fixed64 m = max ?? Fixed64.MaxValue;
81+
Fixed64 m = max ?? Fixed64.MAX_VALUE;
8282
return f1 < min ? min : f1 > m ? m : f1;
8383
}
8484

@@ -100,7 +100,7 @@ public static Fixed64 Abs(Fixed64 value)
100100
{
101101
// For the minimum value, return the max to avoid overflow
102102
if (value.m_rawValue == MIN_VALUE_L)
103-
return Fixed64.MaxValue;
103+
return Fixed64.MAX_VALUE;
104104

105105
// Use branchless absolute value calculation
106106
long mask = value.m_rawValue >> 63; // If negative, mask will be all 1s; if positive, all 0s
@@ -335,7 +335,7 @@ public static Fixed64 MoveTowards(Fixed64 from, Fixed64 to, Fixed64 maxAmount)
335335
/// <returns>The sum of <paramref name="x"/> and <paramref name="y"/>.</returns>
336336
/// <remarks>
337337
/// Overflow is detected by checking for a change in the sign bit that indicates a wrap-around.
338-
/// Additionally, a special check is performed for adding <see cref="Fixed64.MinValue"/> and -1,
338+
/// Additionally, a special check is performed for adding <see cref="Fixed64.MIN_VALUE"/> and -1,
339339
/// as this is a known edge case for overflow.
340340
/// </remarks>
341341
public static long AddOverflowHelper(long x, long y, ref bool overflow)

src/FixedMathSharp/Core/FixedTrigonometry.cs

Lines changed: 54 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -22,25 +22,30 @@ public static partial class FixedMath
2222

2323
// Trigonometric and logarithmic constants
2424
internal const double PI_D = 3.14159265358979323846;
25-
public static readonly Fixed64 PI = new Fixed64(PI_D);
25+
public static readonly Fixed64 PI = (Fixed64)PI_D;
2626
public static readonly Fixed64 TwoPI = PI * 2;
2727
public static readonly Fixed64 PiOver2 = PI / 2;
2828
public static readonly Fixed64 PiOver3 = PI / 3;
2929
public static readonly Fixed64 PiOver4 = PI / 4;
3030
public static readonly Fixed64 PiOver6 = PI / 6;
31-
public static readonly Fixed64 Ln2 = new Fixed64(0.6931471805599453); // Natural logarithm of 2
31+
public static readonly Fixed64 Ln2 = (Fixed64)0.6931471805599453; // Natural logarithm of 2
3232

33-
public static readonly Fixed64 Log2Max = new Fixed64(63L * ONE_L);
34-
public static readonly Fixed64 Log2Min = new Fixed64(-64L * ONE_L);
33+
public static readonly Fixed64 LOG_2_MAX = new Fixed64(63L * ONE_L);
34+
public static readonly Fixed64 LOG_2_MIN = new Fixed64(-64L * ONE_L);
3535

3636
internal const double DEG2RAD_D = 0.01745329251994329576; // π / 180
37-
public static readonly Fixed64 Deg2Rad = new Fixed64(DEG2RAD_D); // Degrees to radians conversion factor
37+
public static readonly Fixed64 Deg2Rad = (Fixed64)DEG2RAD_D; // Degrees to radians conversion factor
3838
internal const double RAD2DEG_D = 57.2957795130823208767; // 180 / π
39-
public static readonly Fixed64 Rad2Deg = new Fixed64(RAD2DEG_D); // Radians to degrees conversion factor
39+
public static readonly Fixed64 Rad2Deg = (Fixed64)RAD2DEG_D; // Radians to degrees conversion factor
4040

4141
// Asin Padé approximations
42-
private static readonly Fixed64 PadeA1 = new Fixed64(0.183320102);
43-
private static readonly Fixed64 PadeA2 = new Fixed64(0.0218804099);
42+
private static readonly Fixed64 PADE_A1 = (Fixed64)0.183320102;
43+
private static readonly Fixed64 PADE_A2 = (Fixed64)0.0218804099;
44+
45+
// Carefully optimized polynomial coefficients for sin(x), ensuring maximum precision in Fixed64 math.
46+
private static readonly Fixed64 SIN_COEFF_3 = (Fixed64)0.16666667605750262737274169921875d; // 1/3!
47+
private static readonly Fixed64 SIN_COEFF_5 = (Fixed64)0.0083328341133892536163330078125d; // 1/5!
48+
private static readonly Fixed64 SIN_COEFF_7 = (Fixed64)0.00019588856957852840423583984375d; // 1/7!
4449

4550
#endregion
4651

@@ -93,11 +98,11 @@ public static Fixed64 Pow2(Fixed64 x)
9398
if (x == Fixed64.One)
9499
return neg ? Fixed64.One / Fixed64.Two : Fixed64.Two;
95100

96-
if (x >= Log2Max)
97-
return neg ? Fixed64.One / Fixed64.MaxValue : Fixed64.MaxValue;
101+
if (x >= LOG_2_MAX)
102+
return neg ? Fixed64.One / Fixed64.MAX_VALUE : Fixed64.MAX_VALUE;
98103

99-
if (x <= Log2Min)
100-
return neg ? Fixed64.MaxValue : Fixed64.Zero;
104+
if (x <= LOG_2_MIN)
105+
return neg ? Fixed64.MAX_VALUE : Fixed64.Zero;
101106

102107
/*
103108
* Taylor series expansion for exp(x)
@@ -269,19 +274,30 @@ public static Fixed64 DegToRad(Fixed64 deg)
269274
}
270275

271276
/// <summary>
272-
/// Returns the sine of a specified angle in radians.
277+
/// Computes the sine of a given angle in radians using an optimized
278+
/// minimax polynomial approximation.
273279
/// </summary>
280+
/// <param name="x">The angle in radians.</param>
281+
/// <returns>The sine of the given angle, in fixed-point format.</returns>
274282
/// <remarks>
275-
/// The relative error is less than 1E-10 for x in [-2PI, 2PI], and less than 1E-7 in the worst case.
283+
/// - This function uses a Chebyshev-polynomial-based approximation to ensure high accuracy
284+
/// while maintaining performance in fixed-point arithmetic.
285+
/// - The coefficients have been carefully tuned to minimize fixed-point truncation errors.
286+
/// - The error is less than 1 ULP (unit in the last place) at key reference points,
287+
/// ensuring <c>Sin(π/4) = 0.707106781192124</c> exactly within Fixed64 precision.
288+
/// - The function automatically normalizes input values to the range [-π, π] for stability.
276289
/// </remarks>
277290
public static Fixed64 Sin(Fixed64 x)
278291
{
279292
// Check for special cases
280-
if (x == Fixed64.Zero) return Fixed64.Zero;
281-
if (x == PiOver2) return Fixed64.One;
282-
if (x == -PiOver2) return -Fixed64.One;
283-
284-
// Ensure x is in the range [-2π, 2π]
293+
if (x == Fixed64.Zero) return Fixed64.Zero; // sin(0) = 0
294+
if (x == PiOver2) return Fixed64.One; // sin(π/2) = 1
295+
if (x == -PiOver2) return -Fixed64.One; // sin(-π/2) = -1
296+
if (x == PI) return Fixed64.Zero; // sin(π) = 0
297+
if (x == -PI) return Fixed64.Zero; // sin(-π) = 0
298+
if (x == TwoPI || x == -TwoPI) return Fixed64.Zero; // sin(2π) = 0
299+
300+
// Normalize x to [-π, π]
285301
x %= TwoPI;
286302
if (x < -PI)
287303
x += TwoPI;
@@ -298,31 +314,33 @@ public static Fixed64 Sin(Fixed64 x)
298314
if (x > PiOver2)
299315
x = PI - x;
300316

301-
// Use Taylor series approximation
302-
Fixed64 result = x;
303-
Fixed64 term = x;
317+
// Precompute x^2
304318
Fixed64 x2 = x * x;
305-
int sign = -1;
306-
307-
for (int i = 3; i < 15; i += 2)
308-
{
309-
term *= x2 / (i * (i - 1));
310-
if (term.Abs() < Fixed64.Epsilon)
311-
break;
312319

313-
result += term * sign;
314-
sign = -sign;
315-
}
320+
// Optimized Chebyshev Polynomial for Sin(x)
321+
Fixed64 result = x * (Fixed64.One
322+
- x2 * SIN_COEFF_3
323+
+ (x2 * x2) * SIN_COEFF_5
324+
- (x2 * x2 * x2) * SIN_COEFF_7);
316325

317326
return flip ? -result : result;
318327
}
319328

320329
/// <summary>
321-
/// Returns the cosine of x.
322-
/// The relative error is less than 1E-10 for x in [-2PI, 2PI], and less than 1E-7 in the worst case.
330+
/// Computes the cosine of a given angle in radians using a sine-based identity transformation.
323331
/// </summary>
332+
/// <param name="x">The angle in radians.</param>
333+
/// <returns>The cosine of the given angle, in fixed-point format.</returns>
334+
/// <remarks>
335+
/// - Instead of directly approximating cosine, this function derives <c>cos(x)</c> using
336+
/// the identity <c>cos(x) = sin(x + π/2)</c>. This ensures maximum accuracy.
337+
/// - The underlying sine function is computed using a highly optimized minimax polynomial approximation.
338+
/// - By leveraging this transformation, cosine achieves the same precision guarantees
339+
/// as sine, including <c>Cos(π/4) = 0.707106781192124</c> exactly within Fixed64 precision.
340+
/// - The function automatically normalizes input values to the range [-π, π] for stability.
341+
/// </remarks>
324342
public static Fixed64 Cos(Fixed64 x)
325-
{
343+
{
326344
long xl = x.m_rawValue;
327345
long rawAngle = xl + (xl > 0 ? -PI.m_rawValue - PiOver2.m_rawValue : PiOver2.m_rawValue);
328346
return Sin(Fixed64.FromRaw(rawAngle));
@@ -401,7 +419,7 @@ public static Fixed64 Asin(Fixed64 x)
401419
{
402420
// Padé approximation of asin(x) for |x| < 0.5
403421
Fixed64 xSquared = x * x;
404-
Fixed64 numerator = x * (Fixed64.One + (xSquared * (PadeA1 + (xSquared * PadeA2))));
422+
Fixed64 numerator = x * (Fixed64.One + (xSquared * (PADE_A1 + (xSquared * PADE_A2))));
405423
return numerator;
406424
}
407425

src/FixedMathSharp/Numerics/Fixed64.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ public partial struct Fixed64 : IEquatable<Fixed64>, IComparable<Fixed64>, IEqua
2121
/// </summary>
2222
public long m_rawValue;
2323

24-
public static readonly Fixed64 MaxValue = new Fixed64(FixedMath.MAX_VALUE_L);
25-
public static readonly Fixed64 MinValue = new Fixed64(FixedMath.MIN_VALUE_L);
24+
public static readonly Fixed64 MAX_VALUE = new Fixed64(FixedMath.MAX_VALUE_L);
25+
public static readonly Fixed64 MIN_VALUE = new Fixed64(FixedMath.MIN_VALUE_L);
2626

2727
public static readonly Fixed64 One = new Fixed64(FixedMath.ONE_L);
2828
public static readonly Fixed64 Two = One * 2;
@@ -332,18 +332,18 @@ public static explicit operator decimal(Fixed64 value)
332332
if (opSignsEqual)
333333
{
334334
if (sum < 0 || (overflow && xl > 0))
335-
return MaxValue;
335+
return MAX_VALUE;
336336
}
337337
else
338338
{
339339
if (sum > 0)
340-
return MinValue;
340+
return MIN_VALUE;
341341
}
342342

343343
// Final overflow check: if the high 32 bits are non-zero or non-sign-extended, it's an overflow
344344
long topCarry = hihi >> FixedMath.SHIFT_AMOUNT_I;
345345
if (topCarry != 0 && topCarry != -1)
346-
return opSignsEqual ? MaxValue : MinValue;
346+
return opSignsEqual ? MAX_VALUE : MIN_VALUE;
347347

348348
// Negative overflow check
349349
if (!opSignsEqual)
@@ -352,7 +352,7 @@ public static explicit operator decimal(Fixed64 value)
352352
long negOp = xl < yl ? xl : yl;
353353

354354
if (sum > negOp && negOp < -FixedMath.ONE_L && posOp > FixedMath.ONE_L)
355-
return MinValue;
355+
return MIN_VALUE;
356356
}
357357

358358
return new Fixed64(sum);
@@ -413,7 +413,7 @@ public static explicit operator decimal(Fixed64 value)
413413

414414
// Detect overflow
415415
if ((div & ~(0xFFFFFFFFFFFFFFFF >> bitPos)) != 0)
416-
return ((xl ^ yl) & FixedMath.MIN_VALUE_L) == 0 ? MaxValue : MinValue;
416+
return ((xl ^ yl) & FixedMath.MIN_VALUE_L) == 0 ? MAX_VALUE : MIN_VALUE;
417417

418418
remainder <<= 1;
419419
--bitPos;
@@ -456,7 +456,7 @@ public static explicit operator decimal(Fixed64 value)
456456
[MethodImpl(MethodImplOptions.AggressiveInlining)]
457457
public static Fixed64 operator -(Fixed64 x)
458458
{
459-
return x.m_rawValue == FixedMath.MIN_VALUE_L ? MaxValue : new Fixed64(-x.m_rawValue);
459+
return x.m_rawValue == FixedMath.MIN_VALUE_L ? MAX_VALUE : new Fixed64(-x.m_rawValue);
460460
}
461461

462462
/// <summary>

src/FixedMathSharp/Numerics/FixedRange.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ public struct FixedRange : IEquatable<FixedRange>
1414
/// <summary>
1515
/// The smallest possible range.
1616
/// </summary>
17-
public static readonly FixedRange MinRange = new FixedRange(Fixed64.MinValue, Fixed64.MinValue);
17+
public static readonly FixedRange MinRange = new FixedRange(Fixed64.MIN_VALUE, Fixed64.MIN_VALUE);
1818

1919
/// <summary>
2020
/// The largest possible range.
2121
/// </summary>
22-
public static readonly FixedRange MaxRange = new FixedRange(Fixed64.MaxValue, Fixed64.MaxValue);
22+
public static readonly FixedRange MaxRange = new FixedRange(Fixed64.MAX_VALUE, Fixed64.MAX_VALUE);
2323

2424
#endregion
2525

tests/FixedMathSharp.Tests/Fixed64.Tests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -174,19 +174,19 @@ public void Fraction_CreatesCorrectFixed64Value()
174174
[Fact]
175175
public void Add_OverflowProtection_ReturnsMaxValue()
176176
{
177-
var a = Fixed64.MaxValue;
177+
var a = Fixed64.MAX_VALUE;
178178
var b = new Fixed64(1);
179179
var result = a + b;
180-
Assert.Equal(Fixed64.MaxValue, result);
180+
Assert.Equal(Fixed64.MAX_VALUE, result);
181181
}
182182

183183
[Fact]
184184
public void Subtract_OverflowProtection_ReturnsMinValue()
185185
{
186-
var a = Fixed64.MinValue;
186+
var a = Fixed64.MIN_VALUE;
187187
var b = new Fixed64(1);
188188
var result = a - b;
189-
Assert.Equal(Fixed64.MinValue, result);
189+
Assert.Equal(Fixed64.MIN_VALUE, result);
190190
}
191191

192192
#endregion

tests/FixedMathSharp.Tests/FixedCurveTests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,11 @@ public void Evaluate_NegativeValues_ShouldInterpolateCorrectly()
124124
public void Evaluate_ExtremeValues_ShouldHandleCorrectly()
125125
{
126126
FixedCurve curve = new FixedCurve(FixedCurveMode.Linear,
127-
new FixedCurveKey(Fixed64.MinValue, -(Fixed64)10000),
128-
new FixedCurveKey(Fixed64.MaxValue, (Fixed64)10000));
127+
new FixedCurveKey(Fixed64.MIN_VALUE, -(Fixed64)10000),
128+
new FixedCurveKey(Fixed64.MAX_VALUE, (Fixed64)10000));
129129

130-
Assert.Equal((Fixed64)(-10000), curve.Evaluate(Fixed64.MinValue));
131-
Assert.Equal((Fixed64)(10000), curve.Evaluate(Fixed64.MaxValue));
130+
Assert.Equal((Fixed64)(-10000), curve.Evaluate(Fixed64.MIN_VALUE));
131+
Assert.Equal((Fixed64)(10000), curve.Evaluate(Fixed64.MAX_VALUE));
132132
}
133133

134134
[Fact]

0 commit comments

Comments
 (0)