11

In creating trig functions my_sind(d), my_cosd(d), my_tand(d), that used a degree argument rather than a radian one and provided exact answers at multiples of 90, I noticed that the result was sometimes -0.0 rather than 0.0.

my_sind( 0.0) -->  0.0
my_sind(-0.0) --> -0.0

my_sind(180.0) --> -0.0
my_sind(360.0) -->  0.0

sin() and tan() typically return the same sign zero result for a given sign zero input. It makes sense that my_sin() should match sin() for those inputs.

my_sind( 0.0) alike sin( 0.0) -->  0.0
my_sind(-0.0) alike sin(-0.0) --> -0.0

The question is: for what whole number non_zero_n should/may the result ever return -0.0 for my_sind(180*non_zero_n), my_cosd(180*n + 180), my_tand(180*non_zero_n)?

It is easy enough to code so only f(-0.0) produces -0.0 and be done with it. Simple wondering if there is any reason to make other f(x) return -0.0 for any other (non-zero) x and the importance of insuring that sign.


Note: This is not a question of why 0.0 vs. -0.0 occurs. This is not why cos(machine_pi/4) does not return 0.0. Neither is this a question of how to control the generation of 0.0 or -0.0. I see it best as a design question.

0

3 Answers 3

6

The design principle of "least surprise" suggests that we look to previously established functionality for guidance. In this case, the closest established functionality is provided by sinpi and cospi functions introduced in IEEE Std 754-2008 (the IEEE Standard for Floating-Point Arithmetic), section 9. These functions are not part of the current ISO C and ISO C++ standards, but have been incorporated into the math libraries of various programming platforms, for example CUDA.

These functions compute sin(πx) and cos(πx), where multiplication with π occurs implicitly inside the function. tanpi is not defined, but could, based on mathematical equivalency, be assumed to provide functionality according to tanpi(x) = sinpi(x) / cospi(x).

We can now define sind(x) = sinpi(x/180), cosd(x) = cospi(x/180), tand(x) = tanpi(x/180) in an intuitive manner. Section 9.1.2 of IEEE-754 spells out the handling of special arguments for sinpi and cospi. In particular:

sinPi(+n) is +0 and sinPi(−n) is −0 for positive integers n. This implies, under appropriate rounding modes, that sinPi(−x) and −sinPi(x) are the same number (or both NaN) for all x. cosPi(n + ½) is +0 for any integer n when n + ½ is representable.

The IEEE 754-2008 standard does not give a rationale for the quoted requirements, however, an early draft of the relevant section states:

If the value of the function is zero, the sign of this 0 is best determined by considering the continuous extension of the sign function of the mathematical function.

Perusal of the 754 Working Group Mail Archive may yield additional insights, I have not had the time to dig through it. Implementing sind(), cosd(), and tand() as described above, we then arrive at this table of example cases:

SIND
 angle value 
  -540 -0
  -360 -0
  -180 -0
     0  0
   180  0
   360  0
   540  0

COSD
 angle value
  -630  0
  -450  0
  -270  0
   -90  0
    90  0
   270  0
   450  0

TAND
 angle value
  -540  0
  -360 -0
  -180  0
     0  0
   180 -0
   360  0
   540 -0
5

sin() and tan() typically return the same sign zero result for a given sign zero input

