Complexifying pi

I want to make a type that internally stores values in a Complex form. I assumed that I should simply convert any Real input. However, some math constants give an error doing this:

julia> Complex(2.0f0)
2.0f0 + 0.0f0im

julia> Complex(pi/2)
1.5707963267948966 + 0.0im

julia> Complex(pi)
ERROR: MethodError: no method matching Irrational{:π}(::Int64)
 ...
Stacktrace:
 [1] convert(::Type{Irrational{:π}}, ::Int64) at ./number.jl:7
 [2] oftype(::Irrational{:π}, ::Int64) at ./essentials.jl:334
 [3] zero(::Irrational{:π}) at ./number.jl:262
 [4] Complex(::Irrational{:π}) at ./complex.jl:16
 [5] top-level scope at none:0

The same happens for . I caught it only because of a very arbitrary unit test. I’m not saying this behavior is irrational (har har!), but it is arguably not intuitive, so I would love to know if it’s intentional.

For the Real types I’ve thought to try, it does always work to add 0im. Not a big deal, but it feels a bit non-Julian to use addition for what is fundamentally a type conversion. I’m open to equally simple yet general alternatives.

a easy solution would be Complex(1*pi), but i’m checking about the irrational type:

update, i found this:
https://julialang.org/blog/2017/03/piday
basically, the Irrational type gives arbitrary precision for math constants, allowing BigFloat(pi) to be pi with more digits, instead of a Float64 aproximation, the blog post explains that with more detail

1 Like

To add to @longemen3000’s explanation: the thing that does work is

julia> Complex{Float64}(pi)
3.141592653589793 + 0.0im

because now we can infer to what precision you want to have it.

4 Likes

The question is, should those types compose? Should there be a Complex{Irrational{...}} type?

in this case, i think is preferable to allow the composition, and transform the irrational to a concrete numeric type when an operation is performed, but the problem is that the Complex type asigns the type of the stored numbers to be the same:

julia> x = Complex(1,BigFloat(1))
1.0 + 1.0im

julia> typeof(x)
Complex{BigFloat}

julia> typeof(x.re)
BigFloat

