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

Fix incorrect sign of atanh(complex(x,y)) if x == -1 #31061

Merged
merged 10 commits into from
Feb 19, 2019
Merged

Fix incorrect sign of atanh(complex(x,y)) if x == -1 #31061

merged 10 commits into from
Feb 19, 2019

Conversation

cafaxo
Copy link
Contributor

@cafaxo cafaxo commented Feb 13, 2019

In the case that x==-1, we have to flip the sign of ξ. Fixes the problem noticed in #31054.

Update:
The current Julia implementation was mistranscribed from the reference: The multiplication with the sign of x is missing (see #31061 (comment)). This PR currently adds that multiplication, fixing both the original sign issue and the problems (DomainError) near z=-1+0im.

In the case that x==-1, we have to flip the sign of ξ.
@cafaxo cafaxo changed the title Fix incorrect sign in atanh if x == -1 Fix incorrect sign of atanh(complex(x,y)) if x == -1 Feb 13, 2019
base/complex.jl Outdated Show resolved Hide resolved
Co-Authored-By: cafaxo <cafaxo@gmail.com>
@fredrikekre fredrikekre added domain:maths Mathematical functions needs tests Unit tests are required for this change kind:bugfix This change fixes an existing bug status:triage This should be discussed on a triage call backport 1.0 labels Feb 13, 2019
@stevengj
Copy link
Member

The η = zero(y) in the if y == 0 also seems wrong.

@Keno
Copy link
Member

Keno commented Feb 13, 2019

This should have a test to prevent this from regressing.

@fredrikekre fredrikekre removed the needs tests Unit tests are required for this change label Feb 13, 2019
@StefanKarpinski
Copy link
Sponsor Member

The η = zero(y) in the if y == 0 also seems wrong.

Should it be η = y in that case?

@chethega
Copy link
Contributor

Can we also fix atanh(prevfloat(-1.0) + 0im)?

Most complex functions return some complex mix of Inf and NaN for singularities, instead of throwing. Worst case, NaN + NaN*im seems like a better alternative.

@martinholters
Copy link
Member

I think we have at some point decided that throwing an appropriate error is more Julian than silently producing NaN. The exception is where something built-in gives NaN and, for performance, we don't do an extra check to throw instead. (0.0/0.0 would be the prime example for the latter.)

@stevengj
Copy link
Member

stevengj commented Feb 14, 2019

Arguably in the y==0 case it should be η = copysign(oftype(y,pi)/4, y), but I guess we wanted to agree with atan(real(z)) in that case? But at least we should do η = y to not discard the sign of zero.

@stevengj
Copy link
Member

stevengj commented Feb 14, 2019

Note that

julia> ccall(:catanh, ComplexF64, (ComplexF64,), complex(1.0, +0.0))
Inf + 0.7853981633974483im

julia> ccall(:catanh, ComplexF64, (ComplexF64,), complex(1.0, -0.0))
Inf - 0.7853981633974483im

so that would also argue for η = copysign(oftype(y,pi)/4, y) in the y==0 case.

(Python cmath.atanh throws a domain error here.)

@stevengj
Copy link
Member

Note that this code was from #2891 by @jiahao, which cites Kahan, but I think it mistranscribed the reference, which says:
image

@stevengj
Copy link
Member

stevengj commented Feb 14, 2019

Note that this reference doesn't have the copysign on the ξ term, but it multiplies by the sign at the end via β … it went through some extra machinations to deal with unsigned zeros on ancient hardware, apparently?

@cafaxo
Copy link
Contributor Author

cafaxo commented Feb 14, 2019

Yes, is differs in another way: It does z = β * conj(z). Thus it only has to handle the case x == 1. The case x == -1 cannot occur, in contrast to the current julia implementation.

@cafaxo
Copy link
Contributor Author

cafaxo commented Feb 14, 2019

We can fix atanh(prevfloat(-1.0) + 0im) by multiplying z with the sign of x and then multiplying (ξ, η) with that sign, as in the paper. As we do not have to handle x == -1 anymore, we can drop the copysign that this PR would currently introduce to fix the sign issue. This results in

julia> ccall(:catanh, ComplexF64, (ComplexF64,), prevfloat(-1.0) + 0im)
-18.36840028483855 + 1.5707963267948966im

julia> atanh(prevfloat(-1.0) + 0im)
-18.36840028483855 + 1.5707963267948966im

Should I add this change?

test/complex.jl Outdated Show resolved Hide resolved
@stevengj
Copy link
Member

stevengj commented Feb 14, 2019

I don't understand the rationale for the ay+ρ logic in the code (either Julia's or Kahan's). Eliminating that seems to make the code more accurate as well as simpler.

In particular, consider the following example where our current code gives quite inaccurate results:

julia> atanh(1+2.98e-154im)
176.7528106982835 + 0.7853981633974483im

julia> tanh(atanh(1+2.98e-154im))
1.0 + 5.963336292480018e-154im

In contrast, catanh(z) = ccall(:catanh, ComplexF64, (ComplexF64,), z) gives accurate results

julia> catanh(1+2.98e-154im)
177.09966410056285 + 0.7853981633974483im

julia> tanh(catanh(1+2.98e-154im))
1.0 + 2.9799999999999177e-154im

And if I simplify our atanh implementation to just eliminate the factors, I get similarly accurate results:

function myatanh(z::Complex{T}) where T<:AbstractFloat
    Ω = prevfloat(typemax(T))
    θ = sqrt(Ω)/4
    x, y = reim(z)
    ax = abs(x)
    ay = abs(y)
    if ax > θ || ay > θ #Prevent overflow
        if isnan(y)
            if isinf(x)
                return Complex(copysign(zero(x),x), y)
            else
                return Complex(real(1/z), y)
            end
        end
        if isinf(y)
            return Complex(copysign(zero(x),x), copysign(oftype(y,pi)/2, y))
        end
        return Complex(real(1/z), copysign(oftype(y,pi)/2, y))
    elseif ax==1
        ξ = copysign(log(sqrt(sqrt(4+y*y))/sqrt(ay)), x)
        η = copysign(oftype(y,pi)/2 + atan(ay/2), y)/2
    else #Normal case
        ysq = ay^2
        ξ = log1p(4x/((1-x)^2 + ysq))/4
        η = angle(Complex((1-x)*(1+x)-ysq, 2y))/2
    end
    Complex(ξ, η)
end

julia> myatanh(1+2.98e-154im)
177.09966410056285 + 0.7853981633974483im

julia> tanh(myatanh(1+2.98e-154im))
1.0 + 2.9799999999999177e-154im

What problem does the supposedly solve?

@stevengj
Copy link
Member

As a more minor matter, I also don't understand why we do log(sqrt(sqrt(4+y*y)) rather than log(4+y*y)/4

@stevengj
Copy link
Member

@cafaxo, I agree that flipping the sign seems like a good idea.

@JeffBezanson JeffBezanson removed backport 1.0 status:triage This should be discussed on a triage call labels Feb 14, 2019
@StefanKarpinski
Copy link
Sponsor Member

This seems like a minor change to everyone on triage: technically breaking, but better, so we should change it but not backport that change. Keep in mind that users of long term support versions are super conservative and want their programs to keep working as is unless the previous behavior was so horribly wrong that it couldn't be working right.

@JeffBezanson
Copy link
Sponsor Member

Ok, triage might be wrong about this one.

@chethega
Copy link
Contributor

Can't we just use a different expansion near -1.0+0im? if abs(x+1)+abs(y) < 1e-2 ξ = log((1+x)^2 +ysq)/((1-x)^2 +ysq)/4 end, or something.

Our expansion near that singularity is needlessly bad, which leads to rounding causing the DomainError. log1p is definitely the right expansion near z=0, and looks ok near z=1+0im.

@cafaxo
Copy link
Contributor Author

cafaxo commented Feb 15, 2019

The current Julia implementation mistranscribed the reference: The multiplication with the sign of x is missing (see #31061 (comment)). This PR currently adds that multiplication, fixing both the original sign issue and the problems near z=-1+0im.

@StefanKarpinski
Copy link
Sponsor Member

Should this also add some tests of for x ± 0.0im values to make sure that the branch cuts are correct?

@simonbyrne
Copy link
Contributor

There are a bunch of branch cut and limit tests here:

julia/test/complex.jl

Lines 684 to 730 in dfee945

@testset "atanh(op(z)) == op(atanh(z)) for op in (conj, -)" begin
@test isequal(atanh(complex( 0, 0)),complex( 0.0, 0.0)) #integer fallback
@test isequal(atanh(complex( 0.0, 0.0)),complex( 0.0, 0.0))
@test isequal(atanh(complex( 0.0,-0.0)),complex( 0.0,-0.0))
@test isequal(atanh(complex( 0.0, NaN)),complex( 0.0, NaN))
@test isequal(atanh(complex( 0.0, Inf)),complex( 0.0, pi/2))
@test isequal(atanh(complex( 0.0,-Inf)),complex( 0.0,-pi/2))
@test isequal(atanh(complex(-0.0, NaN)),complex(-0.0, NaN))
@test isequal(atanh(complex(-0.0, 0.0)),complex(-0.0, 0.0))
@test isequal(atanh(complex(-0.0,-0.0)),complex(-0.0,-0.0))
@test isequal(atanh(complex(-0.0, Inf)),complex(-0.0, pi/2))
@test isequal(atanh(complex(-0.0,-Inf)),complex(-0.0,-pi/2))
@test isequal(atanh(complex( 1.0, 0.0)),complex( Inf, 0.0))
@test isequal(atanh(complex(-1.0, 0.0)),complex(-Inf, 0.0))
@test isequal(atanh(complex( 5.0, Inf)),complex( 0.0, pi/2))
@test isequal(atanh(complex( 5.0,-Inf)),complex( 0.0,-pi/2))
@test isequal(atanh(complex( 5.0, NaN)),complex( NaN, NaN))
@test isequal(atanh(complex(-5.0, Inf)),complex(-0.0, pi/2))
@test isequal(atanh(complex(-5.0,-Inf)),complex(-0.0,-pi/2))
@test isequal(atanh(complex(-5.0, NaN)),complex( NaN, NaN))
@test isequal(atanh(complex( Inf, 0.0)),complex(0.0, pi/2))
@test isequal(atanh(complex( Inf,-0.0)),complex(0.0,-pi/2))
@test isequal(atanh(complex( Inf, 5.0)),complex(0.0, pi/2))
@test isequal(atanh(complex( Inf,-5.0)),complex(0.0,-pi/2))
@test isequal(atanh(complex( Inf, Inf)),complex(0.0, pi/2))
@test isequal(atanh(complex( Inf,-Inf)),complex(0.0,-pi/2))
@test isequal(atanh(complex( Inf, NaN)),complex(0.0, NaN))
@test isequal(atanh(complex(-Inf, 0.0)),complex(-0.0, pi/2))
@test isequal(atanh(complex(-Inf,-0.0)),complex(-0.0,-pi/2))
@test isequal(atanh(complex(-Inf, 5.0)),complex(-0.0, pi/2))
@test isequal(atanh(complex(-Inf,-5.0)),complex(-0.0,-pi/2))
@test isequal(atanh(complex(-Inf, Inf)),complex(-0.0, pi/2))
@test isequal(atanh(complex(-Inf,-Inf)),complex(-0.0,-pi/2))
@test isequal(atanh(complex(-Inf, NaN)),complex(-0.0, NaN))
@test isequal(atanh(complex( NaN, 0.0)),complex( NaN, NaN))
@test isequal(atanh(complex( NaN,-0.0)),complex( NaN, NaN))
@test isequal(atanh(complex( NaN, 5.0)),complex( NaN, NaN))
@test isequal(atanh(complex( NaN,-5.0)),complex( NaN, NaN))
@test isequal(atanh(complex( NaN, Inf)),complex( 0.0, pi/2))
@test isequal(atanh(complex( NaN,-Inf)),complex( 0.0,-pi/2))
@test isequal(atanh(complex( NaN, NaN)),complex( NaN, NaN))
end

so can add those there.

@StefanKarpinski
Copy link
Sponsor Member

I guess they were not incorrect previously then. Carry on.

base/complex.jl Outdated Show resolved Hide resolved
@cafaxo
Copy link
Contributor Author

cafaxo commented Feb 18, 2019

I have added tests for the cases that were fixed after replacing η = zero(y) with η = y.

@simonbyrne
Copy link
Contributor

simonbyrne commented Feb 18, 2019

Looks like the CI failures are due to getting slightly different values here:

julia> acos(cos([0.5 0.1; -0.2 0.3]))
2×2 Array{Complex{Float64},2}:
0.5-5.55112e-17im 0.1-2.77556e-17im
-0.2+2.498e-16im 0.3-3.46945e-16im

These should be updated with the new values.

@simonbyrne simonbyrne closed this Feb 18, 2019
@simonbyrne simonbyrne reopened this Feb 18, 2019
@simonbyrne simonbyrne merged commit 66341a3 into JuliaLang:master Feb 19, 2019
@simonbyrne
Copy link
Contributor

Thanks @cafaxo!

@simonbyrne simonbyrne added status:triage This should be discussed on a triage call backport 1.0 labels Feb 19, 2019
@StefanKarpinski
Copy link
Sponsor Member

It would be nice to clean up the commit message when squashing this kind of PR. The GitHub UI makes it quite easy to do—you just need to edit the message in the web interface.

@simonbyrne
Copy link
Contributor

Good point, will do in future.

@fp4code
Copy link
Contributor

fp4code commented Mar 27, 2019

A partial backport should be done! This is not about the choice of the cut, just
tanh(atanh(-1.0 + 0.1im)) ≈ 1.0 + 0.1im is a wrong result!

@KristofferC KristofferC removed the status:triage This should be discussed on a triage call label Aug 26, 2019
KristofferC pushed a commit that referenced this pull request Aug 26, 2019
* Fix incorrect sign in atanh

In the case that x==-1, we have to flip the sign of ξ.

* Formatting: Add space after comma

Co-Authored-By: cafaxo <cafaxo@gmail.com>

* Add test

* Do not drop sign of zero

* Flip sign to avoid a DomainError

This fixes atanh(prevfloat(-1.0) + 0im)

* Test all four combinations

Co-Authored-By: cafaxo <cafaxo@gmail.com>

* Tests for expressions that were signed incorrectly

* Use the correct type for 1

* Update doc

* Update doc: Remove trailing whitespace

(cherry picked from commit 66341a3)
KristofferC pushed a commit that referenced this pull request Aug 27, 2019
* Fix incorrect sign in atanh

In the case that x==-1, we have to flip the sign of ξ.

* Formatting: Add space after comma

Co-Authored-By: cafaxo <cafaxo@gmail.com>

* Add test

* Do not drop sign of zero

* Flip sign to avoid a DomainError

This fixes atanh(prevfloat(-1.0) + 0im)

* Test all four combinations

Co-Authored-By: cafaxo <cafaxo@gmail.com>

* Tests for expressions that were signed incorrectly

* Use the correct type for 1

* Update doc

* Update doc: Remove trailing whitespace

(cherry picked from commit 66341a3)
@KristofferC KristofferC mentioned this pull request Dec 3, 2019
56 tasks
KristofferC pushed a commit that referenced this pull request Feb 20, 2020
* Fix incorrect sign in atanh

In the case that x==-1, we have to flip the sign of ξ.

* Formatting: Add space after comma

Co-Authored-By: cafaxo <cafaxo@gmail.com>

* Add test

* Do not drop sign of zero

* Flip sign to avoid a DomainError

This fixes atanh(prevfloat(-1.0) + 0im)

* Test all four combinations

Co-Authored-By: cafaxo <cafaxo@gmail.com>

* Tests for expressions that were signed incorrectly

* Use the correct type for 1

* Update doc

* Update doc: Remove trailing whitespace

(cherry picked from commit 66341a3)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
domain:maths Mathematical functions kind:bugfix This change fixes an existing bug
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet