13
votes

In julia, one can find (supposedly) efficient implementations of the min/minimum and max/maximum over collections of real numbers. As these concepts are not uniquely defined for complex numbers, I was wondering if a parametrized version of these functions was already implemented somewhere.

I am currently sorting elements of the array of interest, then taking the last value, which is as far as I know, much more costly than finding the value with the maximum absolute value (or something else).

This is mostly to reproduce the Matlab behavior of the max function over complex arrays.

Here is my current code

a = rand(ComplexF64,4)
b = sort(a,by  = (x) -> abs(x))
c = b[end]

The probable function call would look like

c = maximum/minimum(a,by=real/imag/abs/phase)

EDIT Some performance tests in Julia 1.5.3 with the provided solutions

function maxby0(f,iter)
    b = sort(iter,by  = (x) -> f(x))
    c = b[end]
end

function maxby1(f, iter)
    reduce(iter) do x, y
        f(x) > f(y) ? x : y
    end
end

function maxby2(f, iter; default = zero(eltype(iter)))
    isempty(iter) && return default
    res, rest = Iterators.peel(iter)
    fa = f(res)
    for x in rest
        fx = f(x)
        if fx > fa
            res = x
            fa = fx
        end
    end

    return res
end

compmax(CArray) = CArray[ (abs.(CArray) .== maximum(abs.(CArray))) .& (angle.(CArray) .== maximum( angle.(CArray))) ][1]


Main.isless(u1::ComplexF64, u2::ComplexF64) = abs2(u1) < abs2(u2)

function maxby5(arr)
    arr_max = arr[argmax(map(abs, arr))]
end

a = rand(ComplexF64,10)

using BenchmarkTools
@btime maxby0(abs,$a)
@btime maxby1(abs,$a)
@btime maxby2(abs,$a)
@btime compmax($a)
@btime maximum($a)
@btime maxby5($a)

Output for a vector of length 10:

>841.653 ns (1 allocation: 240 bytes)
>214.797 ns (0 allocations: 0 bytes)
>118.961 ns (0 allocations: 0 bytes)
>Execution fails
>20.340 ns (0 allocations: 0 bytes)
>144.444 ns (1 allocation: 160 bytes)

Output for a vector of length 1000:

>315.100 μs (1 allocation: 15.75 KiB)
>25.299 μs (0 allocations: 0 bytes)
>12.899 μs (0 allocations: 0 bytes)
>Execution fails
>1.520 μs (0 allocations: 0 bytes)
>14.199 μs (1 allocation: 7.94 KiB)

Output for a vector of length 1000 (with all comparisons made with abs2):

>35.399 μs (1 allocation: 15.75 KiB)
>3.075 μs (0 allocations: 0 bytes)
>1.460 μs (0 allocations: 0 bytes)
>Execution fails
>1.520 μs (0 allocations: 0 bytes)
>2.211 μs (1 allocation: 7.94 KiB)

Some remarks :

  • Sorting clearly (and as expected) slows the operations
  • Using abs2 saves a lot of performance (expected as well)

To conclude :

  • As a built-in function will provide this in 1.7, I will avoid using the additional Main.isless method, though it is all things considered the most performing one, to not modify the core of my julia
  • The maxby1 and maxby2 allocate nothing
  • The maxby1 feels more readable

the winner is therefore Andrej Oskin!

EDIT n°2 a new benchmark using the corrected compmax implementation

julia> @btime maxby0(abs2,$a)
  36.799 μs (1 allocation: 15.75 KiB)
julia> @btime maxby1(abs2,$a)
  3.062 μs (0 allocations: 0 bytes)
julia> @btime maxby2(abs2,$a)
  1.160 μs (0 allocations: 0 bytes)
julia> @btime compmax($a)
  26.899 μs (9 allocations: 12.77 KiB)
julia> @btime maximum($a)
  1.520 μs (0 allocations: 0 bytes)
julia> @btime maxby5(abs2,$a)
2.500 μs (4 allocations: 8.00 KiB)
4

4 Answers

7
votes

In Julia 1.7 you can use argmax

julia> a = rand(ComplexF64,4)
4-element Vector{ComplexF64}:
  0.3443509906876845 + 0.49984979589871426im
  0.1658370274750809 + 0.47815764287341156im
  0.4084798173736195 + 0.9688268736875587im
 0.47476987432458806 + 0.13651720575229853im

julia> argmax(abs2, a)
0.4084798173736195 + 0.9688268736875587im

Since it will take some time to get to 1.7, you can use the following analog

maxby(f, iter) = reduce(iter) do x, y
                   f(x) > f(y) ? x : y
                 end
julia> maxby(abs2, a)
0.4084798173736195 + 0.9688268736875587im

UPD: slightly more efficient way to find such maximum is to use something like

function maxby(f, iter; default = zero(eltype(iter)))
    isempty(iter) && return default
    res, rest = Iterators.peel(iter)
    fa = f(res)
    for x in rest
        fx = f(x)
        if fx > fa
            res = x
            fa = fx
        end
    end

    return res
end
5
votes

According to octave's documentation (which presumably mimics matlab's behaviour):

 For complex arguments, the magnitude of the elements are used for
 comparison.  If the magnitudes are identical, then the results are
 ordered by phase angle in the range (-pi, pi].  Hence,

      max ([-1 i 1 -i])
          => -1

 because all entries have magnitude 1, but -1 has the largest phase
 angle with value pi.

Therefore, if you'd like to mimic matlab/octave functionality exactly, then based on this logic, I'd construct a 'max' function for complex numbers as:

function compmax( CArray )
    Absmax   = CArray[   abs.(CArray) .== maximum(  abs.(CArray)) ]
    Totalmax = Absmax[ angle.(Absmax) .== maximum(angle.(Absmax)) ]
    return Totalmax[1]
end

(adding appropriate typing as desired).

Examples:

Nums0 = [ 1, 2, 3 + 4im, 3 - 4im, 5 ];   compmax( Nums0 )
# 1-element Array{Complex{Int64},1}:
#  3 + 4im

Nums1 = [ -1, im, 1, -im ];   compmax( Nums1 )
# 1-element Array{Complex{Int64},1}:
#  -1 + 0im
4
votes

If this was a code for my computations, I would have made my life much simpler by:

julia> Main.isless(u1::ComplexF64, u2::ComplexF64) = abs2(u1) < abs2(u2)

julia> maximum(rand(ComplexF64, 10))
0.9876138798492835 + 0.9267321874614858im

This adds a new implementation for an existing method in Main. Therefore for a library code it is not an elegant idea, but it will you get where you need it with the least effort.

3
votes

The "size" of a complex number is determined by the size of its modulus. You can use abs for that. Or get 1.7 as Andrej Oskin said.

julia> arr = rand(ComplexF64, 10)
10-element Array{Complex{Float64},1}:
 0.12749588414783353 + 0.09918182087026373im
  0.7486501790575264 + 0.5577981676269863im
  0.9399200789666509 + 0.28339836191094747im
  0.9695470502095325 + 0.9978696209350371im
  0.6599207157942191 + 0.0999992072342546im
 0.30059521996405425 + 0.6840859625686171im
 0.22746651600614132 + 0.33739559003514885im
  0.9212471084010287 + 0.2590484924393446im
    0.74848598947588 + 0.41129443181449554im
  0.8304447441317468 + 0.8014240389454632im
julia> arr_max = arr[argmax(map(abs, arr))]
0.9695470502095325 + 0.9978696209350371im
julia> arr_min = arr[argmin(map(abs, arr))]
0.12749588414783353 + 0.09918182087026373im