Semi-automatic function vectorization (Julia)

Viewed 79

I'd like to (somehow) generate methods that dispatch when a function is called with one vector argument, starting from a "template".

My particular use case is plotting. I have a function that plots the behavior of a system defined by several scalar parameters, like this:

function myplot(a::Number, b::Number, c::Number)
    results = calculate(a, b, c)
    plot(results; xlabel="X Label", ylabel="Y Label", label="Signal")
end

I want to extend this method to explore how changing one parameter (a, b, or c) affects the behavior of this system.

The built-in vectorization with dot-calls (i.e., my plot.([a1 a2 a3], b, c) produces multiple plots. However, I would like to draw these as subplots within a single figure instead, while tweaking some subplot parameters to make it pretty. This is trivial to do for one parameter, like a:

function myplot(as::AbstractVecOrMat{T}, b::Number, c::Number) where T <: Number
    plots = []
    for (ix, a) in enumerate(as)
        results = calculate(a, b, c)
        push!(plots, plot(results;
            label=ix==1 ? "Signal" : nothing,
            xlabel=ix==length(as) ? "X Label" : "")
    end

    plot(plots...; layout=grid(length(plots), 1), plot_title="Effects of varying a")
end 

What is the best way to create similar functions for visualizing changes in b or c? These would be identical, except for a) the signature, b) the loop variable, and c) the plot_title. Copy-and-paste is one obvious option, but feels inelegant. Another option would be to make the vectorized version take only vectors. However, it is slightly annoying to provide [x] instead of x, and this visualization can be misleading if multiple parameters vary at the same time.

It feels like there must be an elegant metaprogramming/macro solution, but I can't quite figure it out.

Code

1 Answers

I would use Iterators.product and check if the result is dimension at most one. Number is automatically a zero-dimensional container of Number. One thing to be aware here is that if you pass in a matrix argument it will also fail the ndims check, since matrixes add two dimensions.

function myplot(as, bs, cs)
    plots = []
    it = Iterators.product(as, bs, cs)
    if ndims(it) > 1
        error("multiple iterables passed, or iterables of too high dimension")
    end
    for (ix, (a, b, c)) in enumerate(it)
        results = calculate(a, b, c)
        push!(plots, plot(results;
            label=ix==1 ? "Signal" : nothing,
            xlabel=ix==length(as) ? "X Label" : "")
    end

    plot(plots...; layout=grid(length(plots), 1), plot_title="Effects of varying a")
end 

Demonstrating using a print statement:

julia> myplot(1, 2, 3)
1 2 3

julia> myplot([1, 2, 3], 2, 3)
1 2 3
2 2 3
3 2 3

julia> myplot([1, 2], 3, [4, 5])
ERROR: multiple iterables passed, or iterables of too high dimension
Stacktrace:
 [1] error(s::String)
   @ Base ./error.jl:33
 [2] myplot(as::Vector{Int64}, bs::Int64, cs::Vector{Int64})
   @ Main ./REPL[12]:5
 [3] top-level scope
   @ REPL[16]:1
Related