It could be generally true since:

  • Speed/accuracy. For small enough doubles, the best answer for sin(x) is x. That is, for numbers smaller than about 1.49e-8, the closest double to the sine of x is actually x itself (see the glibc source code for sin()).

  • Handling of special cases.

    A few extraordinary arithmetic operations are affected by zero's sign; for example "1/(+0) = +inf" but "1/(-0) = -inf". To retain its usefulness, the sign bit must propagate through certain arithmetic operations according to rules derived from continuity considerations.

    Implementations of elementary transcendental functions like sin(z) and tan(z) and their inverses and hyperbolic analogs, though not specified by the IEEE standards, are expected to follow similar rules. The implementation of sin(z) is expected to reproduce the sign of z as well as its value at z = ±O.

    (Branch Cuts for Complex Elementary Functions or Much Ado About Nothing's Sign Bit by W. Kahan)

    Negatively signed zero echoes the mathematical analysis concept of approaching 0 from below as a one-sided limit (consider 1 / sin(x): the sign of zero makes a huge difference).

EDIT

Considering the second point I'd write my_sind so that:

my_sind(-0.0) is -0.0
my_sind(0.0) is 0.0

The latest C standard (F.10.1.6 sin and F.10.1.7 tan, implementations with a signed zero), specifies that if the argument is ±0, it is returned unmodified.

EDIT 2

For the other values I think it's a matter of approximation. Given M_PI < π:

0 = sin(π) < sin(M_PI) ≈ 1.2246467991473532e-16 ≈ +0.0
0 = sin(-π) > sin(-M_PI) ≈ -1.2246467991473532e-16 ≈ -0.0
0 = sin(2*π) > sin(2*M_PI) ≈ -2.4492935982947064e-16
0 = sin(-2*π) < sin(-2*M_PI) ≈ 2.4492935982947064e-16

So if my_sind provides exact answers at multiples of 180° it can return +0.0 or -0.0 (I don't see a clear reason to prefer one over the other).

If my_sind uses some approximation (e.g. a degree * M_PI / 180.0 conversion formula), it should consider how it's approaching the critical values.

5
  • What are your thoughts concerning sind(180), sind(-180), sind(360), sind(-360),...? Nov 3, 2014 at 12:33
  • Thanks for the update. Maybe my post is not clear. The main question is should my_trig(x) ever return -0.0 when |x| is not 0.0? Nov 3, 2014 at 15:33
  • Thanks for the "So if my_sind provides exact answers at multiples of 180° it can return +0.0 or -0.0 (I don't see a clear reason to prefer one over the other)." It is the closest discussion point so far. I'm thinking that "principle of least astonishment" encourages always returning +0.0, but looking to see if there are compelling reasons to return -0.0 in some situations (other than x == +/-0.0). Nov 3, 2014 at 18:22
  • @chux: I think for multiples of 180.0, one really has to examine the values of relative machine precision given those values. That is, the smallest increment/decrement that gives a different representable value in that numerical format. Then, compare that value with the true value to see whether it would fall on the plus side or minus side.
    – rwong
    Jan 3, 2015 at 22:23
  • @rwong Thanks for the idea. Multiples of 90.0 degrees, the exact sind(double degrees) and cosd(double degrees) value can be returned: -1.0, +0.0, +1.0. This post is about should -0.0 ever be returned (aside from sind(-0.0)). Note: sind() does not use the simplistic sin(x/360*M_PI) approach. Jan 4, 2015 at 1:50
3

The library doesn't try to distinguish +0 from -0. IEEE 754 worries quite a bit about this distinction...I found the functions [in math.h] quite hard enough to write without fretting about the sign of nothing. -- P.J. Plauger, The Standard C Library, 1992, page 128.

Formally, trig functions should return the sign of zero in accord with the C standard...which leaves the behavior undefined.

In the face of undefined behavior, the principle of least astonishment suggests duplicating the behavior of the corresponding function from math.h. This smells justifiable, while diverging from the behavior of the corresponding function in math.h smells like a way to introduce bugs into exactly the code which depends on the sign of zero.

8
  • The trig functions in math.h do not return 0.0 when given arguments like +/-pi/2 or +/-pi as these functions can only take representable values near +/-pi/2,etc. These "near" values return results near 0.0. Since the std library trig functions (sin cos tan) do not return 0.0 (or -0.0) for any input (except +/-0.0), but my_sind(), my_cosd(), my_tand() can return 0.0 (or -0.0) there is no 0.0 behavior to duplicate. Nov 3, 2014 at 15:28
  • @chux The premise that sin(-0.0) should return -0 is suspect. It treats an implementation detail of the IEEE standard as a trigonometric principle. Although there is a general mathematical principle of zero as the limit of two intervals embodied in the IEEE implementation, it occurs at that level of abstraction not within general trigonometry [hence the variability in what your trigonometric functions return]. The best that can happen is that you can define an arbitrary convention, but it will be divergent from math.h's nonchalance over the sign of zero. Nov 3, 2014 at 16:35
  • Note: I am not suggesting sin(-0.0) should return -0.0, but that my_sind(x) should match sin(x) when x is +/-0.0. IOW: follow previous practice. Further the question itself is more about what to do when x != 0.0, should my_sind(x) ever return -0.0 as in my_sind(180), etc? Maybe your answer / comment addresses that - but I have not seen that. Nov 3, 2014 at 16:50
  • @chux If the behavior is undefined, then it's undefined. That's just the way C is. Plauger didn't worry about +0 versus -0 when he wrote math.h twenty years ago. It's not clear to me what problem your fretting about the difference is solving. Nov 3, 2014 at 17:50
  • 1
    Hopefully you see that for a well implemented sin(rad) for any value rad>0 and of any precision will never yield 0.0 as pi is irrational. [Ref](www.csee.umbc.edu/~phatak/645/supl/Ng-ArgReduction.pdf) However my_sind(deg) yields an exact 0.0 (either + or -) every multiple of 180.0 as the value 0.0 is the correct mathematical result. "Principle of least astonishment" suggests returning 0.0 in these cases. My question is should -0.0 ever be returned in these cases? Nov 3, 2014 at 18:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.