as there is no implementation of zero(<:Irrational) (and it doesn’t make sense, zero and one aren’t irrationals), the generation of the Complex{Irrational{…}}` fails. this can be avoided at expense of hiding the transformation from the user, using a sane default (Float64?)

#something like this?
Complex(x::Irrational) = Complex{Float64}(x)

The type exists already:

julia> complex(pi, pi) |> typeof
Complex{Irrational{:π}}

but the point is that the parameter of Complex{T} is the type of both the real and the imaginary parts, so the only way to build a Complex{Irrational{...}} is to do complex(x, x) with x isa Irrational.

If one wants to have complex(pi) with the highest precision possible, complex(big(pi)) is the only solution currently.

For more information see:

3 Likes

OTOH, they are numbers that can be computed to arbitrarily high precision (trivially), so there’s also no particular reason not to implement such methods.

Not sure that’s helpful in the grander scheme of what you’re saying, just thought it might be a useful way to think about it.

1 Like

Irrationals are represented by singletons. It would be easy to have Irrational{:zero} (after all, Irrational represents a non-terminating turing machine that writes better and better approximations of a real number onto an infinite tape, i.e. is a glorified function pointer), but Complex does not permit real and imaginary parts to have different types. This means that e.g.

julia> Complex(pi, ℯ) |>typeof
Complex{Float64}

Even if we had irrational zero, this would not be enough to represent Complex(pi, true_zero).

2 Likes

These things were already discussed in the issue I linked above, including the irrational zero.

Actually, there are a couple of ways to define a complex irrational number which retains its irrational features:

julia> Complex{Real}(pi, 0)
π = 3.1415926535897... + 0im

[...]

julia> Base.complex(x::T) where {T<:Irrational} = Complex{Union{T, Int}}(x, 0)

julia> complex(pi)
π = 3.1415926535897... + 0im

but they aren’t super efficient (at least, they weren’t two years ago) because of type instability, see

2 Likes

Thanks to @longemen3000 and @giordano for the pointers to the discussions.

To be clear, I’m not personally after some abstract notion of complex irrationals, which I’ve never given any thought to. I was looking for a way to convert to a complex representation built from whatever real type the caller sends. And yes, it’s easy for me to actually do.

My point is the same one made by @giordano on 28 July 2017 in the thread he gave: “I’d say that it’s better to automatically convert complex(pi) to Complex{Float64} (first version of this PR) rather than throwing an error for a simple conversion.”

Justifications of current behavior based on preserving the abstraction of pi to irrationality or arbitrary precision then have to explain why Complex(pi,0) and pi/2 not only work but produce results based on Float64 (which is what the majority of scientific computation expects).

The treatment of pi as a special kind of value is only a mm deep. Indeed, methodswith(typeof(pi)) returns no results in base. Irrational pi should be the use case that you go out of your way to find, as opposed to the current situation of the tail wagging the dog. I’d even go so far as to say it ought to be an explicit import.

1 Like

complex(pi, 0) promotes the arguments to a common type, that is Float64. pi/2 is a Float64.

This is more useful:

julia> methodswith(AbstractIrrational)
[1] *(x::Bool, y::AbstractIrrational) in Base at irrationals.jl:135
[2] *(x::AbstractIrrational, y::AbstractIrrational) in Base at irrationals.jl:133
[3] +(x::AbstractIrrational, y::AbstractIrrational) in Base at irrationals.jl:133
[4] -(x::AbstractIrrational) in Base at irrationals.jl:131
[5] -(x::AbstractIrrational, y::AbstractIrrational) in Base at irrationals.jl:133
[6] /(x::AbstractIrrational, y::AbstractIrrational) in Base at irrationals.jl:133
[7] <(x::Float32, y::AbstractIrrational) in Base at irrationals.jl:76
[8] <(x::Float64, y::AbstractIrrational) in Base at irrationals.jl:74
[9] <(x::Float16, y::AbstractIrrational) in Base at irrationals.jl:78
[10] <(x::BigFloat, y::AbstractIrrational) in Base at irrationals.jl:82
[11] <(x::Rational{BigInt}, y::AbstractIrrational) in Base at irrationals.jl:117
[12] <(x::AbstractIrrational, y::AbstractIrrational) in Base at irrationals.jl:61
[13] <(x::AbstractIrrational, y::Float64) in Base at irrationals.jl:73
[14] <(x::AbstractIrrational, y::Float32) in Base at irrationals.jl:75
[15] <(x::AbstractIrrational, y::Float16) in Base at irrationals.jl:77
[16] <(x::AbstractIrrational, y::BigFloat) in Base at irrationals.jl:79
[17] <(x::AbstractIrrational, y::Rational{BigInt}) in Base at irrationals.jl:116
[18] <(x::AbstractIrrational, y::Rational{T}) where T in Base at irrationals.jl:99
[19] <(x::Rational{T}, y::AbstractIrrational) where T in Base at irrationals.jl:108
[20] <=(x::AbstractIrrational, y::AbstractIrrational) in Base at irrationals.jl:66
[21] <=(x::AbstractIrrational, y::AbstractFloat) in Base at irrationals.jl:86
[22] <=(x::AbstractFloat, y::AbstractIrrational) in Base at irrationals.jl:87
[23] <=(x::AbstractIrrational, y::Rational) in Base at irrationals.jl:119
[24] <=(x::Rational, y::AbstractIrrational) in Base at irrationals.jl:120
[25] ==(::AbstractIrrational, ::AbstractIrrational) in Base at irrationals.jl:57
[26] ==(x::AbstractIrrational, y::Real) in Base at irrationals.jl:69
[27] ==(x::Real, y::AbstractIrrational) in Base at irrationals.jl:70
[28] ^(::Irrational{:ℯ}, x::AbstractIrrational) in Base.MathConstants at mathconstants.jl:91
[29] ^(x::AbstractIrrational, y::AbstractIrrational) in Base at irrationals.jl:133
[30] big(x::AbstractIrrational) in Base at irrationals.jl:174
[31] isfinite(::AbstractIrrational) in Base at irrationals.jl:122
[32] isinteger(::AbstractIrrational) in Base at irrationals.jl:123
[33] isone(::AbstractIrrational) in Base at irrationals.jl:125
[34] iszero(::AbstractIrrational) in Base at irrationals.jl:124
[35] rationalize(::Type{T}, x::AbstractIrrational) where T in Base at irrationals.jl:91
1 Like

I see I missed the modest number methods. I find iszero and isone to be quite bizarre in the context of errors for zero(pi) and one(pi)!

More broadly, I fail to see how the “common type” of two arguments is not the type of either one of them. Logical consistency would imply that pi/2 is irrational, no?

I reiterate: if I have to import special functions, or linear algebra for that matter, because they aren’t of interest to many who want to use Julia, then I don’t see why this stuff is part of the base.

1 Like

Remember that irrationals in Julia are singletons. Rather than “common type” I should have said “the simplest type to which both numbers can be promoted”.

There have been quite some discussions on GitHub about this. It was decided in the end that any time you perform any operation on an Irrational it “collapses” onto a concrete AbstractFloat, either Float64 or BigFloat or whatever. Even though it could be interesting, It’d be very tricky to have an arithmetic for irrationals so that any elementary operations with irrationals gives another irrational, and so.

There is no general reason that should hold. It depends on the operation, eg /(::Int, ::Int) gives a Float64. Even for straightforward conversion (promote_type), you can run into cases where the promoted type is neither argument.

This is slightly different because in 1/1 the arguments are converted to an AbstractFloat, no promotion rule involved here. In the case of complex(pi, 0) the only operations performed is promote, but the promotion rule for AbstractIrrational and Int is to promote to Float64

My general point was that for f(::T1, ::T2, ...)::S, there is no rule that S has to be one of T1, T2, …, or an applicable parametric type with S, regardless of whether f is a constructor or a function.

julia> promote_type(AbstractIrrational, Int)
Float64

is actually a nice example of how this can fail.

Regarding the original question: one solution I did not see above is

julia> Complex(float(pi))
3.141592653589793 + 0.0im

which could actually be a fallback for the single-argument Complex constructor.

1 Like

which was rejected in https://github.com/JuliaLang/julia/pull/22928

1 Like

I am not sure we are talking about the same thing — the PR is about something else, using float was mentioned in a comment and was generally approved.

The PR had initially the method you suggested.

Quoting verbatim from the comment you linked:

He suggested to manually do complex(float(pi)) and keep complex(pi) erroring

1 Like

I appreciate the great, thoughtful replies. The / example isn’t perfect; everyone understands rational/real numbers to generalize integers, while the relationship between Irrational and Float64 is less intuitive. But the larger point is taken.

(Also, why complex(1), Complex(1), float(1), but not Float(1)…never mind, I probably don’t want to know.)

I do still find a language that offers pi and 2pi by default, yet regards them as fundamentally different types of numerical objects, counter to my expectations from decades of both mathematics and computing. I still don’t see the payoff that it provides, aside from modest convenience in showing off VPA. Unlike most of the choices in base Julia, this one feels oddly skewed toward a niche form of computing.

There is probably no use of an Irrational type that doesn’t end in either full symbolic math or bailing out to a float type. Now, I have no problem with reverting to float. I was caught off-guard when that didn’t happen. And there are a bunch of ways to avoid/force it, I know.

I also get that people have thought about these things before, and decisions are made to optimize different goals. Consider this commentary more than criticism. I was relating the experience of one aspiring package author who was trying to follow the use of parameterized types, terminology like Real, and formal-sounding functions like one and zero, but still ended up getting burned in the case of the single input pi. The community can decide for itself whether it finds that concerning, or just the result of an old dog slow to learn new tricks.

4 Likes