4

I have an unit test failing on a Math.Tan(-PI/2) returning the wrong version in .NET.

The 'expected' value is taken from Wolfram online (using the spelled-out constant for -Pi/2). See for yourselves here.

As correctly observed in the comments, the mathematical result of tan(-pi/2) is infinity. However, the constant Math.PI does not perfectly represent PI, so this is a 'near the limit' input.

Here's the code.

double MINUS_HALF_PI = -1.570796326794896557998981734272d;
Console.WriteLine(MINUS_HALF_PI == -Math.PI/2); //just checking...

double tan = Math.Tan(MINUS_HALF_PI);
Console.WriteLine("DotNET  {0:E20}", tan);

double expected = -1.633123935319534506380133589474e16;
Console.WriteLine("Wolfram {0:E20}", expected);

double off = Math.Abs(tan-expected);
Console.WriteLine("         {0:E20}", off);

This is what gets printed:

True
DotNET  -1.63317787283838440000E+016
Wolfram -1.63312393531953460000E+016
         5.39375188498000000000E+011

I thought it's an issue of floating-point representation.

Strangely though, the same thing in Java DOES return the same value as Wolfram, down to the last digit - see it evaluated in Eclipse. (The expressions are cropped - you'll have to believe me they use the same constant as MINUS_HALF_PI above.)

enter image description here

True
DotNET  -1.63317787283838440000E+016
Wolfram -1.63312393531953460000E+016
Java    -1.63312393531953700000E+016

As you can see, the difference is:

  • between Wolfram and .NET: ~5.39 * 10^11
  • between Wolfram and Java: =2.40 * 10^1

That's ten orders of magnitude!

So, any ideas why the .NET and Java implementations differ so much? I would expect them both to just defer the actual computing to the processor. Is this assumption unrealistic for x86?

Update

As requested, I tried running in Java with strictfp. No change:

enter image description here

Cristian Diaconescu
  • 34,633
  • 32
  • 143
  • 233
  • First, Math.Tan() only takes a double as input. Second, I don't think `decimal` can help in this regard. Third, Java and .NET use the same standard representation for `double`s so the ability to represent the value is unlikely to be the problem. – Cristian Diaconescu Nov 29 '13 at 14:22
  • you´r right, Math.Tan cannot work with decimal – nabuchodonossor Nov 29 '13 at 14:23
  • Have you tried running with `strictfp`? – chrylis -cautiouslyoptimistic- Nov 29 '13 at 14:25
  • 5
    Unless I've forgotten all my trigonometry, tan(-Pi/2) is +/- infinity. Figures that different implementations would behave unpredictably around that area when rounding errors start flying around. I wonder what exactly differs between the implementations to cause these different results, though. – BambooleanLogic Nov 29 '13 at 14:29
  • 1
    Possible related: http://www.extremeoptimization.com/Blog/index.php/2011/02/accurate-trigonometric-functions-for-large-arguments/ – nemesv Nov 29 '13 at 14:30
  • @chrylis I did now - see update. – Cristian Diaconescu Nov 29 '13 at 14:34
  • `Tan` is -Infinity at -Pi/2, so I think this is no representative value for checking the precision of the used function. Sure the error is ten orders of magnitude but it is still a relative error of just 0.003%. The number just looks big, but actually it isnt. – user1983983 Nov 29 '13 at 14:38
  • @nemesv Interesting article. But it describes errors that appear for large *inputs* (angles a lot more than 360 degrees). What I have here is a problem of large *output*. – Cristian Diaconescu Nov 29 '13 at 14:42
  • So where did the value for `MINUS_HALF_PI` come from? [Clearly not Wolfram Alpha](http://www.wolframalpha.com/input/?i=-pi%2F2) – Martin Smith Nov 29 '13 at 16:46

1 Answers1

6

The entire question is constructed to create a tendentious result. The double value closest to half PI is -1.5707963267948966; the other digits are just ignored. So it’s no wonder that neither C# nor Java detect that the remaining 14 more digits are not turning the result closer to -PI/2, but carefully chosen to trick Wolfram Alpha to return a value close to the result of Java.

-1.570796326794896557998981734272 // the number from the question
-1.57079632679489661923132169163975… // the real digits of -PI/2
                  ↑
                the end of the double precision

Any other number within the range that would get rounded to the same double number including the exact double value as used by Java yields to a value on Wolfram Alpha having nothing in common with neither, the C# nor Java result.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • Nice catch. Be interesting to see what the OP says! – Martin Smith Nov 29 '13 at 16:44
  • OP says, interesting! It did cross my mind that that constant has way too many digits, but tested it against Math.PI/2 and surprise (not!), they match (in the binary64 representation of double, that is...). It didn't occur to me that the 'useless' extra digits *will* make a difference in Wolfram. I don't really know how that super-long constant came into existence (I needed to port existing java code to C#) - but it really looks like it was reverse-engineered! :) I'll have a look in the source code history on Monday and post an update if I find anything interesting. – Cristian Diaconescu Nov 29 '13 at 21:32
  • As a side question, given a double, how would I get the range of (real numbers) values that would approximate (round off) to that double? I'm thinking of adding and subtracting '1' from the value's mantissa binary representation and use those values as the (exclusive) boundaries of the interval. But maybe there's a simpler way than staring at bits? – Cristian Diaconescu Nov 29 '13 at 21:37
  • 1
    In Java, `Math.ulp(value)` returns the distance to the next `double`. So basically `value±ulp` would be that range which gets mapped to value. But, of course, this range cannot get evaluated using `double` values. – Holger Nov 29 '13 at 21:42
  • Nothing similar seems to be built-in in the .NET BCL, but [here](http://stackoverflow.com/questions/9485943/calculate-the-unit-in-the-last-place-ulp-for-doubles) is a simple way of getting the same result. It's along the lines of my previous comment. – Cristian Diaconescu Nov 29 '13 at 22:20
  • Regarding your answer - I looked more into it, and there's a bit of a twist. Now, you're right that the last digits are moot as far as the compiler is concerned. But this begs the question: which value in the range of values that approximate to that binary representation should I feed into Wolfram? It turns out that converting the *binary* representation to decimal, '-1.5707963267948966' is an approximation. The more exact decimal IS the original, too long constant - [see here](http://www.binaryconvert.com/result_double.html?decimal=045049046053055048055057054051050054055057052056057054054) – Cristian Diaconescu Nov 29 '13 at 22:26
  • [cont'd] ...and it seems correct to me to feed 'the middle of the range' to Wolfram. So ... back to square 1 ? – Cristian Diaconescu Nov 29 '13 at 22:29