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.

chux
  • 638

3 Answers3

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
njuffa
  • 1,326
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.

manlio
  • 4,256
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.