Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

math/cmplx: unpredictable sign of 0 calling Sin and Cos for real arguments #51034

Closed
robpike opened this issue Feb 6, 2022 · 8 comments
Closed
Labels
FrozenDueToAge help wanted NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one.
Milestone

Comments

@robpike
Copy link
Contributor

robpike commented Feb 6, 2022

See https://go.dev/play/p/sSDqGhsdp5a

This program prints cmplx.Cos and cmplx.Sin for a number of real arguments (imaginary part zero). The result is of course real, but the returned result often has negative zero as the imaginary part. I can't see a pattern, and I'm not sure if it matters or, if so, what the right thing to do is, but I wanted to record it.

In particular, check out this gem:

sin (0+0i) (0+0i)
cos (0+0i) (1-0i)
@ianlancetaylor
Copy link
Contributor

This C program:

#include <stdio.h>
#include <complex.h>

int main() {
  int i;
  for (i = 0; i <= 8; i++) {
    complex x = (complex)((double)(i - 4));
    complex r = csin(x);
    printf("sin %g+%gi %g+%gi\n", creal(x), cimag(x), creal(r), cimag(r));
    r = ccos(x);
    printf("cos %g+%gi %g+%gi\n", creal(x), cimag(x), creal(r), cimag(r));
  }
}

prints this:

sin -4+0i 0.756802+-0i
cos -4+0i -0.653644+-0i
sin -3+0i -0.14112+-0i
cos -3+0i -0.989992+0i
sin -2+0i -0.909297+-0i
cos -2+0i -0.416147+0i
sin -1+0i -0.841471+0i
cos -1+0i 0.540302+0i
sin 0+0i 0+0i
cos 0+0i 1+-0i
sin 1+0i 0.841471+0i
cos 1+0i 0.540302+-0i
sin 2+0i 0.909297+-0i
cos 2+0i -0.416147+-0i
sin 3+0i 0.14112+-0i
cos 3+0i -0.989992+-0i
sin 4+0i -0.756802+-0i
cos 4+0i -0.653644+0i

@robpike
Copy link
Contributor Author

robpike commented Feb 7, 2022

The Go cmplx library gives the same unpredictable signs for the imaginary part. That says we're doing the same as the C environment you are using, but it doesn't exactly say what the right result is.

This is mostly a curiosity for me. Are the negative zeroes considered acceptable for a result like cos(0) that is resolutely positive? I don't know. I'm hoping a numerical expert will weigh in.

@robpike
Copy link
Contributor Author

robpike commented Feb 7, 2022

What I expected is either always +0i or that the imaginary part has the same sign as the real part. But that is based on surmise, not on what an expert might know and expect.

@robpike
Copy link
Contributor Author

robpike commented Feb 7, 2022

https://twitter.com/rob_pike/status/1490529340907417601 to see if anyone knows what's right here.

@changkun
Copy link
Member

changkun commented Feb 7, 2022

Not an expert but as far as I remember from school, when computing an inverse elementary function for complex numbers, we need distinguish between -0i and +0i, and sqrt(-0.841471+0i) != sqrt(-0.841471-0i).

A better question might be: what would experts expect for the result of sqrt(sin(x+0i)) and sqrt(sin(x-0i)), or log(sin(x+0i)) and log(sin(x-0i)), or etc?

I would expect the output sign of zero in imaginary part stay consistent with the input, for instance: Let x be real,

  1. For sin(x+0i) = sin(x)+0i. If sin(x) < 0, then sqrt(sin(x+0i)) = sqrt(sin(x)+0i) = sqrt(-sin(x))i; if sin(x) >= 0, then sqrt(sin(x+0i)) = sqrt(sin(x)).
  2. For sin(x-0i) = sin(x)-0i. If sin(x) < 0, then sqrt(sin(x-0i)) = sqrt(sin(x)-0i) = -sqrt(-sin(x))i; if sin(x) >= 0, then sqrt(sin(x-0i)) = sqrt(sin(x)).

@Rupt
Copy link

Rupt commented Feb 7, 2022

They appear to behave as if numerically evaluating the expansions

sin(x + iy) = sin(x)*cosh(y) + i*cos(x)*sinh(y)

cos(x + iy) = cos(x)*cosh(y) - i*sin(x)*sinh(y)

https://godbolt.org/z/Y535n4fbj

Edit: musl does this explicitly (using csin in terms of csinh).
glibc is more cryptic.

@n2vi
Copy link

n2vi commented Feb 7, 2022

I have not closely studied the Go library yet, but the origin of IEEE Floating Point choices in this regard can be found in Kahan's memo "Branch Cuts for Elementary Functions or Much Ado about Nothing's Sign Bit", which circulated in various versions, for example at https://people.freebsd.org/~das/kahan86branch.pdf.

@cherrymui cherrymui added NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. help wanted labels Feb 8, 2022
@cherrymui cherrymui added this to the Backlog milestone Feb 8, 2022
@robpike
Copy link
Contributor Author

robpike commented Feb 9, 2022

@n2vi Thanks for that link. It's very helpful - if remarkably long and roundabout - and it helped me work out what's going on. The sign of the zero is determined by the sheet of the function approaching the "slit" (as Kahan calls it) in the plane where the function has trouble from a all-is-real-and-simple standpoint.

In the case of cosine, for example, where we see 1-0i, it's because the sign of the imaginary part is negative for complex numbers with small imaginary components and positive real ones. For instance,

fmt.Println(cmplx.Cos(0.001 + .001i))

prints

(0.9999999999998332-9.99999999999989e-07i)

Note the large negative exponent on the negative imaginary part.

So the answer is simple and makes sense. Not sure it needed 20+ pages to get there, but I'm happy it's sorted.

@robpike robpike closed this as completed Feb 9, 2022
@golang golang locked and limited conversation to collaborators Feb 9, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
FrozenDueToAge help wanted NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one.
Projects
None yet
Development

No branches or pull requests

7 participants