Files
QuanTAlib/lib/numerics/Atan2Validation.Tests.cs
T
2026-01-25 16:01:45 -08:00

361 lines
12 KiB
C#

using System;
using Xunit;
using Xunit.Abstractions;
namespace QuanTAlib.Tests;
/// <summary>
/// Validates the behavioral differences between .NET's Math.Atan2 and PineScript's custom atan2 implementation.
///
/// PineScript's atan2 uses a numerically stable algorithm:
/// 1. If |x| > |y|: angle = atan(|y|/|x|)
/// 2. If |y| >= |x|: angle = π/2 - atan(|x|/|y|)
/// 3. Then applies quadrant correction based on sign of x and y
///
/// Math.Atan2 follows the standard IEEE convention: atan2(y, x) returns the angle
/// in radians between the positive x-axis and the point (x, y).
///
/// Both return values in the range [-π, π], but they can differ in edge cases
/// and have different numerical stability characteristics.
/// </summary>
public class Atan2ValidationTests
{
private readonly ITestOutputHelper _output;
public Atan2ValidationTests(ITestOutputHelper output)
{
_output = output;
}
/// <summary>
/// PineScript-style atan2 implementation for comparison.
/// This is a direct port of the algorithm from ht_phasor.pine.
/// </summary>
private static double PineScriptAtan2(double y, double x)
{
if (y == 0.0 && x == 0.0)
{
throw new ArgumentException("atan2: Both y and x cannot be zero", nameof(y));
}
double ay = Math.Abs(y);
double ax = Math.Abs(x);
double angle;
if (ax > ay)
{
angle = Math.Atan(ay / ax);
}
else
{
angle = (Math.PI / 2.0) - Math.Atan(ax / ay);
}
if (x < 0.0)
{
angle = Math.PI - angle;
}
if (y < 0.0)
{
angle = -angle;
}
return angle;
}
[Fact]
public void Atan2_StandardQuadrant1_BothMatch()
{
// First quadrant: x > 0, y > 0
double y = 1.0, x = 1.0;
double dotNet = Math.Atan2(y, x);
double pine = PineScriptAtan2(y, x);
_output.WriteLine($"Q1: Math.Atan2({y}, {x}) = {dotNet:F15}");
_output.WriteLine($"Q1: PineAtan2({y}, {x}) = {pine:F15}");
_output.WriteLine($"Q1: Difference = {Math.Abs(dotNet - pine):E}");
Assert.Equal(dotNet, pine, precision: 14);
}
[Fact]
public void Atan2_StandardQuadrant2_BothMatch()
{
// Second quadrant: x < 0, y > 0
double y = 1.0, x = -1.0;
double dotNet = Math.Atan2(y, x);
double pine = PineScriptAtan2(y, x);
_output.WriteLine($"Q2: Math.Atan2({y}, {x}) = {dotNet:F15}");
_output.WriteLine($"Q2: PineAtan2({y}, {x}) = {pine:F15}");
_output.WriteLine($"Q2: Difference = {Math.Abs(dotNet - pine):E}");
Assert.Equal(dotNet, pine, precision: 14);
}
[Fact]
public void Atan2_StandardQuadrant3_BothMatch()
{
// Third quadrant: x < 0, y < 0
double y = -1.0, x = -1.0;
double dotNet = Math.Atan2(y, x);
double pine = PineScriptAtan2(y, x);
_output.WriteLine($"Q3: Math.Atan2({y}, {x}) = {dotNet:F15}");
_output.WriteLine($"Q3: PineAtan2({y}, {x}) = {pine:F15}");
_output.WriteLine($"Q3: Difference = {Math.Abs(dotNet - pine):E}");
Assert.Equal(dotNet, pine, precision: 14);
}
[Fact]
public void Atan2_StandardQuadrant4_BothMatch()
{
// Fourth quadrant: x > 0, y < 0
double y = -1.0, x = 1.0;
double dotNet = Math.Atan2(y, x);
double pine = PineScriptAtan2(y, x);
_output.WriteLine($"Q4: Math.Atan2({y}, {x}) = {dotNet:F15}");
_output.WriteLine($"Q4: PineAtan2({y}, {x}) = {pine:F15}");
_output.WriteLine($"Q4: Difference = {Math.Abs(dotNet - pine):E}");
Assert.Equal(dotNet, pine, precision: 14);
}
[Fact]
public void Atan2_AxisAligned_PositiveY()
{
// On positive Y-axis: x = 0, y > 0 → π/2
double y = 1.0, x = 0.0;
double dotNet = Math.Atan2(y, x);
double pine = PineScriptAtan2(y, x);
double expected = Math.PI / 2.0;
_output.WriteLine($"Positive Y-axis: Math.Atan2({y}, {x}) = {dotNet:F15}");
_output.WriteLine($"Positive Y-axis: PineAtan2({y}, {x}) = {pine:F15}");
_output.WriteLine($"Expected: π/2 = {expected:F15}");
Assert.Equal(expected, dotNet, precision: 14);
Assert.Equal(expected, pine, precision: 14);
}
[Fact]
public void Atan2_AxisAligned_NegativeY()
{
// On negative Y-axis: x = 0, y < 0 → -π/2
double y = -1.0, x = 0.0;
double dotNet = Math.Atan2(y, x);
double pine = PineScriptAtan2(y, x);
double expected = -Math.PI / 2.0;
_output.WriteLine($"Negative Y-axis: Math.Atan2({y}, {x}) = {dotNet:F15}");
_output.WriteLine($"Negative Y-axis: PineAtan2({y}, {x}) = {pine:F15}");
_output.WriteLine($"Expected: -π/2 = {expected:F15}");
Assert.Equal(expected, dotNet, precision: 14);
Assert.Equal(expected, pine, precision: 14);
}
[Fact]
public void Atan2_AxisAligned_PositiveX()
{
// On positive X-axis: x > 0, y = 0 → 0
double y = 0.0, x = 1.0;
double dotNet = Math.Atan2(y, x);
double pine = PineScriptAtan2(y, x);
double expected = 0.0;
_output.WriteLine($"Positive X-axis: Math.Atan2({y}, {x}) = {dotNet:F15}");
_output.WriteLine($"Positive X-axis: PineAtan2({y}, {x}) = {pine:F15}");
_output.WriteLine($"Expected: 0 = {expected:F15}");
Assert.Equal(expected, dotNet, precision: 14);
Assert.Equal(expected, pine, precision: 14);
}
[Fact]
public void Atan2_AxisAligned_NegativeX()
{
// On negative X-axis: x < 0, y = 0 → π
double y = 0.0, x = -1.0;
double dotNet = Math.Atan2(y, x);
double pine = PineScriptAtan2(y, x);
double expected = Math.PI;
_output.WriteLine($"Negative X-axis: Math.Atan2({y}, {x}) = {dotNet:F15}");
_output.WriteLine($"Negative X-axis: PineAtan2({y}, {x}) = {pine:F15}");
_output.WriteLine($"Expected: π = {expected:F15}");
Assert.Equal(expected, dotNet, precision: 14);
Assert.Equal(expected, pine, precision: 14);
}
[Fact]
public void Atan2_Origin_DotNetReturnsZero_PineThrows()
{
// Origin: x = 0, y = 0
// Math.Atan2 returns 0 (by convention)
// PineScript throws an error
double y = 0.0, x = 0.0;
double dotNet = Math.Atan2(y, x);
_output.WriteLine($"Origin: Math.Atan2({y}, {x}) = {dotNet:F15}");
_output.WriteLine("Origin: PineScript throws ArgumentException");
Assert.Equal(0.0, dotNet);
Assert.Throws<ArgumentException>(() => PineScriptAtan2(y, x));
}
[Fact]
public void Atan2_VerySmallValues_NumericalStability()
{
// Test numerical stability with very small values
double y = 1e-300, x = 1e-300;
double dotNet = Math.Atan2(y, x);
double pine = PineScriptAtan2(y, x);
double expected = Math.PI / 4.0; // 45 degrees
_output.WriteLine($"Small values: Math.Atan2({y:E}, {x:E}) = {dotNet:F15}");
_output.WriteLine($"Small values: PineAtan2({y:E}, {x:E}) = {pine:F15}");
_output.WriteLine($"Expected: π/4 = {expected:F15}");
_output.WriteLine($"Difference = {Math.Abs(dotNet - pine):E}");
// Both should handle small values well
Assert.Equal(expected, dotNet, precision: 10);
Assert.Equal(expected, pine, precision: 10);
}
[Fact]
public void Atan2_VeryLargeValues_NumericalStability()
{
// Test numerical stability with very large values
double y = 1e300, x = 1e300;
double dotNet = Math.Atan2(y, x);
double pine = PineScriptAtan2(y, x);
double expected = Math.PI / 4.0; // 45 degrees
_output.WriteLine($"Large values: Math.Atan2({y:E}, {x:E}) = {dotNet:F15}");
_output.WriteLine($"Large values: PineAtan2({y:E}, {x:E}) = {pine:F15}");
_output.WriteLine($"Expected: π/4 = {expected:F15}");
_output.WriteLine($"Difference = {Math.Abs(dotNet - pine):E}");
Assert.Equal(expected, dotNet, precision: 10);
Assert.Equal(expected, pine, precision: 10);
}
[Fact]
public void Atan2_AspectRatioExtreme_TallVector()
{
// PineScript algorithm switches behavior when |y| > |x|
// Test with y >> x
double y = 1000.0, x = 1.0;
double dotNet = Math.Atan2(y, x);
double pine = PineScriptAtan2(y, x);
_output.WriteLine($"Tall vector: Math.Atan2({y}, {x}) = {dotNet:F15}");
_output.WriteLine($"Tall vector: PineAtan2({y}, {x}) = {pine:F15}");
_output.WriteLine($"Difference = {Math.Abs(dotNet - pine):E}");
// Should be very close to π/2
Assert.True(Math.Abs(dotNet - pine) < 1e-12, $"Difference {Math.Abs(dotNet - pine):E} exceeds tolerance");
}
[Fact]
public void Atan2_AspectRatioExtreme_WideVector()
{
// Test with x >> y
double y = 1.0, x = 1000.0;
double dotNet = Math.Atan2(y, x);
double pine = PineScriptAtan2(y, x);
_output.WriteLine($"Wide vector: Math.Atan2({y}, {x}) = {dotNet:F15}");
_output.WriteLine($"Wide vector: PineAtan2({y}, {x}) = {pine:F15}");
_output.WriteLine($"Difference = {Math.Abs(dotNet - pine):E}");
// Should be very close to 0
Assert.True(Math.Abs(dotNet - pine) < 1e-12, $"Difference {Math.Abs(dotNet - pine):E} exceeds tolerance");
}
[Theory]
[InlineData(0.5, 0.866025403784439)] // 30 degrees
[InlineData(0.707106781186548, 0.707106781186548)] // 45 degrees
[InlineData(0.866025403784439, 0.5)] // 60 degrees
public void Atan2_CommonAngles_Match(double y, double x)
{
double dotNet = Math.Atan2(y, x);
double pine = PineScriptAtan2(y, x);
_output.WriteLine($"Math.Atan2({y}, {x}) = {dotNet:F15} rad = {dotNet * 180 / Math.PI:F10}°");
_output.WriteLine($"PineAtan2({y}, {x}) = {pine:F15} rad = {pine * 180 / Math.PI:F10}°");
_output.WriteLine($"Difference = {Math.Abs(dotNet - pine):E}");
Assert.Equal(dotNet, pine, precision: 13);
}
[Fact]
public void Atan2_FullCircle_360DegreesSweep()
{
// Sweep through 360 degrees and verify both implementations match
const int steps = 360;
double maxDiff = 0;
int maxDiffStep = 0;
for (int i = 0; i < steps; i++)
{
double angle = i * 2.0 * Math.PI / steps;
double y = Math.Sin(angle);
double x = Math.Cos(angle);
// Skip origin (angle = 0 with x=1, y=0 is fine, but need to handle numerical zeros)
if (Math.Abs(x) < 1e-15 && Math.Abs(y) < 1e-15)
{
continue;
}
double dotNet = Math.Atan2(y, x);
double pine = PineScriptAtan2(y, x);
double diff = Math.Abs(dotNet - pine);
if (diff > maxDiff)
{
maxDiff = diff;
maxDiffStep = i;
}
}
_output.WriteLine($"Full circle sweep: {steps} steps");
_output.WriteLine($"Maximum difference: {maxDiff:E} at step {maxDiffStep} ({maxDiffStep}°)");
// Expect very small differences
Assert.True(maxDiff < 1e-14, $"Maximum difference {maxDiff:E} exceeds tolerance");
}
[Fact]
public void Summary_ImplementationDifferences()
{
_output.WriteLine("=== Math.Atan2 vs PineScript atan2 Summary ===");
_output.WriteLine("");
_output.WriteLine("1. Origin handling:");
_output.WriteLine(" - Math.Atan2(0, 0) returns 0");
_output.WriteLine(" - PineScript throws an error");
_output.WriteLine("");
_output.WriteLine("2. Algorithm:");
_output.WriteLine(" - Math.Atan2: IEEE standard implementation");
_output.WriteLine(" - PineScript: Uses |y|/|x| or |x|/|y| based on which is larger");
_output.WriteLine(" This avoids division by very small numbers");
_output.WriteLine("");
_output.WriteLine("3. Numerical stability:");
_output.WriteLine(" - Both handle extreme values well");
_output.WriteLine(" - PineScript's approach may be slightly more stable for extreme aspect ratios");
_output.WriteLine("");
_output.WriteLine("4. Recommendation:");
_output.WriteLine(" - Use Math.Atan2 for general purposes (standard, well-tested)");
_output.WriteLine(" - PineScript algorithm adds ~zero benefit in .NET");
_output.WriteLine(" - Only difference is origin handling (error vs 0)");
Assert.True(true); // This is a documentation test
}
}