julia icon indicating copy to clipboard operation
julia copied to clipboard

mod(::Complex{<:Integer}, ::Integer) and div etc.

Open stevengj opened this issue 5 years ago • 18 comments

As discussed on discourse, this seems like the only sensible definition:

mod(z::Complex{<:Integer}, n::Integer) = Complex(mod(real(z), n), mod(imag(z), n))

and similarly for div, rem (and probably divrem).

Should be easy to create a patch (much simpler than #35374): just add the 1-line definitions analogous to the one above, docs, news, and a test.

stevengj avatar Sep 03 '20 13:09 stevengj

I disagree, I think it should be

mod(z::Complex, n) = Complex(mod(real(z), n), mod(imag(z), n))

Since this defines mod recursively, you can just let mod of a Real coefficient to handle the dispatch

chakravala avatar Sep 03 '20 14:09 chakravala

Yes, that's a reasonable further generalization, although I would be more conservative and restrict it to

mod(z::Complex, n::Real) = ...

If n is not real, then splitting the computation into real and imaginary parts makes less sense (especially since Complex(a,b) expects real arguments).

stevengj avatar Sep 03 '20 14:09 stevengj

Edit: sorry I was mistaken in my last post.

I don't quite see the argument for restricting the dispatch though, if it errors, then it errors, why restrict the dispatch?

And why is Complex(1+im,1-im) not allowed? Nested complex numbers are quite reasonable.

chakravala avatar Sep 03 '20 14:09 chakravala

The problem with dispatching to something that is unlikely to ever be implemented is that the error message is needlessly confusing, both to users and to future implementors — if we want to support mod(::Complex, ::Complex), we will add that method (ala #35374), opposed to implementing mod(::Real, ::Complex).

As for generalizing the Complex constructor, that's a better subject for a separate issue.

stevengj avatar Sep 03 '20 14:09 stevengj

Also, if n is not real, the return value is not necessarily Complex. For example, if n is a quaternion then the return value would not be Complex.

stevengj avatar Sep 03 '20 16:09 stevengj

Hello @stevengj @chakravala. I would like to work on this issue. Can you please guide me? I have been using Julia for quite some time but this is the first time I decided to contribute.

prithvitewatia avatar Feb 18 '21 16:02 prithvitewatia

@prithvitewatia, welcome!

Basically you just need to add a 1-line definition similar to the one discussed above, e.g. mod(z::Complex, n::Real) = Complex(mod(real(z), n), mod(imag(z), n)), to base/complex.jl, and also add a test and update NEWS.md, and then submit a PR.

See also https://github.com/JuliaLang/julia/blob/master/CONTRIBUTING.md

stevengj avatar Feb 19 '21 04:02 stevengj

Is this the obvious definition? I would have guessed that it was this:

mod(z::Complex, n::Real) = Complex(mod(real(z), n), imag(z))

That is, perform one identification on the complex plane, equating z and z+n. Performing two identifications, the second of which rotates n to lie in the imaginary direction, z ~ z + im * n, seems a strange choice to me. What's the logic?

For example, I would expect mod(z, 2pi*im) to leave the real part alone, and identify only in the imaginary direction. This maps you to the fundamental domain of exp. A similar rotation to identify also z ~ z + 2pi in the real direction would be surprising.

mcabbott avatar Feb 19 '21 11:02 mcabbott

@mcabbott I am sorry but I am not able to understand your point. Could you please provide some more examples?

prithvitewatia avatar Feb 19 '21 13:02 prithvitewatia

Sure, I can try.

Ordinary trig functions are periodic on the real line, e.g. tand(5) == tand(5 + 180) == tand(5 + 2*180), and this fact is captured by mod(5, 180) == mod(5 + 180, 180): the function x -> mod(x, 180) standardises the argument, making the function one-to-one.

This periodicity still exists in the complex plane: tand(5+7im) ≈ tand(5+7im + 180). The step here is still in the real direction. There is no extra periodicity in the imaginary direction, there is nothing special at all about tand(5+7im + 180im). If mod is still going to capture this behaviour, then mod(x, 180) needs to act only on the real part.

Other functions can have periods in other directions, e.g. f(x) = tand((1+im) * x) obeys f(17) ≈ f(17 + 90*(1-im)). And other functions can be periodic in both real and imaginary directions (e.g. Jacobi trig). But even then, there's little reason to think the period would be the same in the real and imaginary directions (rather than independent parameters).

Now maybe there are other uses of complex mod which I haven't thought of. But the linked discourse thread doesn't mention any.

mcabbott avatar Feb 19 '21 14:02 mcabbott

@mcabbott Thanks for the explanation, now I get your point. But now I am confused about which implementation we should have. This is because WolframAlpha uses mod(z::complex,n::Real)=Complex(mod(real(z),n),mod(imag(z),n)) whereas languages such as Python and Octave do not support complex types for modulo operator or function.

prithvitewatia avatar Feb 19 '21 15:02 prithvitewatia

I didn't think to look, but here's what Mathematica does:

Mod[5+I,10]
Mod[6+I,10] (* real output in -5 to 5, although Mod[6, 10] == 6 *)
Mod[15+I,10]
Mod[16+I,10]

Out[175]= 5+I
Out[176]= -4+I
Out[177]= -5+I
Out[178]= -4+I

Mod[3+8I,10]
Mod[3+18I,10]
Mod[3+25I,10] (* imaginary output in -5 to 5 *)

Out[182]= 3-2 I
Out[183]= 3-2 I
Out[184]= 3+5 I

I think they are getting this from the equivalent of div, help says this:

Mod[m,n] is equivalent to m-n Quotient[m,n]. 
Mod works with complex numbers, using its definition in terms of Quotient.
Quotient[m,n] is equivalent to Floor[m/n] for integers m and n. 

but doesn't seem to say much about non-integer m,n. The behaviour is:

Quotient[5, 10]
Quotient[25, 10]

Out[143]= 0
Out[144]= 2

Quotient[14+2I, 10]
Quotient[14+22I, 10]
Quotient[14+22I, 10I]

Out[154]= 1
Out[155]= 1+2 I
Out[156]= 2-I

Floor[3/2 + 5I/2]
Floor[-3/2 - 5I/2]  (* rounds down *)

Out[188]= 1+2 I
Out[189]= -2-3 I

Quotient[14+22I, 1+2I]  (* rounds to nearest? *)
(14+22I)/( 1+2I) // N
Floor[11.6` -1.2` I]

Out[173]= 12-I
Out[174]= 11.6 -1.2 I
Out[175]= 11-2 I

Note that in Julia, floor(1+im) and div(1+im,2) are errors.

mcabbott avatar Feb 19 '21 17:02 mcabbott

Here's another argument:

x ≡ y (mod z) means that, for some integer n, we have x = y + n * z. The function is mod is supposed to be such that mod(x,z) == mod(y,z) if and only if x ≡ y (mod z).

That makes sense for integer x,y,z. It also makes sense for real-valued x,y,z, but we do not change the fact that n is an integer. The obvious extension to complex numbers is to allow complex x,y,z, but without changing the fact that n is an integer.

The proposed definition does something different. For complex x,y and real z, it takes n to be a real integer plus a complex integer. That corresponds to something like ∃ x′ such that x ≡ x′ (mod z) and x′ ≡ y (mod im*z). What's the argument for this being natural, or useful? With the simpler definition, this one would be mod(mod(x, n), im*n), explicitly acting in two directions.

mcabbott avatar Feb 22 '21 13:02 mcabbott

@mcabbott, the proposed PR only adds z mod n where n is real, not mod z. I think that is uncontroversial?

stevengj avatar Mar 10 '21 17:03 stevengj

@stevengj I still think it should be mod(z::Complex, n::Real) = Complex(mod(real(z), n), imag(z)) for this case. I've tried to argue various ways above. The proposed behaviour seems right for a tuple, mod.((x,y), n) but not for z = x+im*y; the complex plane has more structure.

What's the argument for this behaviour? Is it standard somewhere that I don't know about? Is it copied from some other language?

mcabbott avatar Mar 11 '21 01:03 mcabbott

Can someone explain why when I try to find the order of 10(mod 71) online I get n = 35 but when I try Mod(10^35 , 71) I get 24? When I compute Mod(10^70, 71) I get 0. What's going on here?

jdvalderra avatar Apr 27 '21 23:04 jdvalderra

@jdvalderra questions should go to discourse. Github is made for development. What you're seeing is that Julia uses Int64, so 10^70==0. You should use mod(big(10)^70,71) instead.

oscardssmith avatar Apr 27 '21 23:04 oscardssmith

The proposed definition treats complex numbers as a vector space over the reals (or integers), and inherits much structure from it. (In addition, there is complex multiplication.)

Scalar multiplication and scalar division are inherited from being a vector space. div/mod/rem can be inherited in the same way. The special structure imbued by complex multiplication does not play a role here.

Other languages (e.g. Mathematica) make this choice as well. It's also a natural choice for Julia, given that complex numbers are represented as a vector of its real and imaginary components.

eschnett avatar Sep 13 '22 16:09 eschnett