Created
March 7, 2017 01:38
-
-
Save mellinoe/86dafbbcdf4ba6a5e8bd46d727adbcb4 to your computer and use it in GitHub Desktop.
Simple test program showing differences in actual and expected behavior in the .NET implementation of Complex.Acos.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Numerics; | |
using static System.Numerics.Complex; | |
namespace ConsoleApp | |
{ | |
public class Program | |
{ | |
// This is the largest x for which 2 x^2 will not overflow. It is used for branching inside Asin and Acos. | |
private static readonly double s_asinOverflowThreshold = Math.Sqrt(double.MaxValue) / 2.0; | |
static void Main(string[] args) | |
{ | |
ShowDiff(0, 0, Math.PI / 2, 0); | |
ShowDiff(0, double.NaN, Math.PI / 2, double.NaN); | |
ShowDiff(10, double.PositiveInfinity, Math.PI / 2, double.NegativeInfinity); | |
ShowDiff(10, Double.NaN, double.NaN, double.NaN); | |
ShowDiff(double.NegativeInfinity, 10, Math.PI, double.NegativeInfinity); | |
ShowDiff(double.PositiveInfinity, 10, 0, double.NegativeInfinity); | |
ShowDiff(double.NegativeInfinity, double.PositiveInfinity, 3 * Math.PI / 4, double.NegativeInfinity); | |
ShowDiff(double.PositiveInfinity, double.PositiveInfinity, Math.PI / 4, double.NegativeInfinity); | |
ShowDiff(double.PositiveInfinity, double.NaN, double.NaN, double.PositiveInfinity); | |
ShowDiff(double.NaN, 10, double.NaN, double.NaN); | |
ShowDiff(double.NaN, double.PositiveInfinity, double.NaN, double.NegativeInfinity); | |
ShowDiff(double.NaN, double.NaN, double.NaN, double.NaN); | |
} | |
public static void ShowDiff(double r, double i, double rE, double iE) | |
{ | |
ShowDiff(new Complex(r, i), new Complex(rE, iE)); | |
} | |
public static void ShowDiff(Complex c, Complex expected) | |
{ | |
Console.WriteLine($"Acos( {c.Real}, {c.Imaginary} )"); | |
Console.WriteLine($" OLD: {Acos(c)}"); | |
Console.WriteLine($" NEW: {Acos_New(c)}"); | |
Console.WriteLine($" EXP: {expected}"); | |
} | |
public static Complex Asin_Old(Complex value) | |
{ | |
if ((value.Imaginary == 0 && value.Real < 0) || value.Imaginary > 0) | |
{ | |
return -Asin_Old(-value); | |
} | |
return (-ImaginaryOne) * Log(ImaginaryOne * value + Sqrt(One - value * value)); | |
} | |
public static Complex Asin_New(Complex value) | |
{ | |
double b, bPrime, v; | |
Asin_Internal(Math.Abs(value.Real), Math.Abs(value.Imaginary), out b, out bPrime, out v); | |
double u; | |
if (bPrime < 0.0) | |
{ | |
u = Math.Asin(b); | |
} | |
else | |
{ | |
u = Math.Atan(bPrime); | |
} | |
if (value.Real < 0.0) u = -u; | |
if (value.Imaginary < 0.0) v = -v; | |
return new Complex(u, v); | |
} | |
public static Complex Acos_New(Complex value) | |
{ | |
double b, bPrime, v; | |
Asin_Internal(Math.Abs(value.Real), Math.Abs(value.Imaginary), out b, out bPrime, out v); | |
double u; | |
if (bPrime < 0.0) | |
{ | |
u = Math.Acos(b); | |
} | |
else | |
{ | |
u = Math.Atan(1.0 / bPrime); | |
} | |
if (value.Real < 0.0) u = Math.PI - u; | |
if (value.Imaginary > 0.0) v = -v; | |
return new Complex(u, v); | |
} | |
private static void Asin_Internal(double x, double y, out double b, out double bPrime, out double v) | |
{ | |
// This method for the inverse complex sine (and cosine) is described in | |
// Hull and Tang, "Implementing the Complex Arcsine and Arccosine Functions Using Exception Handling", | |
// ACM Transactions on Mathematical Software (1997) | |
// (https://www.researchgate.net/profile/Ping_Tang3/publication/220493330_Implementing_the_Complex_Arcsine_and_Arccosine_Functions_Using_Exception_Handling/links/55b244b208ae9289a085245d.pdf) | |
// First, the basics: start with sin(w) = (e^{iw} - e^{-iw})/(2i) = z. Here z is the input | |
// and w is the output. To solve for w, define t = e^{i w} and multiply through by t to | |
// get the quadratic equation t^2 - 2 i z t - 1 = 0. The solution is t = i z + sqrt(1 - z^2), so | |
// w = arcsin(z) = - i log( i z + sqrt(1 - z^2) ) | |
// Decompose z = x + i y, multiply out i z + sqrt(1 - z^2), use log(s) = |s| + i arg(s), and do a | |
// bunch of algebra to get the components of w = arcsin(z) = u + i v | |
// u = arcsin(\beta) v = sign(y) log(\alpha + sqrt(\alpha^2 - 1)) | |
// where | |
// \alpha = (\rho + \sigma)/2 \beta = (\rho - \sigma)/2 | |
// \rho = sqrt((x + 1)^2 + y^2) \sigma = \sqrt((x - 1)^2 + y^2) | |
// This appears in DLMF section 4.23. (http://dlmf.nist.gov/4.23), along with the analogous | |
// arccos(w) = arccos(\beta) - i sign(y) log(\alpha + sqrt(\alpha^2 - 1)) | |
// So \alpha and \beta together give us arcsin(w) and arccos(w). | |
// As written, \alpha is not susceptible to cancelation errors, but \beta is. To avoid cancelation, note | |
// \beta = (\rho^2 - \sigma^2)/(\rho + \sigma))/2 = (2 x)/(\rho + \sigma) = x / \alpha | |
// which is not subject to cancelation. Note \alpha >= 1 and |\beta| <= 1. | |
// For \alpha ~ 1, the argument of the log is near unity, so we compute (\alpha - 1) instead, | |
// and write the argument as 1 + (\alpha - 1) + \sqrt{(\alpha - 1)(\alpha + 1)}. | |
// For \beta ~ 1, arccos does not accurately resolve small angles, so we compute the tangent of the angle | |
// instead. Hull and Tang derive formulas for (\alpha - 1) and \beta' = \tan(u) that do not suffer | |
// cancelation for these cases. | |
// For simplicity, we assume all positive inputs and return all positive outputs. The caller should | |
// assign signs appropriate to the desired cut conventions. We return v directly since its magnitude | |
// is the same for both arcsin and arccos. Instead of u, we usually return \beta and sometimes \beta'. | |
// If \beta' is not computed, it is set to -1; if it is computed, it should be used instead of \beta | |
// to determine u. Compute u = \arcsin(\beta) or u = \arctan(\beta') for arcsin, u = \arccos(\beta) | |
// or \arctan(1/\beta') for arccos. | |
// For x or y large enough to overflow \alpha^2, we can simplify our formulas and avoid overflow. | |
if ((x > s_asinOverflowThreshold) || (y > s_asinOverflowThreshold)) | |
{ | |
b = -1.0; | |
bPrime = x / y; | |
double small, big; | |
if (x < y) | |
{ | |
small = x; | |
big = y; | |
} | |
else | |
{ | |
small = y; | |
big = x; | |
} | |
double ratio = small / big; | |
v = Math.Log(2.0) + Math.Log(big) + 0.5 * Log1P(ratio * ratio); | |
} | |
else | |
{ | |
double r = Hypot((x + 1.0), y); | |
double s = Hypot((x - 1.0), y); | |
double a = (r + s) / 2.0; | |
b = x / a; | |
if (b > 0.75) | |
{ | |
double amx; | |
if (x <= 1.0) | |
{ | |
amx = (y * y / (r + (x + 1.0)) + (s + (1.0 - x))) / 2.0; | |
} | |
else | |
{ | |
amx = y * y * (1.0 / (r + (x + 1.0)) + 1.0 / (s + (x - 1.0))) / 2.0; | |
} | |
bPrime = x / Math.Sqrt((a + x) * amx); | |
} | |
else | |
{ | |
bPrime = -1.0; | |
} | |
if (a < 1.5) | |
{ | |
double am1; | |
if (x < 1.0) | |
{ | |
am1 = y * y / 2.0 * (1.0 / (r + (x + 1.0)) + 1.0 / (s + (1.0 - x))); | |
} | |
else | |
{ | |
am1 = (y * y / (r + (x + 1.0)) + (s + (x - 1.0))) / 2.0; | |
} | |
v = Log1P(am1 + Math.Sqrt(am1 * (a + 1.0))); | |
} | |
else | |
{ | |
// Because of the test above, we can be sure that a * a will not overflow. | |
v = Math.Log(a + Math.Sqrt((a - 1.0) * (a + 1.0))); | |
} | |
} | |
} | |
private static double Hypot(double a, double b) | |
{ | |
// Using | |
// sqrt(a^2 + b^2) = |a| * sqrt(1 + (b/a)^2) | |
// we can factor out the larger component to dodge overflow even when a * a would overflow. | |
a = Math.Abs(a); | |
b = Math.Abs(b); | |
double small, large; | |
if (a < b) | |
{ | |
small = a; | |
large = b; | |
} | |
else | |
{ | |
small = b; | |
large = a; | |
} | |
if (small == 0.0) | |
{ | |
return (large); | |
} | |
else if (double.IsPositiveInfinity(large) && !double.IsNaN(small)) | |
{ | |
// The NaN test is necessary so we don't return +inf when small=NaN and large=+inf. | |
// NaN in any other place returns NaN without any special handling. | |
return (double.PositiveInfinity); | |
} | |
else | |
{ | |
double ratio = small / large; | |
return (large * Math.Sqrt(1.0 + ratio * ratio)); | |
} | |
} | |
private static double Log1P(double x) | |
{ | |
// Compute log(1 + x) without loss of accuracy when x is small. | |
// Our only use case so far is for positive values, so this isn't coded to handle negative values. | |
double xp1 = 1.0 + x; | |
if (xp1 == 1.0) | |
{ | |
return x; | |
} | |
else if (x < 0.75) | |
{ | |
// This is accurate to within 5 ulp with any floating-point system that uses a guard digit, | |
// as proven in Theorem 4 of "What Every Computer Scientist Should Know About Floating-Point | |
// Arithmetic" (https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html) | |
return x * Math.Log(xp1) / (xp1 - 1.0); | |
} | |
else | |
{ | |
return Math.Log(xp1); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment